diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index 7479371d81f..47980905be6 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -2932,6 +2932,53 @@ describe("openai transport stream", () => { expect(params.tool_choice).toBe("required"); }); + it("sorts Responses tools by name for stable prompt-cache payloads", () => { + const model = { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "openai", + baseUrl: "https://api.openai.com/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"openai-responses">; + const zetaTool = { + name: "zeta", + description: "Z", + parameters: { type: "object", properties: {}, additionalProperties: false }, + }; + const alphaTool = { + name: "alpha", + description: "A", + parameters: { type: "object", properties: {}, additionalProperties: false }, + }; + + const first = buildOpenAIResponsesParams( + model, + { + systemPrompt: "system", + messages: [], + tools: [zetaTool, alphaTool], + } as never, + { sessionId: "session-123" } as never, + ) as { tools?: Array<{ name?: string }> }; + const second = buildOpenAIResponsesParams( + model, + { + systemPrompt: "system", + messages: [], + tools: [alphaTool, zetaTool], + } as never, + { sessionId: "session-123" } as never, + ) as { tools?: Array<{ name?: string }> }; + + expect(first.tools?.map((tool) => tool.name)).toEqual(["alpha", "zeta"]); + expect(first.tools).toEqual(second.tools); + }); + it("falls back to strict:false when a native OpenAI tool schema is not strict-compatible", () => { const params = buildOpenAIResponsesParams( { @@ -3809,6 +3856,54 @@ describe("openai transport stream", () => { expect(notOptedIn.prompt_cache_key).toBeUndefined(); }); + it("sorts Chat Completions tools by function name for stable prompt-cache payloads", () => { + const model = { + id: "custom-model", + name: "Custom Model", + api: "openai-completions", + provider: "custom-cpa", + baseUrl: "https://proxy.example.com/v1", + compat: { supportsPromptCacheKey: true }, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 8192, + } as unknown as Model<"openai-completions">; + const zetaTool = { + name: "zeta", + description: "Z", + parameters: { type: "object", properties: {} }, + }; + const alphaTool = { + name: "alpha", + description: "A", + parameters: { type: "object", properties: {} }, + }; + + const first = buildOpenAICompletionsParams( + model, + { + systemPrompt: "system", + messages: [], + tools: [zetaTool, alphaTool], + } as never, + { sessionId: "session-123" }, + ) as { tools?: Array<{ function?: { name?: string } }> }; + const second = buildOpenAICompletionsParams( + model, + { + systemPrompt: "system", + messages: [], + tools: [alphaTool, zetaTool], + } as never, + { sessionId: "session-123" }, + ) as { tools?: Array<{ function?: { name?: string } }> }; + + expect(first.tools?.map((tool) => tool.function?.name)).toEqual(["alpha", "zeta"]); + expect(first.tools).toEqual(second.tools); + }); + it("disables developer-role-only compat defaults for configured custom proxy completions providers", () => { const params = buildOpenAICompletionsParams( { diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 1bcb0e1cb53..7fa2baff74f 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -950,7 +950,7 @@ function convertResponsesTools( transport: "responses", model, }); - return tools.map((tool): FunctionTool => { + return sortTransportToolsByName(tools).map((tool): FunctionTool => { const base = { type: "function" as const, name: tool.name, @@ -2686,7 +2686,7 @@ function convertTools( model, }, ); - return tools.map((tool) => ({ + return sortTransportToolsByName(tools).map((tool) => ({ type: "function", function: { name: tool.name, @@ -2701,6 +2701,28 @@ function convertTools( })); } +function compareTransportToolText(left: string | undefined, right: string | undefined): number { + const leftText = left ?? ""; + const rightText = right ?? ""; + if (leftText < rightText) { + return -1; + } + if (leftText > rightText) { + return 1; + } + return 0; +} + +function sortTransportToolsByName( + tools: readonly T[], +): T[] { + return tools.toSorted( + (left, right) => + compareTransportToolText(left.name, right.name) || + compareTransportToolText(left.description, right.description), + ); +} + function extractGoogleThoughtSignature(toolCall: unknown): string | undefined { const tc = toolCall as Record | undefined; if (!tc) {