fix(cloud-code): scope thinking stripping to executor boundaries (#1401)

* fix(cloud-code): scope thinking stripping to executors

* fix(cloud-code): guard antigravity normalized body
This commit is contained in:
Randi
2026-04-18 16:15:59 -04:00
committed by GitHub
parent ecab0edad1
commit 293290e12a
8 changed files with 186 additions and 17 deletions

View File

@@ -15,6 +15,10 @@ import { persistCreditBalance, getAllPersistedCreditBalances } from "@/lib/db/cr
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
import { resolveAntigravityVersion } from "../services/antigravityVersion.ts";
import { resolveAntigravityModelId } from "../config/antigravityModelAliases.ts";
import {
shouldStripCloudCodeThinking,
stripCloudCodeThinkingConfig,
} from "../services/cloudCodeThinking.ts";
const MAX_RETRY_AFTER_MS = 60_000;
const LONG_RETRY_THRESHOLD_MS = 60_000;
@@ -166,9 +170,15 @@ export class AntigravityExecutor extends BaseExecutor {
return resp as unknown as never;
}
const upstreamModel = cleanModelName(model);
const baseBody = body && typeof body === "object" ? body : {};
const normalizedBody = shouldStripCloudCodeThinking(this.provider, upstreamModel)
? stripCloudCodeThinkingConfig(baseBody)
: baseBody;
// Fix contents for Claude models via Antigravity
const normalizedContents =
body.request?.contents?.map((c) => {
normalizedBody.request?.contents?.map((c) => {
let role = c.role;
// functionResponse must be role "user" for Claude models
if (c.parts?.some((p) => p.functionResponse)) {
@@ -203,18 +213,16 @@ export class AntigravityExecutor extends BaseExecutor {
}
const transformedRequest = {
...body.request,
...normalizedBody.request,
...(contents.length > 0 && { contents }),
sessionId: body.request?.sessionId || this.generateSessionId(),
sessionId: normalizedBody.request?.sessionId || this.generateSessionId(),
safetySettings: undefined,
toolConfig:
body.request?.tools?.length > 0
normalizedBody.request?.tools?.length > 0
? { functionCallingConfig: { mode: "VALIDATED" } }
: body.request?.toolConfig,
: normalizedBody.request?.toolConfig,
};
const upstreamModel = cleanModelName(model);
// Obfuscate sensitive client names in user content (e.g. "OpenCode", "Cursor")
const requestContents = transformedRequest.contents;
if (Array.isArray(requestContents)) {
@@ -230,7 +238,7 @@ export class AntigravityExecutor extends BaseExecutor {
}
return {
...body,
...normalizedBody,
project: projectId,
model: upstreamModel,
userAgent: "antigravity",

View File

@@ -3,6 +3,10 @@ import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
import { geminiCLIUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts";
import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts";
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
import {
shouldStripCloudCodeThinking,
stripCloudCodeThinkingConfig,
} from "../services/cloudCodeThinking.ts";
const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
const ONBOARD_USER_URL = "https://cloudcode-pa.googleapis.com/v1internal:onboardUser";
@@ -276,16 +280,24 @@ export class GeminiCLIExecutor extends BaseExecutor {
async transformRequest(model, body, stream, credentials) {
this._currentModel = normalizeGeminiModel(model);
const normalizedBody =
shouldStripCloudCodeThinking(this.provider, this._currentModel) &&
body &&
typeof body === "object"
? stripCloudCodeThinkingConfig(body)
: body;
// Refresh the project ID via loadCodeAssist (cached for 30s).
if (body && typeof body === "object" && body.request && credentials.accessToken) {
const freshProject = await this.refreshProject(credentials.accessToken);
if (freshProject) {
body.project = freshProject;
if (normalizedBody && typeof normalizedBody === "object" && normalizedBody.request) {
if (credentials.accessToken) {
const freshProject = await this.refreshProject(credentials.accessToken);
if (freshProject) {
normalizedBody.project = freshProject;
}
}
// Obfuscate sensitive client names in user content
const contents = body.request?.contents;
const contents = normalizedBody.request?.contents;
if (Array.isArray(contents)) {
for (const msg of contents) {
if (Array.isArray(msg.parts)) {
@@ -298,7 +310,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
}
}
}
return body;
return normalizedBody;
}
async refreshCredentials(credentials, log) {

View File

@@ -30,6 +30,7 @@
- **`intentClassifier.ts`** — Classifies request intent (chat, embedding, image, video, etc.) for intelligent routing.
- **`taskAwareRouter.ts`** — Routes based on task characteristics (reasoning-heavy → o1, code-gen → Cursor, long-context → Claude).
- **`thinkingBudget.ts`** — Allocates thinking tokens for o1/o3 models; enforces per-request budget.
Provider-specific Cloud Code compatibility stripping belongs in executors, not in this service.
- **`contextManager.ts`** — Injects routing context (system prompts, memory) into requests.
### Model Lifecycle & Fallback

View File

@@ -0,0 +1,44 @@
import { supportsReasoning } from "./modelCapabilities.ts";
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function stripGeminiThinkingConfig(value: unknown): unknown {
if (!isRecord(value)) return value;
if (!("thinkingConfig" in value) && !("thinking_config" in value)) return value;
const next = { ...value };
delete next.thinkingConfig;
delete next.thinking_config;
return next;
}
export function shouldStripCloudCodeThinking(provider: string, model: string): boolean {
if (!provider || !model) return false;
return !supportsReasoning(`${provider}/${model}`);
}
export function stripCloudCodeThinkingConfig(
body: Record<string, unknown>
): Record<string, unknown> {
const next = { ...body };
delete next.reasoning_effort;
delete next.reasoning;
delete next.thinking;
if ("generationConfig" in next) {
next.generationConfig = stripGeminiThinkingConfig(next.generationConfig);
}
if (isRecord(next.request)) {
const request = { ...next.request };
if ("generationConfig" in request) {
request.generationConfig = stripGeminiThinkingConfig(request.generationConfig);
}
next.request = request;
}
return next;
}

View File

@@ -157,10 +157,9 @@ export function applyThinkingBudget(body, config = null) {
if (!body || typeof body !== "object") return body;
// Early exit: strip ALL reasoning/thinking params for models that don't support them.
// Sending thinking params to unsupported models (e.g. AG claude-sonnet-4-6) causes 400 errors.
// Provider-specific Cloud Code restrictions should be handled at the executor boundary.
const modelStr = typeof body.model === "string" ? body.model : "";
const isClaude = modelStr.toLowerCase().includes("claude");
if (modelStr && (!supportsReasoning(modelStr) || (!isClaude && modelStr.includes("gemini")))) {
if (modelStr && !supportsReasoning(modelStr)) {
return stripThinkingConfig(body);
}

View File

@@ -88,6 +88,63 @@ test("AntigravityExecutor.transformRequest normalizes model, project and content
assert.equal(result.request.contents[1].role, "user");
});
test("AntigravityExecutor.transformRequest strips thinking config for Cloud Code models that do not support reasoning", async () => {
const executor = new AntigravityExecutor();
const body = {
reasoning_effort: "high",
request: {
generationConfig: {
thinkingConfig: {
thinkingBudget: 8192,
includeThoughts: true,
},
},
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
assert.equal(result.reasoning_effort, undefined);
assert.equal(result.request.generationConfig.thinkingConfig, undefined);
});
test("AntigravityExecutor.transformRequest preserves thinking config for supported Gemini models", async () => {
const executor = new AntigravityExecutor();
const body = {
request: {
generationConfig: {
thinkingConfig: {
thinkingBudget: 8192,
includeThoughts: true,
},
},
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
};
const result = await executor.transformRequest("antigravity/gemini-3.1-pro-high", body, true, {
projectId: "project-1",
});
assert.equal(result.request.generationConfig.thinkingConfig.thinkingBudget, 8192);
assert.equal(result.request.generationConfig.thinkingConfig.includeThoughts, true);
});
test("AntigravityExecutor.transformRequest tolerates a missing body when projectId is present", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest("antigravity/gemini-3.1-pro", null, true, {
projectId: "project-1",
});
assert.equal(result.project, "project-1");
assert.equal(result.model, "gemini-3.1-pro-low");
assert.ok(result.request.sessionId);
});
test("AntigravityExecutor.transformRequest returns a structured error response when projectId is missing", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(

View File

@@ -56,6 +56,42 @@ test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transfo
}
});
test("GeminiCLIExecutor.transformRequest preserves thinking config for supported Gemini models", async () => {
const executor = new GeminiCLIExecutor();
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(JSON.stringify({ cloudaicompanionProject: "fresh-project-id" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
try {
const transformed = await executor.transformRequest(
"models/gemini-3.1-pro-preview",
{
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
generationConfig: {
thinkingConfig: {
thinkingBudget: 8192,
includeThoughts: true,
},
},
},
},
true,
{ accessToken: "access-token-1" }
);
assert.equal(transformed.project, "fresh-project-id");
assert.equal(transformed.request.generationConfig.thinkingConfig.thinkingBudget, 8192);
assert.equal(transformed.request.generationConfig.thinkingConfig.includeThoughts, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test("GeminiCLIExecutor.refreshProject returns null on failed loadCodeAssist responses", async () => {
const executor = new GeminiCLIExecutor();
const originalFetch = globalThis.fetch;

View File

@@ -42,6 +42,18 @@ test("PASSTHROUGH: body unchanged", () => {
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
});
test("PASSTHROUGH: keeps reasoning_effort for OpenAI-compatible Gemini routes", () => {
setThinkingBudgetConfig({ mode: ThinkingMode.PASSTHROUGH });
const body = {
model: "openai-compatible-sp-google/gemini-3.1-pro-preview",
messages: [{ role: "user", content: "hello" }],
reasoning_effort: "high",
};
const result = applyThinkingBudget(body);
assert.equal(result.reasoning_effort, "high");
setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG);
});
// ─── AUTO Mode ──────────────────────────────────────────────────────────────
test("AUTO: strips Claude thinking config", () => {