From f3361dc92870fa951d1e4f205877e57af12f27be Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 14 May 2026 01:36:33 +0100 Subject: [PATCH] test(agents): surface live OpenAI replay auth failures --- src/agents/models.profiles.live.test.ts | 35 ++++++++++++++++++++++ src/agents/openai-transport-stream.test.ts | 31 +++++++++++++++++++ src/agents/openai-transport-stream.ts | 5 ++++ 3 files changed, 71 insertions(+) diff --git a/src/agents/models.profiles.live.test.ts b/src/agents/models.profiles.live.test.ts index 12768549902..3fc6554ffa5 100644 --- a/src/agents/models.profiles.live.test.ts +++ b/src/agents/models.profiles.live.test.ts @@ -545,6 +545,34 @@ async function completeSimpleWithTimeout( } } +function requireToolChoicePayload(payload: unknown): unknown | undefined { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + return undefined; + } + const candidate = payload as { tools?: unknown; tool_choice?: unknown }; + if (!Array.isArray(candidate.tools) || candidate.tools.length === 0) { + return undefined; + } + return { + ...candidate, + tool_choice: { type: "function", name: "noop" }, + }; +} + +describe("requireToolChoicePayload", () => { + it("requires tool use when a Responses payload has tools", () => { + expect(requireToolChoicePayload({ model: "gpt", tools: [{ name: "noop" }] })).toEqual({ + model: "gpt", + tools: [{ name: "noop" }], + tool_choice: { type: "function", name: "noop" }, + }); + }); + + it("leaves payloads without tools unchanged", () => { + expect(requireToolChoicePayload({ model: "gpt", tools: [] })).toBeUndefined(); + }); +}); + async function completeOkWithRetry(params: { model: Model; apiKey: string; @@ -982,6 +1010,7 @@ describeLive("live models (profile keys)", () => { apiKey, reasoning: resolveTestReasoning(model), maxTokens: 128, + onPayload: requireToolChoicePayload, }, perModelTimeoutMs, `${progressLabel}: tool-only regression first call`, @@ -1012,6 +1041,7 @@ describeLive("live models (profile keys)", () => { apiKey, reasoning: resolveTestReasoning(model), maxTokens: 128, + onPayload: requireToolChoicePayload, }, perModelTimeoutMs, `${progressLabel}: tool-only regression retry ${i + 1}`, @@ -1025,6 +1055,11 @@ describeLive("live models (profile keys)", () => { .trim(); } + if (first.stopReason === "error") { + throw new Error( + first.errorMessage || "tool-only regression returned error with no message", + ); + } expect(firstText.length).toBe(0); if (!toolCall || toolCall.type !== "toolCall") { throw new Error("expected tool call"); diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index b0d5485bd4d..676f6ceac67 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -2314,6 +2314,37 @@ describe("openai transport stream", () => { }); }); + it("passes explicit Responses tool_choice when tools are present", () => { + const params = buildOpenAIResponsesParams( + { + 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">, + { + systemPrompt: "system", + messages: [], + tools: [ + { + name: "lookup_weather", + description: "Get forecast", + parameters: { type: "object", properties: {}, additionalProperties: false }, + }, + ], + } as never, + { toolChoice: "required" } as never, + ) as { tool_choice?: string }; + + expect(params.tool_choice).toBe("required"); + }); + it("falls back to strict:false when a native OpenAI tool schema is not strict-compatible", () => { const params = buildOpenAIResponsesParams( { diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index d07bd457e58..e2f168f67ee 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -94,6 +94,7 @@ type OpenAIResponsesOptions = BaseStreamOptions & { reasoningEffort?: OpenAIReasoningEffort; reasoningSummary?: "auto" | "detailed" | "concise" | null; serviceTier?: ResponseCreateParamsStreaming["service_tier"]; + toolChoice?: ResponseCreateParamsStreaming["tool_choice"]; }; type OpenAICompletionsOptions = BaseStreamOptions & { @@ -1364,6 +1365,9 @@ export function buildOpenAIResponsesParams( transport: "stream", }), }); + if (options?.toolChoice) { + params.tool_choice = options.toolChoice; + } } if (model.reasoning) { if (options?.reasoningEffort || options?.reasoning || options?.reasoningSummary) { @@ -2188,6 +2192,7 @@ type OpenAIResponsesRequestParams = { top_p?: number; service_tier?: ResponseCreateParamsStreaming["service_tier"]; tools?: FunctionTool[]; + tool_choice?: ResponseCreateParamsStreaming["tool_choice"]; reasoning?: | { effort: OpenAIApiReasoningEffort } | {