Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
9 changes: 5 additions & 4 deletions structure/providers/google.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
46 changes: 43 additions & 3 deletions tests/adapters/google/google-structured-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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();
});

Comment thread
agentHits marked this conversation as resolved.
test("the schema survives compilation byte-for-byte, unsanitized", async () => {
const nested = {
type: "object",
Expand Down Expand Up @@ -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/);
});

Expand All @@ -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"));

Expand Down
Loading