fix(gemini-cli): align Cloud Code transport (#1869)

Integrated into release/v3.7.9
This commit is contained in:
Raxxoor
2026-05-02 10:01:07 +01:00
committed by GitHub
parent 476ef48fa5
commit 0cf33fdaa3
12 changed files with 309 additions and 54 deletions

View File

@@ -120,6 +120,8 @@ export class GeminiCLIExecutor extends BaseExecutor {
}
buildUrl(model, stream, urlIndex = 0) {
void model;
void urlIndex;
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
return `${this.config.baseUrl}:${action}`;
}

View File

@@ -281,8 +281,12 @@ export function translateNonStreamingResponse(
const fn = toRecord(partObj.functionCall);
const rawName = toString(fn.name);
const restoredName = toolNameMap?.get(rawName) ?? rawName;
const nativeId = toString(fn.id);
toolCalls.push({
id: `call_${toString(restoredName, "unknown")}_${Date.now()}_${toolCalls.length}`,
id:
nativeId.length > 0
? nativeId
: `call_${toString(restoredName, "unknown")}_${Date.now()}_${toolCalls.length}`,
type: "function",
function: {
name: restoredName,

View File

@@ -7,6 +7,16 @@ import {
export const GEMINI_CLI_VERSION = "0.40.1";
export const GEMINI_CLI_GOOGLE_API_NODE_CLIENT_VERSION = "9.15.1";
const GEMINI_CLI_LOAD_CODE_ASSIST_METADATA = Object.freeze({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
});
export function getGeminiCliLoadCodeAssistMetadata(): Record<string, string> {
return { ...GEMINI_CLI_LOAD_CODE_ASSIST_METADATA };
}
export function geminiCliUserAgent(model: string): string {
const normalizedModel = model || "unknown";
return `GeminiCLI/${GEMINI_CLI_VERSION}/${normalizedModel} (${normalizeCloudCodePlatform()}; ${normalizeCloudCodeArch()}; terminal) google-api-nodejs-client/${GEMINI_CLI_GOOGLE_API_NODE_CLIENT_VERSION}`;

View File

@@ -21,7 +21,6 @@ import {
convertOpenAIContentToParts,
extractTextContent,
tryParseJSON,
generateRequestId,
generateSessionId,
cleanJSONSchemaForAntigravity,
} from "../helpers/geminiHelper.ts";
@@ -97,6 +96,7 @@ type CloudCodeEnvelope = {
type GeminiToolNameOptions = {
stripNamespace?: boolean;
functionResponseShape?: "result" | "output";
};
function buildChangedToolNameMap(toolNameMap: Map<string, string>): Map<string, string> | null {
@@ -296,7 +296,10 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
functionResponse: {
id: fid,
name: name,
response: { result: parsedResp },
response:
toolNameOptions.functionResponseShape === "output"
? { output: typeof resp === "string" ? resp : JSON.stringify(resp) }
: { result: parsedResp },
},
});
}
@@ -350,8 +353,16 @@ export function openaiToGeminiRequest(model, body, stream) {
}
// OpenAI -> Gemini CLI (Cloud Code Assist)
export function openaiToGeminiCLIRequest(model, body, stream) {
const gemini = openaiToGeminiBase(model, body, stream, { stripNamespace: true });
export function openaiToGeminiCLIRequest(
model,
body,
stream,
options: { functionResponseShape?: "result" | "output" } = {}
) {
const gemini = openaiToGeminiBase(model, body, stream, {
stripNamespace: true,
functionResponseShape: options.functionResponseShape,
});
// Add thinking config for CLI
if (body.reasoning_effort) {
@@ -413,7 +424,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
: {
model: cleanModel,
project: projectId,
user_prompt_id: generateRequestId(),
user_prompt_id: generateUUID(),
request: {
contents: geminiCLI.contents,
systemInstruction: geminiCLI.systemInstruction,
@@ -443,7 +454,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
}
} else {
// Gemini CLI's native Cloud Code envelope uses snake_case identifiers.
envelope.request.session_id = generateSessionId();
envelope.request.session_id = envelope.user_prompt_id;
envelope.request.safetySettings = geminiCLI.safetySettings;
}
@@ -619,7 +630,11 @@ register(
FORMATS.OPENAI,
FORMATS.GEMINI_CLI,
(model, body, stream, credentials) =>
wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials),
wrapInCloudCodeEnvelope(
model,
openaiToGeminiCLIRequest(model, body, stream, { functionResponseShape: "output" }),
credentials
),
null
);
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null);

View File

@@ -2,6 +2,12 @@ import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
import { storeGeminiThoughtSignature } from "../../services/geminiThoughtSignatureStore.ts";
function buildToolCallId(functionCall, toolName, toolCallIndex) {
return typeof functionCall?.id === "string" && functionCall.id.length > 0
? functionCall.id
: `${toolName}-${Date.now()}-${toolCallIndex}`;
}
// Convert Gemini response chunk to OpenAI format
export function geminiToOpenAIResponse(chunk, state) {
if (!chunk) return null;
@@ -111,7 +117,7 @@ export function geminiToOpenAIResponse(chunk, state) {
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
id: buildToolCallId(part.functionCall, fcName, toolCallIndex),
index: toolCallIndex,
type: "function",
function: {
@@ -169,7 +175,7 @@ export function geminiToOpenAIResponse(chunk, state) {
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
id: buildToolCallId(part.functionCall, fcName, toolCallIndex),
index: toolCallIndex,
type: "function",
function: {

View File

@@ -1,4 +1,5 @@
import { GEMINI_CONFIG } from "../constants/oauth";
import { getGeminiCliLoadCodeAssistMetadata } from "@omniroute/open-sse/services/geminiCliHeaders.ts";
export const gemini = {
config: GEMINI_CONFIG,
@@ -72,11 +73,7 @@ export const gemini = {
"Content-Type": "application/json",
},
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
metadata: getGeminiCliLoadCodeAssistMetadata(),
}),
}
);

View File

@@ -1,9 +1,6 @@
import crypto from "crypto";
import open from "open";
import {
ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT,
ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT,
} from "@omniroute/open-sse/services/antigravityHeaders.ts";
import { getGeminiCliLoadCodeAssistMetadata } from "@omniroute/open-sse/services/geminiCliHeaders.ts";
import { GEMINI_CONFIG } from "../constants/oauth";
import { getServerCredentials } from "../config/index";
import { startLocalServer } from "../utils/server";
@@ -73,20 +70,9 @@ export class GeminiCLIService {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT,
"X-Goog-Api-Client": ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT,
"Client-Metadata": JSON.stringify({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
}),
},
body: JSON.stringify({
metadata: {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
},
metadata: getGeminiCliLoadCodeAssistMetadata(),
}),
});

View File

@@ -19,9 +19,8 @@ export const FREE_PROVIDERS = {
name: "Gemini CLI",
icon: "terminal",
color: "#4285F4",
deprecated: true,
deprecationReason:
"Google restricts third-party OAuth usage for Gemini CLI (Mar 2026). Pro models require paid plans. Use 'gemini' (API key) provider instead.",
authHint:
"Uses Gemini CLI OAuth / Cloud Code credentials. Pro models require an eligible Google account or paid plan.",
},
kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35" },
"amazon-q": {

View File

@@ -22,6 +22,7 @@ const { skillExecutor } = await import("../../src/lib/skills/executor.ts");
const { handleChat } = await import("../../src/sse/handlers/chat.ts");
const { initTranslators } = await import("../../open-sse/translator/index.ts");
const { clearInflight } = await import("../../open-sse/services/requestDedup.ts");
const { setCliCompatProviders } = await import("../../open-sse/config/cliFingerprints.ts");
const { BaseExecutor } = await import("../../open-sse/executors/base.ts");
const { getCircuitBreaker, resetAllCircuitBreakers } =
await import("../../src/shared/utils/circuitBreaker.ts");
@@ -31,9 +32,40 @@ const { setCliCompatProviders } = await import("../../open-sse/config/cliFingerp
const originalFetch = globalThis.fetch;
const originalRetryDelayMs = BaseExecutor.RETRY_CONFIG.delayMs;
function toPlainHeaders(headers: Headers | Record<string, unknown> | undefined | null) {
type SeedConnectionOverrides = {
name?: string;
authType?: string;
apiKey?: string;
accessToken?: string;
refreshToken?: string;
tokenType?: string;
expiresAt?: string;
tokenExpiresAt?: string;
isActive?: boolean;
testStatus?: string;
priority?: number;
rateLimitedUntil?: string | number | null;
providerSpecificData?: Record<string, unknown>;
};
type FetchCall = {
url: string;
method?: string;
headers: Record<string, string>;
body: Record<string, any> | null;
};
type SeedApiKeyOptions = {
name?: string;
noLog?: boolean;
allowedConnections?: string[];
allowedModels?: string[];
};
function toPlainHeaders(headers: HeadersInit | undefined | null) {
if (!headers) return {};
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
if (Array.isArray(headers)) return Object.fromEntries(headers);
return Object.fromEntries(
Object.entries(headers).map(([key, value]) => [key, value == null ? "" : String(value)])
);
@@ -344,7 +376,7 @@ async function resetStorage() {
initTranslators();
}
async function seedConnection(provider, overrides = {}) {
async function seedConnection(provider, overrides: SeedConnectionOverrides = {}) {
return providersDb.createProviderConnection({
provider,
authType: overrides.authType || "apikey",
@@ -369,9 +401,9 @@ async function seedApiKey({
noLog = false,
allowedConnections,
allowedModels,
} = {}) {
}: SeedApiKeyOptions = {}) {
const key = await apiKeysDb.createApiKey(name, "machine-test");
const updates = {};
const updates: Record<string, unknown> = {};
if (noLog) updates.noLog = true;
if (allowedConnections) updates.allowedConnections = allowedConnections;
if (allowedModels) updates.allowedModels = allowedModels;
@@ -494,9 +526,9 @@ test.after(async () => {
test("chat pipeline handles OpenAI passthrough with valid API key auth", async () => {
await seedConnection("openai", { apiKey: "sk-openai-primary" });
const apiKey = await seedApiKey();
const fetchCalls = [];
const fetchCalls: FetchCall[] = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
method: init.method || "GET",
@@ -530,7 +562,7 @@ test("chat pipeline persists Codex responses cache and reasoning tokens to call
await seedConnection("codex", { apiKey: "sk-codex-primary" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -657,7 +689,7 @@ test("chat pipeline treats Codex /responses/compact as non-streaming JSON", asyn
await seedConnection("codex", { apiKey: "sk-codex-compact" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -699,7 +731,7 @@ test("chat pipeline serves repeated /v1/responses requests as MISS then HIT and
await seedConnection("codex", { apiKey: "sk-codex-cache-seq" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -789,7 +821,7 @@ test("chat pipeline translates OpenAI requests to Claude and returns OpenAI-shap
await seedConnection("claude", { apiKey: "sk-claude-primary" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -823,7 +855,7 @@ test("chat pipeline translates OpenAI requests to Gemini and returns OpenAI-shap
await seedConnection("gemini", { apiKey: "sk-gemini-primary" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -853,11 +885,84 @@ test("chat pipeline translates OpenAI requests to Gemini and returns OpenAI-shap
assert.equal(json.choices[0].message.content, "Gemini translated reply");
});
test("chat pipeline sends Gemini CLI OAuth requests with native Cloud Code transport", async () => {
setCliCompatProviders(["gemini-cli"]);
await seedConnection("gemini-cli", {
authType: "oauth",
apiKey: "unused-for-oauth",
accessToken: "gemini-cli-oauth-token",
providerSpecificData: { projectId: "stored-project" },
});
const fetchCalls = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
body: init.body ? JSON.parse(String(init.body)) : null,
});
if (String(url).endsWith("loadCodeAssist")) {
return new Response(JSON.stringify({ cloudaicompanionProject: "fresh-project" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return buildGeminiResponse("Gemini CLI translated reply", "gemini-3-flash-preview");
};
const response = await handleChat(
buildRequest({
body: {
model: "gemini-cli/gemini-3-flash-preview",
stream: false,
messages: [{ role: "user", content: "Hello Gemini CLI" }],
},
})
);
const json = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(fetchCalls.length, 2);
const loadCodeAssistCall = fetchCalls[0];
assert.match(loadCodeAssistCall.url, /loadCodeAssist$/);
assert.equal(loadCodeAssistCall.headers.Authorization, "Bearer gemini-cli-oauth-token");
assert.equal(loadCodeAssistCall.body.metadata.ideType, "IDE_UNSPECIFIED");
const generateCall = fetchCalls[1];
assert.match(generateCall.url, /generateContent$/);
assert.equal(generateCall.headers.Authorization, "Bearer gemini-cli-oauth-token");
assert.equal(generateCall.headers.Accept, "application/json");
assert.match(
generateCall.headers["User-Agent"],
/^GeminiCLI\/0\.40\.1\/gemini-3-flash-preview .* google-api-nodejs-client\/9\.15\.1$/
);
assert.match(generateCall.headers["X-Goog-Api-Client"], /^gl-node\/\d+\.\d+\.\d+$/);
assert.equal(generateCall.body.project, "fresh-project");
assert.equal(generateCall.body.model, "gemini-3-flash-preview");
assert.equal(generateCall.body.userAgent, undefined);
assert.equal(generateCall.body.requestId, undefined);
assert.equal(generateCall.body.user_prompt_id, generateCall.body.request.session_id);
assert.deepEqual(Object.keys(generateCall.body).slice(0, 4), [
"model",
"project",
"user_prompt_id",
"request",
]);
assert.equal(generateCall.body.request.sessionId, undefined);
assert.match(generateCall.body.request.session_id, /^[0-9a-f-]{36}$/i);
assert.equal(generateCall.body.request.contents.at(-1).parts[0].text, "Hello Gemini CLI");
assert.equal(json.object, "chat.completion");
assert.equal(json.choices[0].message.content, "Gemini CLI translated reply");
});
test("chat pipeline translates Claude-format requests into OpenAI upstream and back to Claude", async () => {
await seedConnection("openai", { apiKey: "sk-openai-claude-route" });
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -1010,7 +1115,7 @@ test("chat pipeline supports local mode without Authorization on explicit combos
});
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
headers: toPlainHeaders(init.headers),
@@ -1220,7 +1325,7 @@ test("chat pipeline injects memory context before sending the upstream request",
insertLegacyMemory(apiKey.id, "User prefers concise answers.");
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
body: init.body ? JSON.parse(String(init.body)) : null,
@@ -1281,7 +1386,7 @@ test("chat pipeline injects skills into tools and intercepts tool calls with ski
});
const fetchCalls = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
body: init.body ? JSON.parse(String(init.body)) : null,
@@ -1325,7 +1430,7 @@ test("chat pipeline falls back to the next account after a provider failure", as
});
const seenAuthHeaders = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
const headers = toPlainHeaders(init.headers);
seenAuthHeaders.push(headers.Authorization);
if (seenAuthHeaders.length === 1) {
@@ -1370,7 +1475,7 @@ test("chat pipeline falls back across combo models when the first provider fails
});
const attempts = [];
globalThis.fetch = async (url, init = {}) => {
globalThis.fetch = async (url, init: RequestInit = {}) => {
const call = {
url: String(url),
headers: toPlainHeaders(init.headers),

View File

@@ -5,7 +5,14 @@ import { GeminiCLIExecutor } from "../../open-sse/executors/gemini-cli.ts";
import { setCliCompatProviders } from "../../open-sse/config/cliFingerprints.ts";
import { GEMINI_CLI_VERSION } from "../../open-sse/services/geminiCliHeaders.ts";
type CapturedFetchCall = { url: string; body: Record<string, unknown> };
type CapturedFetchCall = {
url: string;
body: Record<string, unknown>;
};
function parseInitBody(init: RequestInit): Record<string, unknown> {
return init.body ? JSON.parse(String(init.body)) : {};
}
function getMetadata(body: Record<string, unknown>): Record<string, unknown> {
return body.metadata && typeof body.metadata === "object"
@@ -54,6 +61,27 @@ test("GeminiCLIExecutor.buildHeaders uses JSON accept for non-streaming requests
assert.equal(headers.Accept, "application/json");
});
test("GeminiCLIExecutor.buildHeaders derives the User-Agent from the request model", () => {
const executor = new GeminiCLIExecutor();
const flashHeaders = executor.buildHeaders(
{ accessToken: "gcli-token" },
true,
undefined,
"models/gemini-3-flash-preview"
);
const proHeaders = executor.buildHeaders(
{ accessToken: "gcli-token" },
true,
undefined,
"models/gemini-3.1-pro-preview"
);
assert.match(flashHeaders["User-Agent"], /\/gemini-3-flash-preview /);
assert.match(proHeaders["User-Agent"], /\/gemini-3\.1-pro-preview /);
assert.notEqual(flashHeaders["User-Agent"], proHeaders["User-Agent"]);
});
test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transformRequest updates body.project", async () => {
const executor = new GeminiCLIExecutor();
const originalFetch = globalThis.fetch;
@@ -177,7 +205,7 @@ test("GeminiCLIExecutor.refreshProject onboards a managed project when loadCodeA
const calls: CapturedFetchCall[] = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
const body = init.body ? JSON.parse(String(init.body)) : {};
const body = parseInitBody(init);
calls.push({ url: String(url), body });
if (String(url).endsWith("loadCodeAssist")) {

View File

@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
const { openaiToAntigravityRequest, openaiToGeminiCLIRequest, openaiToGeminiRequest } =
await import("../../open-sse/translator/request/openai-to-gemini.ts");
const { getRequestTranslator } = await import("../../open-sse/translator/registry.ts");
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
const {
DEFAULT_SAFETY_SETTINGS,
cleanJSONSchemaForAntigravity,
@@ -509,6 +511,69 @@ test("OpenAI -> Gemini helper IDs and JSON parsing stay in the expected format",
assert.equal(tryParseJSON("not-json"), null as any);
});
test("OpenAI -> Gemini CLI wraps requests like native Cloud Code", () => {
const translate = getRequestTranslator(FORMATS.OPENAI, FORMATS.GEMINI_CLI);
assert.ok(translate, "expected Gemini CLI translator registration");
const envelope = translate(
"gemini-3-flash-preview",
{
messages: [{ role: "user", content: "Hello" }],
reasoning_effort: "high",
},
true,
{ projectId: "project-1" }
) as any;
assert.equal(envelope.model, "gemini-3-flash-preview");
assert.equal(envelope.userAgent, undefined);
assert.equal(envelope.requestId, undefined);
assert.equal(envelope.request.sessionId, undefined);
assert.match(envelope.request.session_id, /^[0-9a-f-]{36}$/i);
assert.equal(envelope.user_prompt_id, envelope.request.session_id);
});
test("OpenAI -> Gemini CLI emits native Cloud Code functionResponse output", () => {
const translate = getRequestTranslator(FORMATS.OPENAI, FORMATS.GEMINI_CLI);
assert.ok(translate, "expected Gemini CLI translator registration");
const envelope = translate(
"gemini-3-flash-preview",
{
messages: [
{ role: "user", content: "Read fixture" },
{
role: "assistant",
tool_calls: [
{
id: "read_file_123_0",
type: "function",
function: { name: "read_file", arguments: '{"file_path":"fixture.txt"}' },
},
],
},
{
role: "tool",
tool_call_id: "read_file_123_0",
content: "The answer is capybara-4729.",
},
],
},
true,
{ projectId: "project-1" }
) as any;
const toolTurn = envelope.request.contents.find(
(content) => content.role === "user" && content.parts.some((part) => part.functionResponse)
);
assert.ok(toolTurn, "expected Gemini CLI tool response turn");
assert.deepEqual(getFunctionResponse(toolTurn.parts[0]), {
id: "read_file_123_0",
name: "read_file",
response: { output: "The answer is capybara-4729." },
});
});
test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", () => {
const result = openaiToAntigravityRequest(
"gemini-2.5-pro",
@@ -678,6 +743,7 @@ 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);
assert.deepEqual(getFunctionResponse(toolTurn.parts[0]).response, { result: { ok: true } });
});
test("OpenAI -> Antigravity Claude bridge applies Antigravity output cap without forwarding thinking", () => {

View File

@@ -64,7 +64,7 @@ test("Gemini non-stream: multiple candidates keep multimodal content, reasoning
{ thought: true, text: "Plan first." },
{ text: "Answer:" },
{ inlineData: { mimeType: "image/png", data: "abc123" } },
{ functionCall: { name: "read_file", args: { path: "/tmp/a" } } },
{ functionCall: { id: "native-read-1", name: "read_file", args: { path: "/tmp/a" } } },
],
},
finishReason: "STOP",
@@ -101,6 +101,7 @@ test("Gemini non-stream: multiple candidates keep multimodal content, reasoning
((result as any).choices[0].message as any).tool_calls[0].function.arguments,
JSON.stringify({ path: "/tmp/a" })
);
assert.equal((result as any).choices[0].message.tool_calls[0].id, "native-read-1");
assert.equal(((result as any).choices[1].message as any).content, "Second option");
(assert as any).equal((result as any).choices[1].finish_reason, "length");
assert.equal((result as any).usage.prompt_tokens, 4);
@@ -259,6 +260,7 @@ test("Gemini stream: reasoning, tool call, image and MAX_TOKENS finish are conve
{ thought: true, thoughtSignature: "sig-1", text: "Need a plan." },
{
functionCall: {
id: "native-call-1",
name: "weather_lookup_bundle_ab12cd34",
args: { city: "Sao Paulo" },
},
@@ -281,6 +283,10 @@ test("Gemini stream: reasoning, tool call, image and MAX_TOKENS finish are conve
);
assert.equal(result[1].choices[0].delta.reasoning_content, "Need a plan.");
assert.equal(
result[2].choices[0].delta.tool_calls[0].id,
"native-call-1"
);
assert.equal(
result[2].choices[0].delta.tool_calls[0].function.name,
"mcp__filesystem__read_multiple_files_with_validation_and_metadata_bundle_v2"
@@ -297,6 +303,37 @@ test("Gemini stream: reasoning, tool call, image and MAX_TOKENS finish are conve
assert.equal(result[4].usage.completion_tokens_details.reasoning_tokens, 2);
});
test("Gemini stream: tool calls without native IDs keep deterministic fallback shape", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(
{
responseId: "resp-tool-no-id",
modelVersion: "gemini-3-flash-preview",
candidates: [
{
content: {
parts: [
{
thoughtSignature: "sig-2",
functionCall: {
name: "read_file",
args: { file_path: "fixture.txt" },
},
},
],
},
},
],
},
state
);
const toolCall = result[1].choices[0].delta.tool_calls[0];
assert.match(toolCall.id, /^read_file-\d+-0$/);
assert.equal(toolCall.function.name, "read_file");
assert.equal(toolCall.function.arguments, JSON.stringify({ file_path: "fixture.txt" }));
});
test("Gemini stream: safety block without candidates emits role chunk then content_filter finish", () => {
const state = createStreamingState();
const result = geminiToOpenAIResponse(