From 14146712f38eda385f89d735ae2ff22907ad57be Mon Sep 17 00:00:00 2001 From: leon80900 <80900400+leon80900@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:28:14 +0800 Subject: [PATCH] fix(adapters): support structured output for Google and CCA models Carries textFormat json_schema and json_object into Gemini generationConfig (responseMimeType: application/json and responseJsonSchema). Ensures Cloud Code Assist (CCA) models (such as gemini-3.8-flash) can receive structured output requests without throwing a fail-closed 400 error. Rejects only image-capable models and malformed json_schema missing schemas. --- src/adapters/google-wire-compiler.ts | 10 ++ src/adapters/google.ts | 19 ++++ tests/adapters/google/google-adapter.test.ts | 97 ++++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/src/adapters/google-wire-compiler.ts b/src/adapters/google-wire-compiler.ts index 88c482ba7d..690b5852c0 100644 --- a/src/adapters/google-wire-compiler.ts +++ b/src/adapters/google-wire-compiler.ts @@ -141,6 +141,16 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined { const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m)); if (valid.length > 0) out.responseModalities = valid; } + // Structured output. This compiler is an allowlist, so without these two the adapter + // could set a schema and it would still be dropped before the wire. + if (typeof value.responseMimeType === "string" && value.responseMimeType.length > 0) { + out.responseMimeType = value.responseMimeType; + } + // Carried through unmodified: a caller-authored output schema is not a tool + // declaration, so sanitizeGeminiToolParameters must not touch it. + if (isObject(value.responseJsonSchema)) { + out.responseJsonSchema = value.responseJsonSchema; + } return Object.keys(out).length > 0 ? out : undefined; } diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 9e1b46307e..86879fe0fa 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -786,6 +786,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte : {}), async buildRequest(parsed: OcxParsedRequest) { + const requestedTextFormat = parsed.options.textFormat; + if (requestedTextFormat) { + if (isImageCapableModel(parsed.modelId)) { + throw new Error( + "google image-capable models cannot combine image output with structured output — " + + "remove response_format or select a text model", + ); + } + if (requestedTextFormat.type === "json_schema" && !requestedTextFormat.schema) { + throw new Error("google structured output requires text.format.schema for type json_schema"); + } + } const routedModelId = provider.googleMode === "cloud-code-assist" ? resolveAntigravityEffortWireModel( parsed.modelId, @@ -841,6 +853,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) { generationConfig.responseModalities = ["TEXT", "IMAGE"]; } + const textFormat = parsed.options.textFormat; + if (textFormat) { + generationConfig.responseMimeType = "application/json"; + if (textFormat.type === "json_schema" && textFormat.schema) { + generationConfig.responseJsonSchema = textFormat.schema; + } + } if (Object.keys(generationConfig).length > 0) body.generationConfig = generationConfig; const method = parsed.stream ? "streamGenerateContent" : "generateContent"; diff --git a/tests/adapters/google/google-adapter.test.ts b/tests/adapters/google/google-adapter.test.ts index 61e1d15d4b..169fedfa83 100644 --- a/tests/adapters/google/google-adapter.test.ts +++ b/tests/adapters/google/google-adapter.test.ts @@ -583,3 +583,100 @@ describe("google adapter — direct -tiered wire renames", () => { } }); }); + +describe("google adapter — structured output", () => { + test("carries json_schema format into generationConfig responseMimeType and responseJsonSchema", async () => { + const parsed = { + modelId: "gemini-2.5-flash", + stream: false, + options: { + textFormat: { + type: "json_schema", + name: "output_schema", + schema: { + type: "object", + properties: { answer: { type: "string" } }, + required: ["answer"], + }, + }, + }, + context: { messages: [{ role: "user", content: "hello" }] }, + } as unknown as OcxParsedRequest; + + const body = await geminiBody(parsed); + const gc = body.generationConfig as Record; + expect(gc.responseMimeType).toBe("application/json"); + expect(gc.responseJsonSchema).toEqual({ + type: "object", + properties: { answer: { type: "string" } }, + required: ["answer"], + }); + }); + + test("carries structured output through Cloud Code Assist envelope", async () => { + const ccaProvider = { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + googleMode: "cloud-code-assist" as const, + apiKey: "test-token", + project: "test-project", + }; + const parsed = { + modelId: "gemini-3.8-flash", + stream: false, + options: { + textFormat: { + type: "json_schema", + name: "decision_format", + schema: { + type: "object", + properties: { decision: { type: "string" } }, + required: ["decision"], + }, + }, + }, + context: { messages: [{ role: "user", content: "review request" }] }, + } as unknown as OcxParsedRequest; + + const request = await createGoogleAdapter(ccaProvider).buildRequest(parsed); + const envelope = JSON.parse(request.body) as Record; + const req = envelope.request as Record; + const gc = req.generationConfig as Record; + expect(gc.responseMimeType).toBe("application/json"); + expect(gc.responseJsonSchema).toEqual({ + type: "object", + properties: { decision: { type: "string" } }, + required: ["decision"], + }); + }); + + test("refuses structured output for image capable models", async () => { + const parsed = { + modelId: "gemini-3.1-flash-image", + stream: false, + options: { + textFormat: { type: "json_object" }, + }, + context: { messages: [{ role: "user", content: "generate image" }] }, + } as unknown as OcxParsedRequest; + + expect(createGoogleAdapter(provider).buildRequest(parsed)).rejects.toThrow( + "google image-capable models cannot combine image output with structured output", + ); + }); + + test("refuses json_schema without schema", async () => { + const parsed = { + modelId: "gemini-2.5-flash", + stream: false, + options: { + textFormat: { type: "json_schema" }, + }, + context: { messages: [{ role: "user", content: "hello" }] }, + } as unknown as OcxParsedRequest; + + expect(createGoogleAdapter(provider).buildRequest(parsed)).rejects.toThrow( + "google structured output requires text.format.schema for type json_schema", + ); + }); +});