diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 0f401c7562fa..c0756ea891b1 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -99,6 +99,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { tools[item.id] = tool({ description: item.description, inputSchema: jsonSchema(schema), + ...(item.strict !== undefined && { strict: item.strict }), execute(args, options) { return run.promise( Effect.gen(function* () { diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 15acc757f3d4..1aa1d2b25cf5 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -134,6 +134,7 @@ const layer = Layer.effect( id, parameters, jsonSchema, + strict: def.strict, description: def.description, execute: (args, toolCtx) => Effect.gen(function* () { @@ -309,6 +310,7 @@ const layer = Layer.effect( description: tool.description, parameters: tool.parameters, jsonSchema: tool.jsonSchema, + strict: tool.strict, } yield* plugin.trigger("tool.definition", { toolID: tool.id }, output) const jsonSchema = @@ -326,6 +328,7 @@ const layer = Layer.effect( .join("\n"), parameters: output.parameters, jsonSchema, + strict: output.strict, execute: tool.execute, formatValidationError: tool.formatValidationError, } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index e5e7802858ca..fee05e00a65c 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -60,6 +60,7 @@ export interface Def< description: string parameters: Parameters jsonSchema?: JSONSchema7 + strict?: boolean execute(args: Schema.Schema.Type, ctx: Context): Effect.Effect> formatValidationError?(error: unknown): string } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 0b88fa59dff8..5e45a528a750 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -586,6 +586,67 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { expect(result.tools.lookup.strict).toBe(false) }) + test("a tool's strict flag survives on bedrock and is still overridden on mantle", async () => { + const prepare = (npm: string, apiId: string, url: string) => + Effect.runPromise( + LLMRequestPrep.prepare({ + user: { + id: "msg_user-test", + sessionID, + role: "user", + time: { created: Date.now() }, + agent: "test", + model: { providerID: "amazon-bedrock", modelID: apiId }, + } as any, + sessionID, + model: { + ...createGpt5Model(apiId), + id: `amazon-bedrock/${apiId}`, + providerID: "amazon-bedrock", + api: { id: apiId, url, npm }, + }, + agent: { + name: "test", + mode: "primary", + options: {}, + permission: [], + } as any, + system: [], + messages: [{ role: "user", content: "Hello" }], + tools: { + lookup: { + description: "Look up a value", + inputSchema: jsonSchema({ type: "object", properties: {} }), + strict: true, + }, + }, + provider: { id: "amazon-bedrock", options: {} } as any, + auth: undefined, + plugin: { + trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output), + list: () => Effect.succeed([]), + init: () => Effect.void, + } as any, + flags: { outputTokenMax: 32_000, client: "test" } as any, + isWorkflow: false, + }), + ) + + const converse = await prepare( + "@ai-sdk/amazon-bedrock", + "anthropic.claude-sonnet-4-6", + "https://bedrock-runtime.us-east-1.amazonaws.com", + ) + const mantle = await prepare( + "@ai-sdk/amazon-bedrock/mantle", + "openai.gpt-5.5", + "https://bedrock-mantle.us-east-2.api.aws/openai/v1", + ) + + expect(converse.tools.lookup.strict).toBe(true) + expect(mantle.tools.lookup.strict).toBe(false) + }) + test("gpt-5.1 should have textVerbosity set to low", () => { const model = createGpt5Model("gpt-5.1") const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index c8c5fac59559..6bf2b983880a 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -194,6 +194,39 @@ describe("tool.registry", () => { }), ) + it.instance("carries strict from a custom tool definition", () => + Effect.gen(function* () { + const test = yield* TestInstance + const tool = path.join(test.directory, ".opencode", "tool") + yield* Effect.promise(() => fs.mkdir(tool, { recursive: true })) + yield* Effect.promise(() => + Bun.write( + path.join(tool, "strict_tool.ts"), + [ + "export default {", + " description: 'strict tool',", + " args: {},", + " strict: true,", + " execute: async () => 'ok',", + "}", + "", + ].join("\n"), + ), + ) + + const registry = yield* ToolRegistry.Service + const agents = yield* Agent.Service + const tools = yield* registry.tools({ + providerID: ProviderV2.ID.opencode, + modelID: ModelV2.ID.make("test"), + agent: yield* agents.defaultInfo(), + }) + + expect(tools.find((item) => item.id === "strict_tool")?.strict).toBe(true) + expect(tools.filter((item) => item.id !== "strict_tool").every((item) => item.strict === undefined)).toBe(true) + }), + ) + it.instance("ignores non-tool exports in .opencode/tool files", () => Effect.gen(function* () { const test = yield* TestInstance diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index edfa0139dfca..efdcdfd42da6 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -331,5 +331,8 @@ export interface Hooks { /** * Modify tool definitions (description and parameters) sent to LLM */ - "tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: any }) => Promise + "tool.definition"?: ( + input: { toolID: string }, + output: { description: string; parameters: any; strict?: boolean }, + ) => Promise } diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 9c6daa34d04a..ca3349fab5ee 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -45,6 +45,7 @@ export type ToolResult = export function tool(input: { description: string args: Args + strict?: boolean execute(args: z.infer>, context: ToolContext): Promise }) { return input