From 11e343e5f877b21aa6f0b2ce52048baf0e34d788 Mon Sep 17 00:00:00 2001 From: agentHits <140916359+agentHits@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:59:59 -0400 Subject: [PATCH 1/3] fix(google): allow structured output for Gemini models on Cloud Code Assist - Lift blanket rejection on Cloud Code Assist for Gemini models (modelId starting with gemini-) - Route structured output into generationConfig.responseMimeType and responseJsonSchema inside envelope.request - Retain explicit fail-closed rejection for non-Gemini models (such as Claude) served through Cloud Code Assist - Keep existing refusals for image-capable models and schemaless json_schema - Update structure/providers/google.md and tests/adapters/google/google-structured-output.test.ts --- src/adapters/google.ts | 14 ++++----- structure/providers/google.md | 9 +++--- .../google/google-structured-output.test.ts | 29 +++++++++++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) 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..c2b845085f0 100644 --- a/tests/adapters/google/google-structured-output.test.ts +++ b/tests/adapters/google/google-structured-output.test.ts @@ -17,7 +17,7 @@ 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; const SCHEMA = { type: "object", @@ -57,6 +57,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 Record; + + 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 Record; + + 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 +107,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/); }); From 6c8f5d181b8aeaca47d0769f95fa61111fddccb5 Mon Sep 17 00:00:00 2001 From: agentHits <140916359+agentHits@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:59:59 -0400 Subject: [PATCH 2/3] fix(google): allow structured output for Gemini models on Cloud Code Assist - Lift blanket rejection on Cloud Code Assist for Gemini models (modelId starting with gemini-) - Route structured output into generationConfig.responseMimeType and responseJsonSchema inside envelope.request - Retain explicit fail-closed rejection for non-Gemini models (such as Claude) served through Cloud Code Assist - Keep existing refusals for image-capable models and schemaless json_schema - Update structure/providers/google.md and tests/adapters/google/google-structured-output.test.ts --- src/adapters/google.ts | 14 ++++----- structure/providers/google.md | 9 +++--- .../google/google-structured-output.test.ts | 29 +++++++++++++++++-- 3 files changed, 38 insertions(+), 14 deletions(-) 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..c2b845085f0 100644 --- a/tests/adapters/google/google-structured-output.test.ts +++ b/tests/adapters/google/google-structured-output.test.ts @@ -17,7 +17,7 @@ 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; const SCHEMA = { type: "object", @@ -57,6 +57,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 Record; + + 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 Record; + + 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 +107,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/); }); From 22965dc2d3303217a6da25042997c41ca026c8c3 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 15 Sep 2026 19:47:41 +0900 Subject: [PATCH 3/3] test(google): pin CCA image refusal and typed schema envelope Add the branch-specific image-output refusal regression and replace the two untyped envelope casts. Static inspection only; product tests and CI remain operator-owned. Co-authored-by: agentHits <140916359+agentHits@users.noreply.github.com> --- .../google/google-structured-output.test.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/adapters/google/google-structured-output.test.ts b/tests/adapters/google/google-structured-output.test.ts index c2b845085f0..b23bbcabc23 100644 --- a/tests/adapters/google/google-structured-output.test.ts +++ b/tests/adapters/google/google-structured-output.test.ts @@ -19,6 +19,17 @@ const aiStudio = { adapter: "google", baseUrl: "https://generativelanguage.googl 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", project: "test-project" } as unknown as OcxProviderConfig; +type CloudCodeAssistEnvelope = { + generationConfig?: unknown; + request?: { + generationConfig?: { + responseMimeType?: unknown; + responseJsonSchema?: unknown; + responseSchema?: unknown; + }; + }; +}; + const SCHEMA = { type: "object", properties: { answer: { type: "string" } }, @@ -61,7 +72,7 @@ describe("F3 Google structured output reaches the generateContent wire", () => { 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 Record; + 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"); @@ -71,7 +82,7 @@ describe("F3 Google structured output reaches the generateContent wire", () => { 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 Record; + 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"); @@ -121,6 +132,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"));