diff --git a/open-sse/config/toolCloaking.ts b/open-sse/config/toolCloaking.ts index 7fa5cd3b35f..429a6ca28bc 100644 --- a/open-sse/config/toolCloaking.ts +++ b/open-sse/config/toolCloaking.ts @@ -27,7 +27,7 @@ const AG_DEFAULT_TOOL_NAMES = [ const AG_DECOY_TOOL_NAMES = [ ...AG_DEFAULT_TOOL_NAMES, - "mcp_sequential-thinking_sequentialthinking", + "mcp_sequential_thinking_sequentialthinking", ] as const; export const AG_DEFAULT_TOOLS = new Set(AG_DEFAULT_TOOL_NAMES); diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index 9cf0cb989da..9a6f23b479e 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -28,15 +28,29 @@ const CREDITS_EXHAUSTED_TTL_MS = 5 * 60 * 60 * 1000; // 5 hours const BARE_PRO_IDS = new Set(["gemini-3.1-pro"]); +function cloneAntigravityRequestBody(body: unknown): unknown { + if (!body || typeof body !== "object") { + return body; + } + + try { + return structuredClone(body); + } catch { + return JSON.parse(JSON.stringify(body)); + } +} + function serializeAntigravityRequest( provider: string, headers: Record, body: unknown ): { headers: Record; bodyString: string } { + const serializedBody = cloneAntigravityRequestBody(body); + if (!isCliCompatEnabled(provider)) { - return { headers, bodyString: JSON.stringify(body) }; + return { headers, bodyString: JSON.stringify(serializedBody) }; } - return applyFingerprint(provider, { ...headers }, body); + return applyFingerprint(provider, { ...headers }, serializedBody); } type AntigravityCollectedStream = { @@ -585,7 +599,11 @@ export class AntigravityExecutor extends BaseExecutor { } try { - const serializedRequest = serializeAntigravityRequest(this.provider, headers, transformedBody); + const serializedRequest = serializeAntigravityRequest( + this.provider, + headers, + transformedBody + ); const finalHeaders = serializedRequest.headers; const response = await fetch(url, { diff --git a/open-sse/translator/helpers/geminiToolsSanitizer.ts b/open-sse/translator/helpers/geminiToolsSanitizer.ts index e35a9df7b1d..d76f23cf7c5 100644 --- a/open-sse/translator/helpers/geminiToolsSanitizer.ts +++ b/open-sse/translator/helpers/geminiToolsSanitizer.ts @@ -30,12 +30,17 @@ function normalizeGeminiToolName( options: GeminiToolSanitizationOptions = {} ): string { const trimmed = name.trim(); - if (!options.stripNamespace) { - return trimmed; - } - - const namespaceIndex = trimmed.indexOf(":"); - return namespaceIndex >= 0 ? trimmed.slice(namespaceIndex + 1) : trimmed; + const namespaceStripped = !options.stripNamespace + ? trimmed + : (() => { + const namespaceIndex = trimmed.indexOf(":"); + return namespaceIndex >= 0 ? trimmed.slice(namespaceIndex + 1) : trimmed; + })(); + + return namespaceStripped + .replace(/[^a-zA-Z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_+|_+$/g, ""); } function buildHashedGeminiToolName( diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts index ff5e2a2803b..4099dec224e 100644 --- a/open-sse/translator/request/claude-to-gemini.ts +++ b/open-sse/translator/request/claude-to-gemini.ts @@ -7,6 +7,7 @@ import { } from "../helpers/geminiHelper.ts"; import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.ts"; import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts"; +import { capMaxOutputTokens } from "../../../src/lib/modelCapabilities.ts"; /** * Direct Claude → Gemini request translator. @@ -49,7 +50,7 @@ export function claudeToGeminiRequest(model, body, stream) { result.generationConfig.topK = body.top_k; } if (body.max_tokens !== undefined) { - result.generationConfig.maxOutputTokens = body.max_tokens; + result.generationConfig.maxOutputTokens = capMaxOutputTokens(model, body.max_tokens); } // ── System instruction ───────────────────────────────────────── @@ -62,7 +63,7 @@ export function claudeToGeminiRequest(model, body, stream) { } if (systemText) { result.systemInstruction = { - role: "user", + role: "system", parts: [{ text: systemText }], }; } diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index 73c20e5bc82..d4b240a568f 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -186,7 +186,7 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName if (systemText) { if (!result.systemInstruction) { result.systemInstruction = { - role: "user", + role: "system", parts: [{ text: systemText }], }; } else { @@ -413,7 +413,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra if (envelope.request.systemInstruction?.parts) { envelope.request.systemInstruction.parts.unshift(defaultPart); } else { - envelope.request.systemInstruction = { role: "user", parts: [defaultPart] }; + envelope.request.systemInstruction = { role: "system", parts: [defaultPart] }; } // Add toolConfig for Antigravity @@ -449,6 +449,28 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu const cleanModel = model.includes("/") ? model.split("/").pop()! : model; + const requestedMaxOutputTokens = + typeof claudeRequest.max_tokens === "number" && Number.isFinite(claudeRequest.max_tokens) + ? claudeRequest.max_tokens + : 4096; + const generationConfig: GeminiGenerationConfig = { + temperature: claudeRequest.temperature || 1, + maxOutputTokens: capMaxOutputTokens(cleanModel, requestedMaxOutputTokens), + }; + const thinkingBudget = + claudeRequest.thinking?.type === "enabled" && + typeof claudeRequest.thinking.budget_tokens === "number" && + Number.isFinite(claudeRequest.thinking.budget_tokens) + ? Math.floor(claudeRequest.thinking.budget_tokens) + : null; + + if (thinkingBudget && thinkingBudget > 0) { + generationConfig.thinkingConfig = { + thinkingBudget: capThinkingBudget(cleanModel, thinkingBudget), + includeThoughts: true, + }; + } + const envelope: CloudCodeEnvelope = { project: projectId, model: cleanModel, @@ -458,10 +480,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu request: { sessionId: generateSessionId(), contents: [], - generationConfig: { - temperature: claudeRequest.temperature || 1, - maxOutputTokens: claudeRequest.max_tokens || 4096, - }, + generationConfig, }, }; @@ -555,7 +574,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu } } - envelope.request.systemInstruction = { role: "user", parts: systemParts }; + envelope.request.systemInstruction = { role: "system", parts: systemParts }; const changedToolNameMap = buildChangedToolNameMap(toolNameMap); if (changedToolNameMap) { diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index cf021d9e8cb..f5d9abcaae6 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -717,6 +717,19 @@ const locateCommandCandidate = async ( reason: null, }; } + + if (result.installed && result.reason === "not_executable") { + return { + command: commands[0], + installed: true, + commandPath: result.commandPath, + reason: "not_executable", + }; + } + + if (result.reason && result.reason !== "not_found") { + return { command: commands[0], ...result }; + } } } diff --git a/tests/unit/antigravity-tool-cloaking.test.ts b/tests/unit/antigravity-tool-cloaking.test.ts index ccdb0cca960..cf413718e47 100644 --- a/tests/unit/antigravity-tool-cloaking.test.ts +++ b/tests/unit/antigravity-tool-cloaking.test.ts @@ -46,6 +46,10 @@ test("cloakAntigravityToolPayload cloaks custom tools, preserves native tools an assert.ok(names.includes(`workspace_read${AG_TOOL_SUFFIX}`)); assert.ok(names.includes("run_command")); assert.ok(names.includes("browser_subagent")); + assert.ok(names.includes("mcp_sequential_thinking_sequentialthinking")); + for (const name of names) { + assert.match(name, /^[a-zA-Z0-9_]+$/); + } assert.equal( result.body.request.contents[0].parts[0].functionCall.name, `workspace_read${AG_TOOL_SUFFIX}` diff --git a/tests/unit/cli-runtime-extended.test.ts b/tests/unit/cli-runtime-extended.test.ts index c4aadfe3dcb..b519e1c8847 100644 --- a/tests/unit/cli-runtime-extended.test.ts +++ b/tests/unit/cli-runtime-extended.test.ts @@ -226,7 +226,7 @@ test("getCliRuntimeStatus ignores suspicious known-path binaries and symlink esc const suspiciousStatus = await cliRuntime.getCliRuntimeStatus("qoder"); assert.equal(suspiciousStatus.installed, false); - assert.equal(suspiciousStatus.reason, "not_found"); + assert.equal(suspiciousStatus.reason, "suspicious_size"); if (process.platform !== "win32") { const escapePrefix = createTempDir("omniroute-cli-escape-"); @@ -246,7 +246,7 @@ test("getCliRuntimeStatus ignores suspicious known-path binaries and symlink esc const escapedStatus = await escapedRuntime.getCliRuntimeStatus("qoder"); assert.equal(escapedStatus.installed, false); - assert.equal(escapedStatus.reason, "not_found"); + assert.equal(escapedStatus.reason, "symlink_escape"); } }); diff --git a/tests/unit/memory-route.test.ts b/tests/unit/memory-route.test.ts index f7dccce3490..7ee36213944 100644 --- a/tests/unit/memory-route.test.ts +++ b/tests/unit/memory-route.test.ts @@ -67,10 +67,10 @@ test("GET /api/memory filters by q and returns matching stats", async () => { assert.equal(response.status, 200); const body = (await response.json()) as any; - assert.deepEqual( - body.data.map((memory) => memory.key), - ["typescript:tooling", "typescript:guide"] - ); + assert.deepEqual(body.data.map((memory) => memory.key).sort(), [ + "typescript:guide", + "typescript:tooling", + ]); assert.equal(body.total, 2); assert.equal(body.stats.total, 2); assert.deepEqual(body.stats.byType, { factual: 1, semantic: 1 }); diff --git a/tests/unit/translator-claude-to-gemini.test.ts b/tests/unit/translator-claude-to-gemini.test.ts index e1e7058604f..b178a72b6bc 100644 --- a/tests/unit/translator-claude-to-gemini.test.ts +++ b/tests/unit/translator-claude-to-gemini.test.ts @@ -76,7 +76,7 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools", ); assert.deepEqual(result.systemInstruction, { - role: "user", + role: "system", parts: [{ text: "Rules" }], }); assert.equal(result.contents[0].role, "model"); @@ -92,6 +92,7 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools", }, }); assert.equal(result.generationConfig.maxOutputTokens, 256); + assert.match((result as any).tools[0].functionDeclarations[0].name, /^[a-zA-Z0-9_]+$/); assert.equal(result.generationConfig.temperature, 0.4); assert.equal(result.generationConfig.topP, 0.8); assert.deepEqual(result.generationConfig.thinkingConfig, { @@ -105,6 +106,19 @@ test("Claude -> Gemini maps system, thinking, tool use, tool result and tools", }); }); +test("Claude -> Gemini clamps maxOutputTokens to the model cap", () => { + const result = claudeToGeminiRequest( + "gemini-2.5-flash", + { + messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + max_tokens: 999999, + }, + false + ); + + assert.equal(result.generationConfig.maxOutputTokens, 8192); +}); + test("Claude -> Gemini converts text and base64 images to Gemini parts", () => { const result = claudeToGeminiRequest( "gemini-2.5-flash", diff --git a/tests/unit/translator-openai-to-gemini.test.ts b/tests/unit/translator-openai-to-gemini.test.ts index f9c3eababbc..3ef2054d7a7 100644 --- a/tests/unit/translator-openai-to-gemini.test.ts +++ b/tests/unit/translator-openai-to-gemini.test.ts @@ -224,7 +224,7 @@ test("OpenAI -> Gemini request maps messages, merged system instructions, tools false ); - assert.equal((result as any).systemInstruction.role, "user"); + assert.equal((result as any).systemInstruction.role, "system"); assert.deepEqual((result as any).systemInstruction.parts, [ { text: "Rule A" }, { text: "Rule B" }, @@ -558,11 +558,15 @@ test("OpenAI -> Antigravity uses the Claude bridge for Claude-family models", () assert.equal(result.project, "proj-claude"); assert.equal(result.userAgent, "antigravity"); + assert.equal((result as any).request?.systemInstruction.role, "system"); assert.equal( (result as any).request?.systemInstruction.parts[0].text, ANTIGRAVITY_DEFAULT_SYSTEM ); assert.equal((result as any).request?.systemInstruction.parts[1].text, "Project rules"); + assert.equal((result as any).request?.generationConfig.maxOutputTokens, 8192); + assert.equal((result as any).request?.generationConfig.temperature, 1); + assert.equal((result as any).request?.generationConfig.thinkingConfig, undefined); const modelTurn = result.request.contents.find( (content) => content.role === "model" && content.parts.some((part) => part.functionCall) @@ -624,6 +628,7 @@ test("OpenAI -> Antigravity Claude bridge sanitizes long names and preserves res const sanitizedToolName = (result as any).request?.tools[0].functionDeclarations[0].name; assert.equal(sanitizedToolName.length, 64); + assert.match(sanitizedToolName, /^[a-zA-Z0-9_]+$/); assert.equal((result as any)._toolNameMap.get(sanitizedToolName), longToolName); const modelTurn = result.request.contents.find( @@ -638,3 +643,22 @@ test("OpenAI -> Antigravity Claude bridge sanitizes long names and preserves res assert.ok(toolTurn, "expected a tool response turn"); assert.equal(getFunctionResponse(toolTurn.parts[0]).name, sanitizedToolName); }); + +test("OpenAI -> Antigravity Claude bridge clamps output tokens and keeps thinking budget separate", () => { + const result = openaiToAntigravityRequest( + "claude-3-7-sonnet", + { + messages: [{ role: "user", content: "Summarize this" }], + max_completion_tokens: 32000, + reasoning_effort: "high", + }, + false, + { projectId: "proj-claude-thinking" } as any + ); + + assert.equal((result as any).request?.generationConfig.maxOutputTokens, 8192); + assert.deepEqual((result as any).request?.generationConfig.thinkingConfig, { + thinkingBudget: 131072, + includeThoughts: true, + }); +});