diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 5489e9fbb9..494d750836 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -87,6 +87,9 @@ const ANTIGRAVITY_TRANSIENT_STATUSES = new Set([ HTTP_STATUS.SERVICE_UNAVAILABLE, HTTP_STATUS.GATEWAY_TIMEOUT, ]); +const ANTIGRAVITY_UNSUPPORTED_SAFETY_CATEGORIES = new Set([ + "HARM_CATEGORY_CIVIC_INTEGRITY", +]); // The upstream API uses plain model IDs (no -high/-low suffix). // Tier suffixes were speculative and caused 404 for gemini-3.x models — the // bare-Pro→Low normalization was retired (the set stayed empty, making the guard @@ -440,6 +443,14 @@ function asRecord(value: unknown): Record | null { : null; } +function getAntigravitySafetySettings(safetySettings: unknown): unknown[] { + const source = Array.isArray(safetySettings) ? safetySettings : DEFAULT_SAFETY_SETTINGS; + return source.filter((setting) => { + const category = asRecord(setting)?.category; + return typeof category !== "string" || !ANTIGRAVITY_UNSUPPORTED_SAFETY_CATEGORIES.has(category); + }); +} + function sanitizeAntigravityGeminiRequest( request: Record ): Record { @@ -687,12 +698,10 @@ export class AntigravityExecutor extends BaseExecutor { credentials, typeof normalizedRequest?.sessionId === "string" ? normalizedRequest.sessionId : undefined ), - // #5003: default to all-OFF safety for parity with the native Gemini paths - // (claude-to-gemini / openai-to-gemini both default to DEFAULT_SAFETY_SETTINGS). - // Previously this was `undefined`, which JSON.stringify drops, so Google Cloud Code - // applied its server-side defaults that false-flag benign technical prompts as - // `prohibited_content` (HTTP 200 + blocked body → terminal combo failover). - safetySettings: normalizedRequest?.safetySettings ?? DEFAULT_SAFETY_SETTINGS, + // #5003: send explicit all-OFF safety entries that Cloud Code accepts. Omitting the + // field lets Cloud Code apply server-side defaults that false-flag benign technical + // prompts as `prohibited_content`. + safetySettings: getAntigravitySafetySettings(normalizedRequest?.safetySettings), toolConfig: Array.isArray(normalizedRequest?.tools) && normalizedRequest.tools.length > 0 ? { functionCallingConfig: { mode: "VALIDATED" } } @@ -700,7 +709,9 @@ export class AntigravityExecutor extends BaseExecutor { }; const transformedRequest = isClaude - ? stripTrailingAntigravityAssistantTurn(sanitizeAntigravityGeminiRequest(rawTransformedRequest)) + ? stripTrailingAntigravityAssistantTurn( + sanitizeAntigravityGeminiRequest(rawTransformedRequest) + ) : rawTransformedRequest; // Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor") diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 15aea48194..2d3fb8811f 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -633,6 +633,7 @@ function wrapInCloudCodeEnvelope(model, cloudCodeRequest, credentials = null) { systemInstruction: cloudCodeRequest.systemInstruction, generationConfig: applyAntigravityGenerationDefaults(cloudCodeRequest.generationConfig), tools: cloudCodeRequest.tools, + safetySettings: cloudCodeRequest.safetySettings, }, model: cleanModel, userAgent: getAntigravityEnvelopeUserAgent(credentials), diff --git a/tests/unit/antigravity-safetysettings-5003.test.ts b/tests/unit/antigravity-safetysettings-5003.test.ts index 152a5eb3ff..f4cc058d15 100644 --- a/tests/unit/antigravity-safetysettings-5003.test.ts +++ b/tests/unit/antigravity-safetysettings-5003.test.ts @@ -3,13 +3,14 @@ import assert from "node:assert/strict"; import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts"; import { DEFAULT_SAFETY_SETTINGS } from "../../open-sse/translator/helpers/geminiHelper.ts"; +import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.ts"; // Regression for #5003: the Antigravity (Google Cloud Code) request builder explicitly set // `safetySettings: undefined`, which `JSON.stringify` drops entirely. With no safetySettings // reaching Cloud Code, Google applies its server-side safety defaults that false-flag benign // technical prompts as `prohibited_content` (HTTP 200 with a blocked body that combo failover -// treats as terminal). The native Gemini paths all default to all-OFF -// (DEFAULT_SAFETY_SETTINGS); Antigravity must match for parity. +// treats as terminal). Antigravity still needs explicit all-OFF safety settings, +// but Cloud Code rejects HARM_CATEGORY_CIVIC_INTEGRITY on the v1internal endpoint. test("transformRequest defaults safetySettings to all-OFF when none supplied (#5003)", async () => { const executor = new AntigravityExecutor(); @@ -26,17 +27,21 @@ test("transformRequest defaults safetySettings to all-OFF when none supplied (#5 if (result instanceof Response) throw new Error("Unexpected Response from transformRequest"); const innerRequest = result.request as Record; + const antigravitySafetySettings = DEFAULT_SAFETY_SETTINGS.filter( + (setting) => setting.category !== "HARM_CATEGORY_CIVIC_INTEGRITY" + ); assert.deepEqual( innerRequest.safetySettings, - DEFAULT_SAFETY_SETTINGS, - "safetySettings must default to all-OFF for parity with native Gemini paths" + antigravitySafetySettings, + "safetySettings must default to all-OFF entries accepted by Cloud Code" ); }); -test("transformRequest honors a caller-supplied safetySettings (#5003)", async () => { +test("transformRequest honors caller-supplied safetySettings accepted by Cloud Code (#5003)", async () => { const executor = new AntigravityExecutor(); const callerSafety = [ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, + { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }, ]; const body = { request: { @@ -54,7 +59,34 @@ test("transformRequest honors a caller-supplied safetySettings (#5003)", async ( const innerRequest = result.request as Record; assert.deepEqual( innerRequest.safetySettings, - callerSafety, - "a caller-supplied safetySettings must not be clobbered" + [{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" }], + "caller-supplied safetySettings should preserve accepted entries and drop rejected ones" ); }); + +test("OpenAI Antigravity translation preserves caller-supplied safetySettings (#5003)", async () => { + const executor = new AntigravityExecutor(); + const callerSafety = [ + { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, + { category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }, + ]; + const translated = openaiToAntigravityRequest( + "gemini-2.5-flash", + { + messages: [{ role: "user", content: "hi" }], + safetySettings: callerSafety, + }, + true, + { projectId: "project-1" } + ); + + const result = await executor.transformRequest("antigravity/gemini-2.5-flash", translated, true, { + projectId: "project-1", + }); + + if (result instanceof Response) throw new Error("Unexpected Response from transformRequest"); + const innerRequest = result.request as Record; + assert.deepEqual(innerRequest.safetySettings, [ + { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" }, + ]); +});