diff --git a/packages/ai/src/api/openai-responses.ts b/packages/ai/src/api/openai-responses.ts index 6bfb3d65b..2b0723798 100644 --- a/packages/ai/src/api/openai-responses.ts +++ b/packages/ai/src/api/openai-responses.ts @@ -75,6 +75,7 @@ function getCompat(model: Model<"openai-responses">): Required { expect(result.usage.cost.total).toBe((model.cost.input + model.cost.output) * multiplier * tokenScale); }); }); + +describe("openai-responses max_output_tokens compat", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("sends max_output_tokens by default", async () => { + let capturedPayload: { max_output_tokens?: number } | undefined; + + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + ); + + const stream = streamOpenAIResponses( + getModel("openai", "gpt-5.4"), + { + systemPrompt: "sys", + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { + apiKey: "test-key", + maxTokens: 1024, + onPayload: (payload) => { + capturedPayload = payload as { max_output_tokens?: number }; + }, + }, + ); + + for await (const event of stream) { + if (event.type === "done" || event.type === "error") break; + } + + expect(capturedPayload?.max_output_tokens).toBe(1024); + }); + + it("omits max_output_tokens when supportsMaxOutputTokens is false", async () => { + const baseModel = getModel("openai", "gpt-5.4"); + const model: Model<"openai-responses"> = { + ...baseModel, + compat: { ...baseModel.compat, supportsMaxOutputTokens: false }, + }; + let capturedPayload: { max_output_tokens?: number } | undefined; + + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + ); + + const stream = streamOpenAIResponses( + model, + { + systemPrompt: "sys", + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { + apiKey: "test-key", + maxTokens: 1024, + onPayload: (payload) => { + capturedPayload = payload as { max_output_tokens?: number }; + }, + }, + ); + + for await (const event of stream) { + if (event.type === "done" || event.type === "error") break; + } + + expect(capturedPayload?.max_output_tokens).toBeUndefined(); + }); +}); diff --git a/packages/coding-agent/src/core/model-config.ts b/packages/coding-agent/src/core/model-config.ts index 6036d5af9..4d426649b 100644 --- a/packages/coding-agent/src/core/model-config.ts +++ b/packages/coding-agent/src/core/model-config.ts @@ -121,6 +121,7 @@ const OpenAIResponsesCompatSchema = Type.Object({ supportsOpenAIGrammarTools: Type.Optional(Type.Boolean()), supportsAdditionalTools: Type.Optional(Type.Boolean()), supportsToolSearch: Type.Optional(Type.Boolean()), + supportsMaxOutputTokens: Type.Optional(Type.Boolean()), }); const AnthropicMessagesCompatSchema = Type.Object({