diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 9617c1ac100..8829dc07846 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -796,14 +796,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // body, URL or credential. const requestedTextFormat = parsed.options.textFormat; if (requestedTextFormat) { - if (provider.googleMode === "cloud-code-assist") { - // Not implemented or verified by opencodex for the Cloud Code Assist envelope, - // including Claude models served through it. This is not a claim that the - // upstream cannot do it — silence would return unconstrained prose as success, - // which is the failure this fix exists to remove. + if (provider.googleMode === "cloud-code-assist" && !parsed.modelId.startsWith("gemini-")) { + // Not implemented by opencodex for non-Gemini models (including Claude) + // served through the Cloud Code Assist envelope. This is not a claim that + // the upstream cannot do it — silence would return unconstrained prose as success, + // which is the failure this refusal exists to prevent. throw new Error( - "google cloud-code-assist structured output is not implemented by opencodex — " - + "remove response_format or route this model through AI Studio or Vertex", + "google cloud-code-assist structured output is not implemented by opencodex for non-Gemini models — " + + "remove response_format or route this model through a direct provider", ); } if (isImageCapableModel(parsed.modelId)) { diff --git a/structure/providers/google.md b/structure/providers/google.md index a403777ee30..21d22fc15c6 100644 --- a/structure/providers/google.md +++ b/structure/providers/google.md @@ -62,12 +62,13 @@ The schema is carried verbatim. `sanitizeGeminiToolParameters` narrows a schema the function-declaration subset and must never be applied to a caller-authored output schema. `compileGenerationConfig` in `google-wire-compiler.ts` is a whitelist, so both keys are listed there as well; setting them in the adapter alone would drop them -before the wire. +before the wire. On Cloud Code Assist, Gemini models carry these same keys inside +`envelope.request.generationConfig`. Three cases refuse explicitly rather than dropping the constraint silently: -cloud-code-assist, which opencodex does not implement or verify for this field -(including Claude models served through that envelope — this is not a claim about -what the upstream can do); an image-capable model, whose `responseModalities` +non-Gemini models on Cloud Code Assist (such as Claude models served through that +envelope), which opencodex does not implement or verify for this field (this is not +a claim about what the upstream can do); an image-capable model, whose `responseModalities` configuration contradicts JSON-constrained text; and a `json_schema` format carrying no schema, which would otherwise downgrade to bare JSON mode. An image-capable model with no structured-output request keeps its existing `responseModalities` behavior. diff --git a/tests/adapters/google/google-structured-output.test.ts b/tests/adapters/google/google-structured-output.test.ts index f569b0adc3b..b93c5734b25 100644 --- a/tests/adapters/google/google-structured-output.test.ts +++ b/tests/adapters/google/google-structured-output.test.ts @@ -17,7 +17,17 @@ import type { OcxParsedRequest, OcxProviderConfig } from "../../../src/types"; const aiStudio = { adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", apiKey: "key" } as unknown as OcxProviderConfig; const vertex = { adapter: "google", googleMode: "vertex", baseUrl: "https://aiplatform.googleapis.com", apiKey: "key" } as unknown as OcxProviderConfig; -const cca = { adapter: "google", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", apiKey: "token" } as unknown as OcxProviderConfig; +const cca = { adapter: "google", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", apiKey: "token", project: "test-project" } as unknown as OcxProviderConfig; +type CloudCodeAssistEnvelope = { + generationConfig?: unknown; + request?: { + generationConfig?: { + responseMimeType?: unknown; + responseJsonSchema?: unknown; + responseSchema?: unknown; + }; + }; +}; const SCHEMA = { type: "object", @@ -57,6 +67,27 @@ describe("F3 Google structured output reaches the generateContent wire", () => { expect(config.responseJsonSchema).toEqual(SCHEMA); }); + test("Gemini-on-CCA carries responseMimeType and responseJsonSchema inside envelope.request", async () => { + const { body } = await createGoogleAdapter(cca).buildRequest( + parsed({ type: "json_schema", name: "answer", schema: SCHEMA, strict: true }), + ); + const envelope = JSON.parse(typeof body === "string" ? body : JSON.stringify(body)) as CloudCodeAssistEnvelope; + + expect(envelope.generationConfig).toBeUndefined(); + expect(envelope.request?.generationConfig?.responseMimeType).toBe("application/json"); + expect(envelope.request?.generationConfig?.responseJsonSchema).toEqual(SCHEMA); + expect(envelope.request?.generationConfig?.responseSchema).toBeUndefined(); + }); + + test("json_object on Cloud Code Assist sets only responseMimeType in envelope.request", async () => { + const { body } = await createGoogleAdapter(cca).buildRequest(parsed({ type: "json_object" })); + const envelope = JSON.parse(typeof body === "string" ? body : JSON.stringify(body)) as CloudCodeAssistEnvelope; + + expect(envelope.generationConfig).toBeUndefined(); + expect(envelope.request?.generationConfig?.responseMimeType).toBe("application/json"); + expect(envelope.request?.generationConfig?.responseJsonSchema).toBeUndefined(); + }); + test("the schema survives compilation byte-for-byte, unsanitized", async () => { const nested = { type: "object", @@ -86,8 +117,10 @@ describe("F3 Google structured output reaches the generateContent wire", () => { }); describe("F3 unsupported modes refuse explicitly instead of dropping the schema", () => { - test("cloud-code-assist reports that opencodex does not implement it", async () => { - const promise = createGoogleAdapter(cca).buildRequest(parsed({ type: "json_schema", schema: SCHEMA })); + test("Claude-on-CCA with textFormat reports that opencodex does not implement it", async () => { + const promise = createGoogleAdapter(cca).buildRequest( + parsed({ type: "json_schema", schema: SCHEMA }, "claude-3-7-sonnet"), + ); await expect(promise).rejects.toThrow(/not implemented by opencodex/); }); @@ -98,6 +131,13 @@ describe("F3 unsupported modes refuse explicitly instead of dropping the schema" await expect(promise).rejects.toThrow(/cannot combine image output with structured output/); }); + test("an image-capable Cloud Code Assist model refuses the structured-output conflict", async () => { + const promise = createGoogleAdapter(cca).buildRequest( + parsed({ type: "json_schema", schema: SCHEMA }, "gemini-3-pro-image-preview"), + ); + await expect(promise).rejects.toThrow("cannot combine image output with structured output"); + }); + test("an image-capable model with NO schema keeps its image behavior", async () => { const config = await generationConfig(aiStudio, parsed(undefined, "gemini-3-pro-image-preview"));