diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 62be5ac9b..744ced66a 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -732,7 +732,10 @@ function applyOpenAICompletionsCompatMetadata(model: Model): void { } function applyStrictToolCompatMetadata(model: Model): void { - if (model.provider === "openai" && model.api === "openai-responses") { + if ( + (model.provider === "openai" || model.provider === "cloudflare-ai-gateway") && + model.api === "openai-responses" + ) { model.compat = { ...(model.compat as OpenAIResponsesCompat | undefined), supportsStrictMode: true }; } else if (model.provider === "anthropic" && model.api === "anthropic-messages") { mergeAnthropicMessagesCompat(model, { supportsStrictTools: true }); diff --git a/packages/ai/test/openai-responses-compat.test.ts b/packages/ai/test/openai-responses-compat.test.ts index cabab8258..c98b2ed55 100644 --- a/packages/ai/test/openai-responses-compat.test.ts +++ b/packages/ai/test/openai-responses-compat.test.ts @@ -9,6 +9,7 @@ type CapturedHeaders = Headers | string[][] | Record; } function getHeader(headers: CapturedHeaders, name: string): string | null { @@ -153,6 +154,57 @@ describe("openai-responses provider defaults", () => { }); }); + it("sets strict mode explicitly for Cloudflare OpenAI Responses tools", async () => { + const model = getModel("cloudflare-ai-gateway", "gpt-5.6-sol"); + let capturedPayload: CapturedResponsesPayload | undefined; + + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("data: [DONE]\n\n", { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + ); + + const stream = streamOpenAIResponses( + model, + { + messages: [{ role: "user", content: "Use a tool.", timestamp: Date.now() }], + tools: [ + { + name: "ordinary", + description: "An ordinary tool", + parameters: Type.Object({ + path: Type.String(), + offset: Type.Optional(Type.Number()), + }), + }, + { + name: "constrained", + description: "A constrained tool", + parameters: Type.Object({ value: Type.String() }), + constrainedSampling: { type: "json_schema", strict: "prefer" }, + }, + ], + }, + { + apiKey: "test-key", + onPayload: (payload) => { + capturedPayload = payload as CapturedResponsesPayload; + }, + }, + ); + + for await (const event of stream) { + if (event.type === "done" || event.type === "error") break; + } + + expect(model.compat?.supportsStrictMode).toBe(true); + expect(capturedPayload?.tools).toEqual([ + expect.objectContaining({ name: "ordinary", strict: false }), + expect.objectContaining({ name: "constrained", strict: true }), + ]); + }); + it.each([ "gpt-5.1", "gpt-5.2",