mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
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:
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
44
open-sse/services/cloudCodeThinking.ts
Normal file
44
open-sse/services/cloudCodeThinking.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user