diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 0b898f1b66..b243971cc3 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { providerDefinitionList, type ProviderDefinition } from "./provider-settings/index.js" import { API_PROVIDER_FIELD, SETTINGS_SHAPE_FIELD } from "./provider-settings/common.js" export { + DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS, OPEN_AI_CODEX_SERVICE_TIER_KEY, parseOpenAiExtraBody, kimiCodeAuthMethodSchema, diff --git a/packages/types/src/provider-settings/index.ts b/packages/types/src/provider-settings/index.ts index 3fe8790af1..5cdadb4b2c 100644 --- a/packages/types/src/provider-settings/index.ts +++ b/packages/types/src/provider-settings/index.ts @@ -37,7 +37,7 @@ import { basetenProviderDefinition } from "./baseten.js" import type { ProviderDefinition } from "./common.js" export { OPEN_AI_CODEX_SERVICE_TIER_KEY } from "./openai-codex.js" -export { parseOpenAiExtraBody } from "./openai.js" +export { parseOpenAiExtraBody, DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS } from "./openai.js" export { kimiCodeAuthMethodSchema, type KimiCodeAuthMethod } from "./kimi-code.js" export { zaiApiLineSchema, type ZaiApiLine } from "./zai.js" export { diff --git a/packages/types/src/provider-settings/openai.ts b/packages/types/src/provider-settings/openai.ts index a14c3a2b5f..19ef88cee4 100644 --- a/packages/types/src/provider-settings/openai.ts +++ b/packages/types/src/provider-settings/openai.ts @@ -6,6 +6,14 @@ import { baseProviderSettingsShape, createModelIdAccessor, createProviderDefinit export const OPEN_AI_MODEL_ID_FIELD = "openAiModelId" +/** + * Default for the OpenAI `openAiStrictToolSchemas` setting. + * When true (default), non-MCP tools are sent with strict: true and their + * schemas are normalized for OpenAI strict mode. Some OpenAI-compatible + * proxies reject strict: true; set this to false for those endpoints. + */ +export const DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS = true + const OPENAI_EXTRA_BODY_RESERVED_KEYS = [ // Prototype-pollution defenses; remaining keys are request-owned, including tool-call protocol controls. "__proto__", @@ -83,6 +91,7 @@ export const openAiProviderDefinition = createProviderDefinition({ openAiBaseUrl: z.string().optional(), openAiApiKey: z.string().optional(), openAiR1FormatEnabled: z.boolean().optional(), + openAiStrictToolSchemas: z.boolean().optional(), [OPEN_AI_MODEL_ID_FIELD]: z.string().optional(), openAiCustomModelInfo: modelInfoSchema.nullish(), openAiUseAzure: z.boolean().optional(), diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index 599bfafbe4..998b08e3f4 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -29,8 +29,8 @@ class TestProvider extends BaseProvider { } // Expose protected method for testing - public testConvertToolsForOpenAI(tools: any[] | undefined): any[] | undefined { - return this.convertToolsForOpenAI(tools) + public testConvertToolsForOpenAI(tools: any[] | undefined, strict?: boolean): any[] | undefined { + return this.convertToolsForOpenAI(tools, strict) } } @@ -179,6 +179,96 @@ describe("BaseProvider", () => { }) }) + it("leaves property entries that are not objects unchanged", () => { + const schema = { + type: "object", + properties: { + broken: null, + list: ["string", "null"], + note: "plain", + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + // The clone guard only normalises object entries, so a null entry, an + // array-valued entry and a plain string entry are passed through as declared. + expect(result.properties.broken).toBe(null) + expect(result.properties.list).toEqual(["string", "null"]) + expect(result.properties.note).toBe("plain") + }) + + it("keeps a nullable type with more than one non-null type as an array", () => { + const schema = { + type: "object", + properties: { + value: { type: ["string", "null", "number"] }, + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + expect(result.properties.value.type).toEqual(["string", "number"]) + }) + + it("does not treat a plain property as an array of objects", () => { + const schema = { + type: "object", + properties: { + path: { type: "string" }, + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + // Only array properties get an items conversion, so a plain property + // keeps exactly the keys it declared. + expect(Object.keys(result.properties.path)).toEqual(["type"]) + }) + + it("does not convert items on a non-array property that carries object items", () => { + const schema = { + type: "object", + properties: { + weird: { + type: "string", + items: { type: "object", properties: { a: { type: "string" } } }, + }, + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + expect(result.properties.weird.items.additionalProperties).toBeUndefined() + }) + + it("leaves an array property without items unchanged", () => { + const schema = { + type: "object", + properties: { + tags: { type: "array" }, + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + expect(Object.keys(result.properties.tags)).toEqual(["type"]) + }) + + it("leaves a property whose type is only null unchanged", () => { + const schema = { + type: "object", + properties: { + only: { type: "null" }, + }, + } + + const result = provider.testConvertToolSchemaForOpenAI(schema) + + // The nullable branch only handles array types; a bare null type is not a + // list to filter. + expect(result.properties.only.type).toBe("null") + }) describe("convertToolsForOpenAI", () => { it("should return undefined for undefined input", () => { const result = provider.testConvertToolsForOpenAI(undefined) @@ -346,6 +436,84 @@ describe("BaseProvider", () => { expect(result?.[0].function.name).toBe("read\uFFFDfile") }) + it("should set strict: false and preserve declared schema when strict is disabled", () => { + const tools = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { + path: { type: "string" }, + offset: { type: "integer" }, + }, + required: ["path"], + }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, false) + + expect(result?.[0].function.strict).toBe(false) + // Declared schema preserved: original required array, no additionalProperties coercion + expect(result?.[0].function.parameters).toEqual(tools[0].function.parameters) + }) + + it("should not mutate caller-owned schemas during strict normalization", () => { + const tools = [ + { + type: "function", + function: { + name: "nullable_tool", + description: "Tool with a nullable property", + parameters: { + type: "object", + properties: { + path: { type: ["string", "null"] }, + }, + required: ["path"], + }, + }, + }, + ] + + const strictResult = provider.testConvertToolsForOpenAI(tools, true) + expect(strictResult?.[0].function.parameters.properties.path.type).toBe("string") + + // Caller-owned schema is untouched, so a later non-strict request + // can still send the original nullable type. + expect(tools[0].function.parameters.properties.path.type).toEqual(["string", "null"]) + + const nonStrictResult = provider.testConvertToolsForOpenAI(tools, false) + expect(nonStrictResult?.[0].function.parameters).toEqual(tools[0].function.parameters) + }) + + it("should still set strict: false for MCP tools when strict is disabled", () => { + const tools = [ + { + type: "function", + function: { + name: "mcp--github--get_me", + description: "Get current user", + parameters: { + type: "object", + properties: { + token: { type: "string" }, + }, + required: ["token"], + }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, false) + + expect(result?.[0].function.strict).toBe(false) + expect(result?.[0].function.parameters).toEqual(tools[0].function.parameters) + }) it("should preserve non-function tools unchanged", () => { const tools = [ { diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 4dc6521153..957e45b99f 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -1835,6 +1835,174 @@ describe("OpenAiHandler", () => { }) }) +describe("OpenAiHandler - strict tool schemas", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }] + const tools: OpenAI.Chat.ChatCompletionFunctionTool[] = [ + { + type: "function", + function: { + name: "test_tool", + description: "A test tool", + parameters: { + type: "object", + properties: { + path: { type: "string" }, + offset: { type: "integer" }, + }, + required: ["path"], + }, + }, + }, + ] + const baseOptions = () => + makeApiHandlerOptions({ + openAiApiKey: "test-api-key", + openAiModelId: "gpt-4", + openAiBaseUrl: "https://api.openai.com/v1", + }) + + beforeEach(() => { + mockCreate.mockClear() + }) + + it("sends strict: true with normalized schemas by default", async () => { + const handler = new OpenAiHandler(baseOptions()) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledTimes(1) + const request = mockCreate.mock.calls[0][0] + expect(request.tools).toHaveLength(1) + expect(request.tools[0].function.strict).toBe(true) + expect(request.tools[0].function.parameters.required).toEqual(["path", "offset"]) + expect(request.tools[0].function.parameters.additionalProperties).toBe(false) + }) + + it("sends strict: false with the declared schema when openAiStrictToolSchemas is false", async () => { + const handler = new OpenAiHandler({ ...baseOptions(), openAiStrictToolSchemas: false }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.tools[0].function.strict).toBe(false) + // Declared schema preserved: original required array, no additionalProperties coercion + expect(request.tools[0].function.parameters).toEqual(tools[0].function.parameters) + }) + + it("propagates a gateway 400 that rejects strict tool schemas", async () => { + const strictRejection = Object.assign( + new Error("strict=true requires generated function arguments to satisfy the declared JSON Schema"), + { status: 400 }, + ) + mockCreate.mockRejectedValueOnce(strictRejection) + + const handler = new OpenAiHandler({ ...baseOptions(), openAiStrictToolSchemas: true }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + + let caught: unknown + try { + await collectStream(stream) + } catch (error) { + caught = error + } + + expect(caught).toBeInstanceOf(Error) + expect((caught as Error).message).toContain("OpenAI completion error") + expect((caught as Error & { status?: number }).status).toBe(400) + }) + + it("omits tools when the request carries none", async () => { + const handler = new OpenAiHandler(baseOptions()) + const stream = handler.createMessage(systemPrompt, messages) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.tools).toBeUndefined() + }) + + it("sends strict: false for non-streaming requests when openAiStrictToolSchemas is false", async () => { + const handler = new OpenAiHandler({ + ...baseOptions(), + openAiStreamingEnabled: false, + openAiStrictToolSchemas: false, + }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.tools[0].function.strict).toBe(false) + expect(request.tools[0].function.parameters).toEqual(tools[0].function.parameters) + }) + + it("sends strict: true with normalized schemas for O3-family streaming requests by default", async () => { + const handler = new OpenAiHandler({ + ...baseOptions(), + openAiModelId: "o3-mini", + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65_536, + supportsPromptCache: false, + reasoningEffort: "medium" as "low" | "medium" | "high", + }, + }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.model).toBe("o3-mini") + expect(request.stream).toBe(true) + expect(request.tools[0].function.strict).toBe(true) + expect(request.tools[0].function.parameters.required).toEqual(["path", "offset"]) + }) + + it("sends strict: false with the declared schema for O3-family streaming requests when disabled", async () => { + // The streaming O3 branch builds its own request, so the strict argument + // must be asserted on that path as well: dropping it at the streaming call + // would otherwise keep passing the non-streaming test. + const handler = new OpenAiHandler({ + ...baseOptions(), + openAiModelId: "o3-mini", + openAiStrictToolSchemas: false, + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65_536, + supportsPromptCache: false, + reasoningEffort: "medium" as "low" | "medium" | "high", + }, + }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.model).toBe("o3-mini") + expect(request.stream).toBe(true) + expect(request.tools[0].function.strict).toBe(false) + expect(request.tools[0].function.parameters).toEqual(tools[0].function.parameters) + }) + + it("sends strict: false with the declared schema for O3-family non-streaming requests when disabled", async () => { + const handler = new OpenAiHandler({ + ...baseOptions(), + openAiModelId: "o3-mini", + openAiStreamingEnabled: false, + openAiStrictToolSchemas: false, + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65_536, + supportsPromptCache: false, + reasoningEffort: "medium" as "low" | "medium" | "high", + }, + }) + const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", tools }) + await collectStream(stream) + + const request = mockCreate.mock.calls[0][0] + expect(request.tools[0].function.strict).toBe(false) + expect(request.tools[0].function.parameters).toEqual(tools[0].function.parameters) + }) +}) + describe("getOpenAiModels", () => { beforeEach(() => { vi.mocked(axios.get).mockClear() diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 53a2ac9d3f..abb35ab6bf 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -1,6 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" -import type { ModelInfo } from "@roo-code/types" +import { DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS, type ModelInfo } from "@roo-code/types" import type { ApiHandler, ApiHandlerCreateMessageMetadata } from "../index" import { ApiStream } from "../transform/stream" @@ -26,9 +26,16 @@ export abstract class BaseProvider implements ApiHandler { /** * Converts an array of tools to be compatible with OpenAI's strict mode. * Filters for function tools, applies schema conversion to their parameters, - * and ensures all tools have consistent strict: true values. + * and ensures all tools have consistent strict values. + * + * Pass strict=false to serve endpoints that reject strict: true (e.g. + * strict-unaware OpenAI-compatible proxies): non-MCP tools are then sent + * with strict: false and their declared schemas are preserved as-is. */ - protected convertToolsForOpenAI(tools: any[] | undefined): any[] | undefined { + protected convertToolsForOpenAI( + tools: any[] | undefined, + strict: boolean = DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS, + ): any[] | undefined { if (!tools) { return undefined } @@ -42,6 +49,11 @@ export abstract class BaseProvider implements ApiHandler { // to preserve optional parameters from the MCP server schema const isMcp = isMcpTool(tool.function.name) + // Strict mode also rewrites the schema (all properties become + // required). When disabled, the declared schema is preserved as-is, + // retaining every original required constraint (e.g. nanogpt). + const useStrict = !isMcp && strict + return { ...tool, function: { @@ -50,9 +62,11 @@ export abstract class BaseProvider implements ApiHandler { // (e.g. DeepSeek) reject the whole request otherwise. See #461. name: sanitizeSurrogates(tool.function.name), description: sanitizeSurrogates(tool.function.description), - strict: !isMcp, + strict: useStrict, parameters: sanitizeSurrogatesDeep( - isMcp ? tool.function.parameters : this.convertToolSchemaForOpenAI(tool.function.parameters), + useStrict + ? this.convertToolSchemaForOpenAI(tool.function.parameters) + : tool.function.parameters, ), }, } @@ -91,19 +105,28 @@ export abstract class BaseProvider implements ApiHandler { for (const key of allKeys) { const prop = newProps[key] - // Handle nullable types by removing null - if (prop && Array.isArray(prop.type) && prop.type.includes("null")) { - const nonNullTypes = prop.type.filter((t: string) => t !== "null") - prop.type = nonNullTypes.length === 1 ? nonNullTypes[0] : nonNullTypes - } + // Clone each property before normalizing so strict conversion never + // mutates caller-owned tool metadata: a later request with strict + // disabled must still send the declared (nullable) schema. + if (prop && typeof prop === "object" && !Array.isArray(prop)) { + const normalizedProp = { ...prop } + + // Handle nullable types by removing null + if (Array.isArray(normalizedProp.type) && normalizedProp.type.includes("null")) { + const nonNullTypes = normalizedProp.type.filter((t: string) => t !== "null") + normalizedProp.type = nonNullTypes.length === 1 ? nonNullTypes[0] : nonNullTypes + } - // Recursively process nested objects - if (prop && prop.type === "object") { - newProps[key] = this.convertToolSchemaForOpenAI(prop) - } else if (prop && prop.type === "array" && prop.items?.type === "object") { - newProps[key] = { - ...prop, - items: this.convertToolSchemaForOpenAI(prop.items), + // Recursively process nested objects + if (normalizedProp.type === "object") { + newProps[key] = this.convertToolSchemaForOpenAI(normalizedProp) + } else if (normalizedProp.type === "array" && normalizedProp.items?.type === "object") { + newProps[key] = { + ...normalizedProp, + items: this.convertToolSchemaForOpenAI(normalizedProp.items), + } + } else { + newProps[key] = normalizedProp } } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 4e704c3078..8ccf3335f0 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -9,6 +9,7 @@ import { isAzureOpenAiBaseUrl, openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE, + DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS, OPENAI_AZURE_AI_INFERENCE_PATH, parseOpenAiExtraBody, } from "@roo-code/types" @@ -92,6 +93,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const modelUrl = this.options.openAiBaseUrl ?? "" const modelId = this.options.openAiModelId ?? "" const enabledR1Format = this.options.openAiR1FormatEnabled ?? false + // Stryker disable next-line LogicalOperator: equivalent. An unset option makes `&&` + // yield undefined, which the parameter default in convertToolsForOpenAI turns back into + // the same strict value as `??`. + const strictToolSchemas = this.options.openAiStrictToolSchemas ?? DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS const isAzureAiInference = this._isAzureAiInference(modelUrl) const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format @@ -173,7 +178,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true as const, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), ...(reasoning && reasoning), - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, strictToolSchemas), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } @@ -241,7 +246,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl : [systemMessage, ...convertToOpenAiMessages(messages)], ...reasoning, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, strictToolSchemas), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } @@ -359,6 +364,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ): ApiStream { const { info: modelInfo, reasoning } = this.getModel() const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) + // Stryker disable next-line LogicalOperator: equivalent. An unset option makes `&&` + // yield undefined, which the parameter default in convertToolsForOpenAI turns back into + // the same strict value as `??`. + const strictToolSchemas = this.options.openAiStrictToolSchemas ?? DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS if (this.options.openAiStreamingEnabled ?? true) { const isGrokXAI = this._isGrokXAI(this.options.openAiBaseUrl) @@ -377,7 +386,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...reasoning, temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, strictToolSchemas), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } @@ -412,7 +421,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...reasoning, temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, strictToolSchemas), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 528cbda497..9e722702fd 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -4,12 +4,35 @@ import { renderWithExtensionState, screen, fireEvent, within, waitFor } from "@/ import { act } from "@testing-library/react" import { vscode } from "@/utils/vscode" -import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS } from "@roo-code/types" +import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, type ProviderSettings } from "@roo-code/types" +import type { ApiOptionsProps } from "../ApiOptions" import SettingsView from "../SettingsView" vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } })) +// SettingsView hands the buffered api configuration to the provider section, so the +// save round trip can be observed without rendering the whole provider tree. +vi.mock("../ApiOptions", () => ({ + __esModule: true, + default: ({ apiConfiguration, setApiConfigurationField }: Pick) => ( +
+ {String(apiConfiguration?.openAiStrictToolSchemas)} +
+ ), +})) vi.mock("../ApiConfigManager", () => ({ __esModule: true, default: ({ currentApiConfigName }: any) => ( @@ -912,3 +935,73 @@ describe("SettingsView - Blanket Auto-Deny", () => { ) }) }) + +describe("SettingsView - openAiStrictToolSchemas save round trip", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + const renderWithConfig = (apiConfiguration: ProviderSettings) => { + const onDone = vi.fn() + renderWithExtensionState(, { + state: { currentApiConfigName: "test-config", apiConfiguration }, + }) + } + + const posted = () => + vi + .mocked(vscode.postMessage) + .mock.calls.map((call) => call[0]) + .find((message) => message?.type === "upsertApiConfiguration") + + it("restores the control from a loaded API configuration", () => { + // The control must show the stored value rather than the display default, so + // a saved false survives a reload. + renderWithConfig({ openAiStrictToolSchemas: false }) + expect(screen.getByTestId("received-strict").textContent).toBe("false") + }) + + it("buffers the change and persists it only on Save", () => { + renderWithConfig({ openAiStrictToolSchemas: true }) + fireEvent.click(screen.getByTestId("set-strict-false")) + // Toggling buffers in cachedState; nothing is persisted before Save. + expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) + fireEvent.click(screen.getByTestId("save-button")) + expect(posted()).toEqual( + expect.objectContaining({ + type: "upsertApiConfiguration", + text: "test-config", + apiConfiguration: expect.objectContaining({ openAiStrictToolSchemas: false }), + }), + ) + }) + + it("persists a toggled-on value as true in the upsert payload", () => { + // The save path must carry the value the user set, not only the loaded one: + // toggling on from a stored false and saving has to put true in the payload. + renderWithConfig({ openAiStrictToolSchemas: false }) + fireEvent.click(screen.getByTestId("set-strict-true")) + fireEvent.click(screen.getByTestId("save-button")) + expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBe(true) + }) + + it("saves a loaded false back as false when toggled on and off again", () => { + renderWithConfig({ openAiStrictToolSchemas: false }) + fireEvent.click(screen.getByTestId("set-strict-true")) + fireEvent.click(screen.getByTestId("set-strict-false")) + fireEvent.click(screen.getByTestId("save-button")) + expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBe(false) + }) + + it("leaves an unset value unset instead of writing the display default", () => { + // The control shows the display default, but saving must not materialise it: + // a config that never stored the setting stays unset. + renderWithConfig({}) + expect(screen.getByTestId("received-strict").textContent).toBe("undefined") + // Make a change elsewhere so Save is enabled, then save. + fireEvent.click(screen.getByTestId("set-other-field")) + fireEvent.click(screen.getByTestId("save-button")) + expect(posted()?.apiConfiguration?.openAiBaseUrl).toBe("https://example.test") + expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBeUndefined() + }) +}) diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 2a77a37325..efb839a7fc 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -12,6 +12,7 @@ import { azureOpenAiDefaultApiVersion, isAzureOpenAiBaseUrl, openAiModelInfoSaneDefaults, + DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS, OpenAiModelsMessageType, parseOpenAiExtraBody, } from "@roo-code/types" @@ -189,6 +190,16 @@ export const OpenAICompatible = ({ {t("settings:includeMaxOutputTokensDescription")} +
+ + {t("settings:providers.openAiStrictToolSchemas")} + +
+ {t("settings:providers.openAiStrictToolSchemasDescription")} +
+
diff --git a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx index fc20d917e9..ce7337b67a 100644 --- a/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx @@ -501,3 +501,56 @@ describe("OpenAICompatible Component - includeMaxTokens checkbox", () => { }) }) }) + +describe("OpenAICompatible Component - openAiStrictToolSchemas checkbox", () => { + const mockSetApiConfigurationField = vi.fn() + const mockOrganizationAllowList = { + allowAll: true, + providers: {}, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should default to checked when openAiStrictToolSchemas is undefined", () => { + render( + , + ) + + const checkboxInput = screen.getByTestId("checkbox-input-settings:providers.openaistricttoolschemas") + expect(checkboxInput).toBeChecked() + }) + + it("should show checkbox as unchecked when openAiStrictToolSchemas is false", () => { + render( + , + ) + + const checkboxInput = screen.getByTestId("checkbox-input-settings:providers.openaistricttoolschemas") + expect(checkboxInput).not.toBeChecked() + }) + + it("should call setApiConfigurationField when toggled", () => { + render( + , + ) + + const checkboxInput = screen.getByTestId("checkbox-input-settings:providers.openaistricttoolschemas") + fireEvent.click(checkboxInput) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("openAiStrictToolSchemas", false) + }) +}) diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-azure-guidance-dark.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-azure-guidance-dark.png index 75caad8a48..e8d6bb3b4b 100644 Binary files a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-azure-guidance-dark.png and b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-azure-guidance-dark.png differ diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-dark.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-dark.png index 2e82ab0811..92ac6867b5 100644 Binary files a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-dark.png and b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-dark.png differ diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast-light.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast-light.png index 0469607672..93c080dd07 100644 Binary files a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast-light.png and b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast-light.png differ diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast.png index 7c51ab1f57..171f554dd1 100644 Binary files a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast.png and b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-high-contrast.png differ diff --git a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-light.png b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-light.png index e48013ba4c..0493d1fcd6 100644 Binary files a/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-light.png and b/webview-ui/src/components/settings/providers/__tests__/__screenshots__/openai-compatible-extra-body-light.png differ diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 39b585a6b1..71cd18cecb 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "Clau API d'OpenAI", "apiKey": "Clau API", "openAiBaseUrl": "URL base", + "openAiStrictToolSchemas": "Esquemes d'eines estrictes (strict: true)", + "openAiStrictToolSchemasDescription": "Envia strict: true amb els esquemes d'eines per activar l'adherència estricta a l'esquema. Desactiva només per usar la crida d'eines no estricta.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nom de desplegament d'Azure", "azureOpenAiDeploymentNameDescription": "Introduïu el nom del desplegament d'Azure AI Studio, no el nom del model subjacent.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index cf6106a6dd..4403a19dbb 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI API-Schlüssel", "apiKey": "API-Schlüssel", "openAiBaseUrl": "Basis-URL", + "openAiStrictToolSchemas": "Strikte Tool-Schemas (strict: true)", + "openAiStrictToolSchemasDescription": "Sendet strict: true mit den Tool-Schemas, um die strikte Schema-Treue zu aktivieren. Deaktiviere, um nicht-striktes Tool-Calling zu nutzen.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure-Bereitstellungsname", "azureOpenAiDeploymentNameDescription": "Gib den Bereitstellungsnamen aus Azure AI Studio ein, nicht den Namen des zugrunde liegenden Modells.", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3ada77ccc3..2e4ff9298d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -614,6 +614,8 @@ "openAiApiKey": "OpenAI API Key", "apiKey": "API Key", "openAiBaseUrl": "Base URL", + "openAiStrictToolSchemas": "Strict tool schemas (strict: true)", + "openAiStrictToolSchemasDescription": "Send strict: true with tool schemas to enable strict schema adherence. Disable to use non-strict tool calling.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure deployment name", "azureOpenAiDeploymentNameDescription": "Enter the deployment name from Azure AI Studio, not the underlying model name.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index edd35629b9..22788f89f9 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "Clave API de OpenAI", "apiKey": "Clave API", "openAiBaseUrl": "URL base", + "openAiStrictToolSchemas": "Esquemas de herramientas estrictos (strict: true)", + "openAiStrictToolSchemasDescription": "Envía strict: true con los esquemas de herramientas para habilitar la adherencia estricta al esquema. Desactívalo para usar el llamado de herramientas no estricto.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nombre de implementación de Azure", "azureOpenAiDeploymentNameDescription": "Introduce el nombre de la implementación de Azure AI Studio, no el nombre del modelo subyacente.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index fbd2512e42..f57562ebf2 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "Clé API OpenAI", "apiKey": "Clé API", "openAiBaseUrl": "URL de base", + "openAiStrictToolSchemas": "Schémas d'outils stricts (strict: true)", + "openAiStrictToolSchemasDescription": "Envoie strict: true avec les schémas d'outils pour activer le respect strict du schéma. Désactivez pour utiliser l'appel d'outils non strict.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nom du déploiement Azure", "azureOpenAiDeploymentNameDescription": "Saisissez le nom du déploiement défini dans Azure AI Studio, et non le nom du modèle sous-jacent.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index c6dffef9a3..52a29ff287 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI API कुंजी", "apiKey": "API कुंजी", "openAiBaseUrl": "बेस URL", + "openAiStrictToolSchemas": "कड़े टूल स्कीमा (strict: true)", + "openAiStrictToolSchemasDescription": "strict schema adherence को सक्षम करने के लिए टूल स्कीमा के साथ strict: true भेजें। गैर-कड़े टूल कॉलिंग के लिए अक्षम करें।", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure परिनियोजन नाम", "azureOpenAiDeploymentNameDescription": "अंतर्निहित मॉडल नाम के बजाय Azure AI Studio में दिया गया परिनियोजन नाम दर्ज करें।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 1ca1c4035b..e3ae8c18fc 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI API Key", "apiKey": "API Key", "openAiBaseUrl": "Base URL", + "openAiStrictToolSchemas": "Skema tool ketat (strict: true)", + "openAiStrictToolSchemasDescription": "Kirim strict: true bersama skema tool untuk mengaktifkan kepatuhan skema ketat. Nonaktifkan untuk menggunakan pemanggilan tool non-ketat.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nama deployment Azure", "azureOpenAiDeploymentNameDescription": "Masukkan nama deployment dari Azure AI Studio, bukan nama model dasarnya.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 12e801aa49..c6ddf94a03 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "Chiave API OpenAI", "apiKey": "Chiave API", "openAiBaseUrl": "URL base", + "openAiStrictToolSchemas": "Schemi di strumenti strict (strict: true)", + "openAiStrictToolSchemasDescription": "Invia strict: true con gli schemi di strumenti per abilitare l'aderenza rigorosa allo schema. Disattiva per usare il tool calling non rigoroso.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nome della distribuzione Azure", "azureOpenAiDeploymentNameDescription": "Inserisci il nome della distribuzione configurato in Azure AI Studio, non il nome del modello sottostante.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 72f0b4e54f..e07b01c510 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI APIキー", "apiKey": "APIキー", "openAiBaseUrl": "ベースURL", + "openAiStrictToolSchemas": "厳格なツールスキーマ(strict: true)", + "openAiStrictToolSchemasDescription": "スキーマの厳密な準拠を有効にするため、ツールスキーマに strict: true を付与して送信します。非厳密なツール呼び出しを使う場合は無効にしてください。", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure デプロイ名", "azureOpenAiDeploymentNameDescription": "基盤モデル名ではなく、Azure AI Studio のデプロイ名を入力してください。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 8f7b1dbbcb..49efb31701 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -534,6 +534,8 @@ "apiKey": "API 키", "openAiApiKey": "OpenAI API 키", "openAiBaseUrl": "기본 URL", + "openAiStrictToolSchemas": "엄격한 도구 스키마 (strict: true)", + "openAiStrictToolSchemasDescription": "스키마를 엄격하게 준수하도록 하려면 도구 스키마에 strict: true를 포함해 보냅니다. 비엄격 도구 호출을 사용하려면 비활성화하세요.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure 배포 이름", "azureOpenAiDeploymentNameDescription": "기반 모델 이름이 아닌 Azure AI Studio의 배포 이름을 입력하세요.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 9751728c80..54e76a1a00 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -534,6 +534,8 @@ "apiKey": "API-sleutel", "openAiApiKey": "OpenAI API-sleutel", "openAiBaseUrl": "Basis-URL", + "openAiStrictToolSchemas": "Strikte toolschemas (strict: true)", + "openAiStrictToolSchemasDescription": "Verstuurt strict: true met de toolschemas om strikte schema-naleving in te schakelen. Schakel uit voor niet-strikte tool calls.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Naam van Azure-implementatie", "azureOpenAiDeploymentNameDescription": "Voer de implementatienaam uit Azure AI Studio in, niet de naam van het onderliggende model.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 5fd8bdfe32..411362c9b6 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -534,6 +534,8 @@ "apiKey": "Klucz API", "openAiApiKey": "Klucz API OpenAI", "openAiBaseUrl": "URL bazowy", + "openAiStrictToolSchemas": "Ścisłe schematy narzędzi (strict: true)", + "openAiStrictToolSchemasDescription": "Wysyła strict: true wraz ze schematami narzędzi, aby włączyć ścisłe przestrzeganie schematu. Wyłącz, aby korzystać z nieścisłego wywoływania narzędzi.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nazwa wdrożenia Azure", "azureOpenAiDeploymentNameDescription": "Wprowadź nazwę wdrożenia z Azure AI Studio, a nie nazwę bazowego modelu.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 2b23f147b0..a65d642a68 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -534,6 +534,8 @@ "apiKey": "Chave de API", "openAiApiKey": "Chave de API OpenAI", "openAiBaseUrl": "URL Base", + "openAiStrictToolSchemas": "Schemas de ferramentas estritos (strict: true)", + "openAiStrictToolSchemasDescription": "Envia strict: true com os schemas de ferramentas para ativar a aderência estrita ao schema. Desative para usar chamada de ferramentas não estrita.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Nome da implantação do Azure", "azureOpenAiDeploymentNameDescription": "Insira o nome da implantação configurado no Azure AI Studio, não o nome do modelo subjacente.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index c684696317..177aaa870e 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -534,6 +534,8 @@ "apiKey": "API-ключ", "openAiApiKey": "OpenAI API-ключ", "openAiBaseUrl": "Базовый URL", + "openAiStrictToolSchemas": "Строгие схемы инструментов (strict: true)", + "openAiStrictToolSchemasDescription": "Отправляет strict: true вместе со схемами инструментов, чтобы включить строгое соблюдение схемы. Отключите, чтобы использовать нестрогий вызов инструментов.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Имя развёртывания Azure", "azureOpenAiDeploymentNameDescription": "Введите имя развёртывания из Azure AI Studio, а не имя базовой модели.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index cd4556c7db..a3bbceb764 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI API Anahtarı", "apiKey": "API Anahtarı", "openAiBaseUrl": "Temel URL", + "openAiStrictToolSchemas": "Katı araç şemaları (strict: true)", + "openAiStrictToolSchemasDescription": "Şema uyumunu etkinleştirmek için araç şemalarıyla strict: true gönderir. Katı olmayan araç çağırmak için devre dışı bırakın.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure dağıtım adı", "azureOpenAiDeploymentNameDescription": "Temel model adı yerine Azure AI Studio'daki dağıtım adını girin.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index b81ad24a2f..4f25fcceb6 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "Khóa API OpenAI", "apiKey": "Khóa API", "openAiBaseUrl": "URL cơ sở", + "openAiStrictToolSchemas": "Sơ đồ công cụ nghiêm ngặt (strict: true)", + "openAiStrictToolSchemasDescription": "Gửi strict: true cùng sơ đồ công cụ để bật sự tuân thủ sơ đồ nghiêm ngặt. Tắt để sử dụng gọi công cụ không nghiêm ngặt.", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Tên triển khai Azure", "azureOpenAiDeploymentNameDescription": "Nhập tên triển khai trong Azure AI Studio, không phải tên mô hình cơ sở.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 53ec69eb80..fee64e4f54 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -534,6 +534,8 @@ "openAiApiKey": "OpenAI API 密钥", "apiKey": "API 密钥", "openAiBaseUrl": "OpenAI 基础 URL", + "openAiStrictToolSchemas": "严格工具 schema(strict: true)", + "openAiStrictToolSchemasDescription": "向工具 schema 一并发送 strict: true 以启用严格的 schema 遵循。关闭以使用非严格工具调用。", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure 部署名称", "azureOpenAiDeploymentNameDescription": "请输入 Azure AI Studio 中的部署名称,而不是底层模型名称。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 312b005843..eefd6012cf 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -561,6 +561,8 @@ "openAiApiKey": "OpenAI API 金鑰", "apiKey": "API 金鑰", "openAiBaseUrl": "基礎 URL", + "openAiStrictToolSchemas": "嚴格工具 schema(strict: true)", + "openAiStrictToolSchemasDescription": "向工具 schema 一併傳送 strict: true 以啟用嚴格的 schema 遵循。停用以使用非嚴格工具呼叫。", "azureOpenAiBaseUrlPlaceholder": "https://.openai.azure.com/openai", "azureOpenAiDeploymentName": "Azure 部署名稱", "azureOpenAiDeploymentNameDescription": "請輸入 Azure AI Studio 中的部署名稱,而不是底層模型名稱。",