fix issue with tool schemas and google

This commit is contained in:
Dax Raad 2025-06-17 11:27:07 -04:00
parent 57b3051024
commit ee91f31313
15 changed files with 82 additions and 19 deletions

View file

@ -86,6 +86,9 @@
"sharp", "sharp",
"esbuild", "esbuild",
], ],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch",
},
"overrides": { "overrides": {
"zod": "3.24.2", "zod": "3.24.2",
}, },

View file

@ -37,5 +37,8 @@
"esbuild", "esbuild",
"protobufjs", "protobufjs",
"sharp" "sharp"
] ],
"patchedDependencies": {
"ai@4.3.16": "patches/ai@4.3.16.patch"
}
} }

View file

@ -93,8 +93,11 @@ const cli = yargs(hideBin(process.argv))
if (Installation.VERSION === latest) return if (Installation.VERSION === latest) return
const method = await Installation.method() const method = await Installation.method()
if (method === "unknown") return if (method === "unknown") return
await Installation.upgrade(method, latest).catch(() => {}) await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest }) Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => {})
})() })()
await proc.exited await proc.exited

View file

@ -117,6 +117,6 @@ export namespace Installation {
export async function latest() { export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest") return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json()) .then((res) => res.json())
.then((data) => data.tag_name.slice(1)) .then((data) => data.tag_name.slice(1) as string)
} }
} }

View file

@ -23,6 +23,7 @@ import { ModelsDev } from "./models"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Auth } from "../auth" import { Auth } from "../auth"
import { TaskTool } from "../tool/task" import { TaskTool } from "../tool/task"
import { GlobalConfig } from "../global/config"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
@ -257,7 +258,10 @@ export namespace Provider {
} }
export async function defaultModel() { export async function defaultModel() {
const [provider] = await list().then((val) => Object.values(val)) const cfg = await GlobalConfig.get()
const provider = await list()
.then((val) => Object.values(val))
.then((x) => x.find((p) => !cfg.provider || cfg.provider === p.info.id))
if (!provider) throw new Error("no providers found") if (!provider) throw new Error("no providers found")
const [model] = sort(Object.values(provider.info.models)) const [model] = sort(Object.values(provider.info.models))
if (!model) throw new Error("no models found") if (!model) throw new Error("no models found")
@ -285,11 +289,16 @@ export namespace Provider {
TaskTool, TaskTool,
TodoReadTool, TodoReadTool,
] ]
const TOOL_MAPPING: Record<string, Tool.Info[]> = { const TOOL_MAPPING: Record<string, Tool.Info[]> = {
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"), anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
openai: TOOLS, openai: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS, google: TOOLS,
} }
export async function tools(providerID: string) { export async function tools(providerID: string) {
/* /*
const cfg = await Config.get() const cfg = await Config.get()
@ -301,6 +310,38 @@ export namespace Provider {
return TOOL_MAPPING[providerID] ?? TOOLS return TOOL_MAPPING[providerID] ?? TOOLS
} }
function optionalToNullable(schema: z.ZodTypeAny): z.ZodTypeAny {
if (schema instanceof z.ZodObject) {
const shape = schema.shape
const newShape: Record<string, z.ZodTypeAny> = {}
for (const [key, value] of Object.entries(shape)) {
const zodValue = value as z.ZodTypeAny
if (zodValue instanceof z.ZodOptional) {
newShape[key] = zodValue.unwrap().nullable()
} else {
newShape[key] = optionalToNullable(zodValue)
}
}
return z.object(newShape)
}
if (schema instanceof z.ZodArray) {
return z.array(optionalToNullable(schema.element))
}
if (schema instanceof z.ZodUnion) {
return z.union(
schema.options.map((option: z.ZodTypeAny) =>
optionalToNullable(option),
) as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]],
)
}
return schema
}
export const ModelNotFoundError = NamedError.create( export const ModelNotFoundError = NamedError.create(
"ProviderModelNotFoundError", "ProviderModelNotFoundError",
z.object({ z.object({

View file

@ -497,7 +497,7 @@ export namespace Session {
msgs.map(toUIMessage).filter((x) => x.parts.length > 0), msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
), ),
], ],
temperature: model.info.id === "codex-mini-latest" ? undefined : 0, temperature: model.info.temperature ? 0 : undefined,
tools: { tools: {
...tools, ...tools,
}, },

View file

@ -35,7 +35,7 @@ export const BashTool = Tool.define({
.min(0) .min(0)
.max(MAX_TIMEOUT) .max(MAX_TIMEOUT)
.describe("Optional timeout in milliseconds") .describe("Optional timeout in milliseconds")
.nullable(), .optional(),
description: z description: z
.string() .string()
.describe( .describe(

View file

@ -21,7 +21,7 @@ export const EditTool = Tool.define({
), ),
replaceAll: z replaceAll: z
.boolean() .boolean()
.nullable() .optional()
.describe("Replace all occurences of old_string (default false)"), .describe("Replace all occurences of old_string (default false)"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {

View file

@ -11,7 +11,7 @@ export const GlobTool = Tool.define({
pattern: z.string().describe("The glob pattern to match files against"), pattern: z.string().describe("The glob pattern to match files against"),
path: z path: z
.string() .string()
.nullable() .optional()
.describe( .describe(
`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`, `The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`,
), ),

View file

@ -14,13 +14,13 @@ export const GrepTool = Tool.define({
.describe("The regex pattern to search for in file contents"), .describe("The regex pattern to search for in file contents"),
path: z path: z
.string() .string()
.nullable() .optional()
.describe( .describe(
"The directory to search in. Defaults to the current working directory.", "The directory to search in. Defaults to the current working directory.",
), ),
include: z include: z
.string() .string()
.nullable() .optional()
.describe( .describe(
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")', 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
), ),

View file

@ -29,11 +29,11 @@ export const ListTool = Tool.define({
.describe( .describe(
"The absolute path to the directory to list (must be absolute, not relative)", "The absolute path to the directory to list (must be absolute, not relative)",
) )
.nullable(), .optional(),
ignore: z ignore: z
.array(z.string()) .array(z.string())
.describe("List of glob patterns to ignore") .describe("List of glob patterns to ignore")
.nullable(), .optional(),
}), }),
async execute(params) { async execute(params) {
const app = App.info() const app = App.info()

View file

@ -19,11 +19,11 @@ export const ReadTool = Tool.define({
offset: z offset: z
.number() .number()
.describe("The line number to start reading from (0-based)") .describe("The line number to start reading from (0-based)")
.nullable(), .optional(),
limit: z limit: z
.number() .number()
.describe("The number of lines to read (defaults to 2000)") .describe("The number of lines to read (defaults to 2000)")
.nullable(), .optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
let filePath = params.filePath let filePath = params.filePath

View file

@ -22,7 +22,7 @@ export const WebFetchTool = Tool.define({
.min(0) .min(0)
.max(MAX_TIMEOUT / 1000) .max(MAX_TIMEOUT / 1000)
.describe("Optional timeout in seconds (max 120)") .describe("Optional timeout in seconds (max 120)")
.nullable(), .optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
// Validate URL // Validate URL

View file

@ -9,7 +9,7 @@ describe("tool.glob", () => {
let result = await GlobTool.execute( let result = await GlobTool.execute(
{ {
pattern: "./node_modules/**/*", pattern: "./node_modules/**/*",
path: null, path: undefined,
}, },
{ {
sessionID: "test", sessionID: "test",
@ -25,7 +25,7 @@ describe("tool.glob", () => {
let result = await GlobTool.execute( let result = await GlobTool.execute(
{ {
pattern: "*.json", pattern: "*.json",
path: null, path: undefined,
}, },
{ {
sessionID: "test", sessionID: "test",

13
patches/ai@4.3.16.patch Normal file
View file

@ -0,0 +1,13 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 92a80377692488c4ba8801ce33e7736ad7055e43..add6281bbecaa1c03d3b48eb99aead4a7a7336b2 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1593,7 +1593,7 @@ function prepareCallSettings({
return {
maxTokens,
// TODO v5 remove default 0 for temperature
- temperature: temperature != null ? temperature : 0,
+ temperature: temperature,
topP,
topK,
presencePenalty,