fix(antigravity): sanitize Cloud Code safety settings (#6839)

Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-11 04:25:32 -03:00
committed by GitHub
parent a3e38a2c0c
commit 112b1499df
3 changed files with 58 additions and 14 deletions

View File

@@ -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<string>([
"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<string, unknown> | 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<string, unknown>
): Record<string, unknown> {
@@ -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")

View File

@@ -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),

View File

@@ -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<string, unknown>;
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<string, unknown>;
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<string, unknown>;
assert.deepEqual(innerRequest.safetySettings, [
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" },
]);
});