mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(cli): add Gemini CLI fingerprint (#1866)
Integrated into release/v3.7.8
This commit is contained in:
@@ -186,6 +186,19 @@ export const CLI_FINGERPRINTS: Record<string, CliFingerprint> = {
|
||||
],
|
||||
userAgent: getAntigravityUserAgent,
|
||||
},
|
||||
"gemini-cli": {
|
||||
headerOrder: [
|
||||
"Host",
|
||||
"Content-Type",
|
||||
"User-Agent",
|
||||
"X-Goog-Api-Client",
|
||||
"Accept",
|
||||
"Accept-Encoding",
|
||||
"Connection",
|
||||
"Authorization",
|
||||
],
|
||||
bodyFieldOrder: ["model", "project", "user_prompt_id", "request"],
|
||||
},
|
||||
qwen: {
|
||||
headerOrder: [
|
||||
"Host",
|
||||
|
||||
@@ -60,6 +60,15 @@ type AntigravityCollectedStream = {
|
||||
remainingCredits: Array<{ creditType: string; creditAmount: string }> | null;
|
||||
};
|
||||
|
||||
type AntigravityRequestEnvelope = Record<string, unknown> & {
|
||||
project: string;
|
||||
model: string;
|
||||
userAgent: "antigravity";
|
||||
requestType: "agent";
|
||||
requestId: string;
|
||||
request: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-account GOOGLE_ONE_AI credits-exhausted tracker.
|
||||
* Key: accountId (OAuth subject / email). Value: expiry timestamp.
|
||||
@@ -249,7 +258,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
return scrubProxyAndFingerprintHeaders(raw);
|
||||
}
|
||||
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
transformRequest(model, body, stream, credentials): AntigravityRequestEnvelope | Response {
|
||||
// TODO: Consider removing project override like gemini-cli.ts — stored projectId
|
||||
// can become stale for Cloud Code accounts, causing 403 "has not been used in project X".
|
||||
// Antigravity accounts may have more stable project IDs, but the risk exists.
|
||||
@@ -352,14 +361,24 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
project: _project,
|
||||
model: _model,
|
||||
userAgent: _userAgent,
|
||||
requestType: _requestType,
|
||||
requestId: _requestId,
|
||||
request: _request,
|
||||
...passthroughFields
|
||||
} = normalizedBody;
|
||||
|
||||
return {
|
||||
...normalizedBody,
|
||||
project: projectId,
|
||||
model: upstreamModel,
|
||||
userAgent: "antigravity",
|
||||
requestType: "agent",
|
||||
requestId: `agent-${crypto.randomUUID()}`,
|
||||
request: transformedRequest,
|
||||
...passthroughFields,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -572,15 +591,17 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
const url = this.buildUrl(model, upstreamStream, urlIndex);
|
||||
const headers = this.buildHeaders(credentials, upstreamStream);
|
||||
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
||||
let transformedBody = await this.transformRequest(model, body, upstreamStream, credentials);
|
||||
const transformed = await this.transformRequest(model, body, upstreamStream, credentials);
|
||||
let requestToolNameMap: Map<string, string> | null = null;
|
||||
|
||||
if (transformedBody instanceof Response) {
|
||||
return { response: transformedBody, url, headers, transformedBody: body };
|
||||
if (transformed instanceof Response) {
|
||||
return { response: transformed, url, headers, transformedBody: body };
|
||||
}
|
||||
|
||||
let transformedBody: Record<string, unknown> = transformed;
|
||||
|
||||
if (transformedBody && typeof transformedBody === "object") {
|
||||
const cloaked = cloakAntigravityToolPayload(transformedBody as Record<string, unknown>);
|
||||
const cloaked = cloakAntigravityToolPayload(transformedBody);
|
||||
transformedBody = cloaked.body;
|
||||
requestToolNameMap = cloaked.toolNameMap;
|
||||
}
|
||||
|
||||
@@ -220,9 +220,11 @@ export class BaseExecutor {
|
||||
buildHeaders(
|
||||
credentials: ProviderCredentials,
|
||||
stream = true,
|
||||
clientHeaders?: Record<string, string> | null
|
||||
clientHeaders?: Record<string, string> | null,
|
||||
model?: string
|
||||
): Record<string, string> {
|
||||
void clientHeaders;
|
||||
void model;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...this.config.headers,
|
||||
@@ -442,7 +444,7 @@ export class BaseExecutor {
|
||||
|
||||
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
|
||||
const url = this.buildUrl(model, stream, urlIndex, activeCredentials);
|
||||
const headers = this.buildHeaders(activeCredentials, stream, clientHeaders);
|
||||
const headers = this.buildHeaders(activeCredentials, stream, clientHeaders, model);
|
||||
applyConfiguredUserAgent(headers, activeCredentials?.providerSpecificData);
|
||||
|
||||
const ccRequestDefaults = isClaudeCodeCompatible(this.provider)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseExecutor } from "./base.ts";
|
||||
import { randomUUID } from "crypto";
|
||||
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
|
||||
import { geminiCLIUserAgent, googApiClientHeader } from "../services/antigravityHeaders.ts";
|
||||
import { getGeminiCliHeaders } from "../services/geminiCliHeaders.ts";
|
||||
import { scrubProxyAndFingerprintHeaders } from "../services/antigravityHeaderScrub.ts";
|
||||
import { obfuscateSensitiveWords } from "../services/antigravityObfuscation.ts";
|
||||
import {
|
||||
@@ -19,15 +20,13 @@ const ONBOARD_DELAY_MS = 5_000;
|
||||
const DEFAULT_PROJECT_ID = "default-project";
|
||||
const DEFAULT_ONBOARD_TIER = "free-tier";
|
||||
const LOAD_CODE_ASSIST_METADATA = Object.freeze({
|
||||
ideType: "ANTIGRAVITY",
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
duetProject: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
const ONBOARD_METADATA = Object.freeze({
|
||||
ideType: "ANTIGRAVITY",
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
duetProject: DEFAULT_PROJECT_ID,
|
||||
});
|
||||
|
||||
// Per-account cache: accessToken -> { projectId, expiresAt }
|
||||
@@ -51,6 +50,22 @@ function normalizeGeminiModel(model: string): string {
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
function generateGeminiCliRequestId(): string {
|
||||
return `agent-${randomUUID()}`;
|
||||
}
|
||||
|
||||
function generateGeminiCliSessionId(): string {
|
||||
return `-${Date.now()}`;
|
||||
}
|
||||
|
||||
function cloneGeminiCliRecord(value: Record<string, any>): Record<string, any> {
|
||||
if (typeof structuredClone === "function") {
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function extractProjectId(payload: unknown): string {
|
||||
if (!payload || typeof payload !== "object") return "";
|
||||
const data = payload as LoadCodeAssistResponse;
|
||||
@@ -105,31 +120,27 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
buildUrl(model, stream, urlIndex = 0) {
|
||||
this._currentModel = normalizeGeminiModel(model);
|
||||
const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
||||
return `${this.config.baseUrl}:${action}`;
|
||||
}
|
||||
|
||||
buildHeaders(credentials, stream = true) {
|
||||
const raw = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${credentials.accessToken}`,
|
||||
// Dynamic headers matching native GeminiCLI client
|
||||
"User-Agent": geminiCLIUserAgent(this._currentModel || "unknown"),
|
||||
"X-Goog-Api-Client": googApiClientHeader(),
|
||||
...(stream && { Accept: "text/event-stream" }),
|
||||
};
|
||||
buildHeaders(credentials, stream = true, clientHeaders?: Record<string, string> | null, model?: string) {
|
||||
void clientHeaders;
|
||||
const raw = getGeminiCliHeaders(
|
||||
normalizeGeminiModel(model || "unknown"),
|
||||
credentials.accessToken,
|
||||
stream ? "*/*" : "application/json"
|
||||
);
|
||||
return scrubProxyAndFingerprintHeaders(raw);
|
||||
}
|
||||
|
||||
// Track current model for dynamic UA. BaseExecutor calls buildUrl before buildHeaders.
|
||||
private _currentModel = "unknown";
|
||||
|
||||
async onboardManagedProject(
|
||||
accessToken: string,
|
||||
tierId = DEFAULT_ONBOARD_TIER,
|
||||
options: OnboardOptions = {}
|
||||
options: OnboardOptions = {},
|
||||
model = "unknown"
|
||||
): Promise<string | null> {
|
||||
const currentModel = normalizeGeminiModel(model);
|
||||
const attempts =
|
||||
Number.isInteger(options.attempts) && options.attempts! > 0
|
||||
? Number(options.attempts)
|
||||
@@ -155,11 +166,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
try {
|
||||
response = await fetch(ONBOARD_USER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": "GeminiCLI/1.0.0",
|
||||
},
|
||||
headers: getGeminiCliHeaders(currentModel, accessToken, "application/json"),
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -201,7 +208,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
* Native Gemini CLI refreshes this every 30 seconds — OmniRoute stores it once
|
||||
* at OAuth connection time, so it goes stale. This method keeps it fresh.
|
||||
*/
|
||||
async refreshProject(accessToken: string): Promise<string | null> {
|
||||
async refreshProject(accessToken: string, model = "unknown"): Promise<string | null> {
|
||||
// Check cache
|
||||
const cached = projectCache.get(accessToken);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
@@ -212,7 +219,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
const inflight = inflightRefresh.get(accessToken);
|
||||
if (inflight) return inflight;
|
||||
|
||||
const promise = this._doRefresh(accessToken);
|
||||
const promise = this._doRefresh(accessToken, model);
|
||||
inflightRefresh.set(accessToken, promise);
|
||||
try {
|
||||
return await promise;
|
||||
@@ -221,7 +228,8 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
async _doRefresh(accessToken: string): Promise<string | null> {
|
||||
async _doRefresh(accessToken: string, model = "unknown"): Promise<string | null> {
|
||||
const currentModel = normalizeGeminiModel(model);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), LOAD_CODE_ASSIST_TIMEOUT_MS);
|
||||
@@ -230,12 +238,8 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
try {
|
||||
response = await fetch(LOAD_CODE_ASSIST_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers: getGeminiCliHeaders(currentModel, accessToken, "application/json"),
|
||||
body: JSON.stringify({
|
||||
cloudaicompanionProject: DEFAULT_PROJECT_ID,
|
||||
metadata: { ...LOAD_CODE_ASSIST_METADATA },
|
||||
}),
|
||||
signal: controller.signal,
|
||||
@@ -258,7 +262,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
console.warn(
|
||||
"[OmniRoute] loadCodeAssist returned no project — attempting managed project onboarding"
|
||||
);
|
||||
projectId = await this.onboardManagedProject(accessToken, extractDefaultTierId(data));
|
||||
projectId = await this.onboardManagedProject(accessToken, extractDefaultTierId(data), {}, currentModel);
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
@@ -279,25 +283,50 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
async transformRequest(model, body, stream, credentials) {
|
||||
this._currentModel = normalizeGeminiModel(model);
|
||||
const currentModel = normalizeGeminiModel(model);
|
||||
const normalizedBody =
|
||||
shouldStripCloudCodeThinking(this.provider, this._currentModel) &&
|
||||
shouldStripCloudCodeThinking(this.provider, currentModel) &&
|
||||
body &&
|
||||
typeof body === "object"
|
||||
? stripCloudCodeThinkingConfig(body)
|
||||
: body;
|
||||
|
||||
// Refresh the project ID via loadCodeAssist (cached for 30s).
|
||||
if (normalizedBody && typeof normalizedBody === "object" && normalizedBody.request) {
|
||||
if (credentials.accessToken) {
|
||||
const freshProject = await this.refreshProject(credentials.accessToken);
|
||||
if (freshProject) {
|
||||
normalizedBody.project = freshProject;
|
||||
}
|
||||
}
|
||||
const bodyRecord =
|
||||
normalizedBody && typeof normalizedBody === "object"
|
||||
? (normalizedBody as Record<string, any>)
|
||||
: { request: {} };
|
||||
const requestRecord =
|
||||
bodyRecord.request && typeof bodyRecord.request === "object"
|
||||
? cloneGeminiCliRecord(bodyRecord.request as Record<string, any>)
|
||||
: {};
|
||||
|
||||
const envelope: Record<string, any> = {
|
||||
model: currentModel,
|
||||
project: bodyRecord.project || credentials.projectId || "",
|
||||
user_prompt_id: bodyRecord.user_prompt_id || generateGeminiCliRequestId(),
|
||||
request: {
|
||||
...requestRecord,
|
||||
session_id: requestRecord.session_id || generateGeminiCliSessionId(),
|
||||
},
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(bodyRecord)) {
|
||||
if (!(key in envelope) && key !== "request") {
|
||||
envelope[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the project ID via loadCodeAssist (cached for 30s).
|
||||
if (credentials.accessToken) {
|
||||
const freshProject = await this.refreshProject(credentials.accessToken, currentModel);
|
||||
if (freshProject) {
|
||||
envelope.project = freshProject;
|
||||
}
|
||||
}
|
||||
|
||||
if (envelope.request) {
|
||||
// Obfuscate sensitive client names in user content
|
||||
const contents = normalizedBody.request?.contents;
|
||||
const contents = envelope.request?.contents;
|
||||
if (Array.isArray(contents)) {
|
||||
for (const msg of contents) {
|
||||
if (Array.isArray(msg.parts)) {
|
||||
@@ -310,7 +339,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizedBody;
|
||||
return envelope;
|
||||
}
|
||||
|
||||
async refreshCredentials(credentials, log) {
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
} from "./antigravityVersion.ts";
|
||||
|
||||
/**
|
||||
* Antigravity and Gemini CLI header utilities.
|
||||
* Antigravity header utilities.
|
||||
*
|
||||
* Generates User-Agent strings and API client headers that match
|
||||
* the real Antigravity and Gemini CLI binaries.
|
||||
* the real Antigravity client flows.
|
||||
*
|
||||
* Based on CLIProxyAPI's misc/header_utils.go.
|
||||
*/
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
type AntigravityHeaderProfile = "loadCodeAssist" | "fetchAvailableModels" | "models";
|
||||
|
||||
const ANTIGRAVITY_VERSION = ANTIGRAVITY_FALLBACK_VERSION;
|
||||
export const GEMINI_CLI_VERSION = "0.39.1";
|
||||
export const GEMINI_SDK_VERSION = "1.30.0";
|
||||
export const NODE_VERSION = "v22.21.1";
|
||||
export const ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/10.3.0";
|
||||
export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT =
|
||||
"google-cloud-sdk vscode_cloudshelleditor/0.1";
|
||||
export const ANTIGRAVITY_CREDIT_PROBE_API_CLIENT = "google-genai-sdk/1.30.0 gl-node/v22.21.1";
|
||||
const LOAD_CODE_ASSIST_METADATA = Object.freeze({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
@@ -38,32 +36,6 @@ function withOptionalBearerAuth(
|
||||
return headers;
|
||||
}
|
||||
|
||||
function getPlatform(): string {
|
||||
const p = typeof process !== "undefined" ? process.platform : "unknown";
|
||||
switch (p) {
|
||||
case "win32":
|
||||
return "windows";
|
||||
case "darwin":
|
||||
return "macos";
|
||||
default:
|
||||
return p; // "linux", etc.
|
||||
}
|
||||
}
|
||||
|
||||
function getArch(): string {
|
||||
const a = typeof process !== "undefined" ? process.arch : "unknown";
|
||||
switch (a) {
|
||||
case "x64":
|
||||
return "x64";
|
||||
case "ia32":
|
||||
return "x86";
|
||||
case "arm64":
|
||||
return "arm64";
|
||||
default:
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity User-Agent: "antigravity/VERSION darwin/arm64"
|
||||
*
|
||||
@@ -118,20 +90,9 @@ export function getAntigravityHeaders(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini CLI User-Agent: "GeminiCLI/VERSION/MODEL (OS; ARCH)"
|
||||
* Example: "GeminiCLI/1.0.0/gemini-3-flash (macos; arm64)"
|
||||
*/
|
||||
export function geminiCLIUserAgent(model: string): string {
|
||||
return `GeminiCLI/${GEMINI_CLI_VERSION}/${model || "unknown"} (${getPlatform()}; ${getArch()})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* X-Goog-Api-Client header value matching the real Gemini SDK.
|
||||
* Example: "google-genai-sdk/1.30.0 gl-node/v22.21.1"
|
||||
*/
|
||||
export function googApiClientHeader(): string {
|
||||
return `google-genai-sdk/${GEMINI_SDK_VERSION} gl-node/${NODE_VERSION}`;
|
||||
/** X-Goog-Api-Client used by Antigravity's credit probe path. */
|
||||
export function getAntigravityCreditProbeApiClientHeader(): string {
|
||||
return ANTIGRAVITY_CREDIT_PROBE_API_CLIENT;
|
||||
}
|
||||
|
||||
export { ANTIGRAVITY_VERSION };
|
||||
|
||||
41
open-sse/services/cloudCodeHeaders.ts
Normal file
41
open-sse/services/cloudCodeHeaders.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export function getRuntimePlatform(): string {
|
||||
return typeof process !== "undefined" && typeof process.platform === "string"
|
||||
? process.platform
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
export function getRuntimeArch(): string {
|
||||
return typeof process !== "undefined" && typeof process.arch === "string"
|
||||
? process.arch
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
export function getRuntimeNodeVersion(): string {
|
||||
return typeof process !== "undefined" && process.versions?.node
|
||||
? process.versions.node
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
export function normalizeCloudCodePlatform(platform = getRuntimePlatform()): string {
|
||||
switch (platform) {
|
||||
case "win32":
|
||||
return "windows";
|
||||
case "darwin":
|
||||
return "macos";
|
||||
default:
|
||||
return platform || "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCloudCodeArch(arch = getRuntimeArch()): string {
|
||||
switch (arch) {
|
||||
case "ia32":
|
||||
return "x86";
|
||||
default:
|
||||
return arch || "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function getCloudCodeNodeApiClientHeader(nodeVersion = getRuntimeNodeVersion()): string {
|
||||
return `gl-node/${nodeVersion.replace(/^v/, "")}`;
|
||||
}
|
||||
31
open-sse/services/geminiCliHeaders.ts
Normal file
31
open-sse/services/geminiCliHeaders.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
getCloudCodeNodeApiClientHeader,
|
||||
normalizeCloudCodeArch,
|
||||
normalizeCloudCodePlatform,
|
||||
} from "./cloudCodeHeaders.ts";
|
||||
|
||||
export const GEMINI_CLI_VERSION = "0.40.1";
|
||||
export const GEMINI_CLI_GOOGLE_API_NODE_CLIENT_VERSION = "9.15.1";
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
export function geminiCliApiClientHeader(): string {
|
||||
return getCloudCodeNodeApiClientHeader();
|
||||
}
|
||||
|
||||
export function getGeminiCliHeaders(
|
||||
model: string,
|
||||
accessToken: string,
|
||||
accept: "application/json" | "*/*"
|
||||
): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": geminiCliUserAgent(model),
|
||||
"X-Goog-Api-Client": geminiCliApiClientHeader(),
|
||||
Accept: accept,
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { safePercentage } from "@/shared/utils/formatting";
|
||||
import { fetchBailianQuota, type BailianTripleWindowQuota } from "./bailianQuotaFetcher.ts";
|
||||
import {
|
||||
antigravityUserAgent,
|
||||
googApiClientHeader,
|
||||
getAntigravityCreditProbeApiClientHeader,
|
||||
getAntigravityHeaders,
|
||||
getAntigravityLoadCodeAssistMetadata,
|
||||
} from "./antigravityHeaders.ts";
|
||||
@@ -1513,7 +1513,7 @@ async function probeAntigravityCreditBalanceUncached(
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": antigravityUserAgent(),
|
||||
"X-Goog-Api-Client": googApiClientHeader(),
|
||||
"X-Goog-Api-Client": getAntigravityCreditProbeApiClientHeader(),
|
||||
Accept: "text/event-stream",
|
||||
};
|
||||
|
||||
|
||||
@@ -73,11 +73,13 @@ type GeminiRequest = {
|
||||
type CloudCodeEnvelope = {
|
||||
project: string;
|
||||
model: string;
|
||||
userAgent: string;
|
||||
requestId: string;
|
||||
user_prompt_id?: string;
|
||||
userAgent?: string;
|
||||
requestId?: string;
|
||||
requestType?: string;
|
||||
request: {
|
||||
sessionId: string;
|
||||
session_id?: string;
|
||||
sessionId?: string;
|
||||
contents: GeminiContent[];
|
||||
systemInstruction?: GeminiContent;
|
||||
generationConfig: GeminiGenerationConfig;
|
||||
@@ -393,27 +395,38 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
|
||||
const cleanModel = model.includes("/") ? model.split("/").pop()! : model;
|
||||
|
||||
const envelope: CloudCodeEnvelope = {
|
||||
project: projectId,
|
||||
model: cleanModel,
|
||||
userAgent: isAntigravity ? "antigravity" : "gemini-cli",
|
||||
requestId: isAntigravity ? `agent-${generateUUID()}` : generateRequestId(),
|
||||
request: {
|
||||
sessionId: generateSessionId(),
|
||||
contents: geminiCLI.contents,
|
||||
systemInstruction: geminiCLI.systemInstruction,
|
||||
generationConfig: geminiCLI.generationConfig,
|
||||
tools: geminiCLI.tools,
|
||||
},
|
||||
};
|
||||
const envelope: CloudCodeEnvelope = isAntigravity
|
||||
? {
|
||||
project: projectId,
|
||||
model: cleanModel,
|
||||
userAgent: "antigravity",
|
||||
requestType: "agent",
|
||||
requestId: `agent-${generateUUID()}`,
|
||||
request: {
|
||||
sessionId: generateSessionId(),
|
||||
contents: geminiCLI.contents,
|
||||
systemInstruction: geminiCLI.systemInstruction,
|
||||
generationConfig: geminiCLI.generationConfig,
|
||||
tools: geminiCLI.tools,
|
||||
},
|
||||
}
|
||||
: {
|
||||
model: cleanModel,
|
||||
project: projectId,
|
||||
user_prompt_id: generateRequestId(),
|
||||
request: {
|
||||
contents: geminiCLI.contents,
|
||||
systemInstruction: geminiCLI.systemInstruction,
|
||||
generationConfig: geminiCLI.generationConfig,
|
||||
tools: geminiCLI.tools,
|
||||
},
|
||||
};
|
||||
if (geminiCLI._toolNameMap instanceof Map && geminiCLI._toolNameMap.size > 0) {
|
||||
envelope._toolNameMap = geminiCLI._toolNameMap;
|
||||
}
|
||||
|
||||
// Antigravity specific fields
|
||||
if (isAntigravity) {
|
||||
envelope.requestType = "agent";
|
||||
|
||||
// Inject required default system prompt for Antigravity
|
||||
const defaultPart: GeminiPart = { text: ANTIGRAVITY_DEFAULT_SYSTEM };
|
||||
if (envelope.request.systemInstruction?.parts) {
|
||||
@@ -429,7 +442,8 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Keep safetySettings for Gemini CLI
|
||||
// Gemini CLI's native Cloud Code envelope uses snake_case identifiers.
|
||||
envelope.request.session_id = generateSessionId();
|
||||
envelope.request.safetySettings = geminiCLI.safetySettings;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function RoutingTab() {
|
||||
const [settings, setSettings] = useState<any>({
|
||||
alwaysPreserveClientCache: "auto",
|
||||
antigravitySignatureCacheMode: "enabled",
|
||||
cliCompatProviders: [],
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
getAntigravityHeaders,
|
||||
antigravityUserAgent,
|
||||
googApiClientHeader,
|
||||
getAntigravityCreditProbeApiClientHeader,
|
||||
} from "@omniroute/open-sse/services/antigravityHeaders.ts";
|
||||
import {
|
||||
getAntigravityFetchAvailableModelsUrls,
|
||||
@@ -202,7 +202,7 @@ async function probeAntigravityCreditBalance(
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": antigravityUserAgent(),
|
||||
"X-Goog-Api-Client": googApiClientHeader(),
|
||||
"X-Goog-Api-Client": getAntigravityCreditProbeApiClientHeader(),
|
||||
Accept: "text/event-stream",
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const IMPLEMENTED_CLI_FINGERPRINT_PROVIDER_IDS = [
|
||||
"codex",
|
||||
"github",
|
||||
"antigravity",
|
||||
"gemini-cli",
|
||||
"qwen",
|
||||
] as const;
|
||||
|
||||
@@ -15,6 +16,7 @@ export const CLI_COMPAT_DISPLAY_PROVIDER_IDS = [
|
||||
"codex",
|
||||
"copilot",
|
||||
"antigravity",
|
||||
"gemini-cli",
|
||||
"qwen",
|
||||
] as const;
|
||||
|
||||
@@ -81,6 +83,10 @@ export const CLI_COMPAT_PROVIDER_DISPLAY: Record<
|
||||
name: "Antigravity",
|
||||
description: "Google Antigravity IDE compatibility",
|
||||
},
|
||||
"gemini-cli": {
|
||||
name: "Gemini CLI",
|
||||
description: "Google Gemini CLI compatibility",
|
||||
},
|
||||
qwen: {
|
||||
name: "Qwen Code / Qoder",
|
||||
description: "Qwen Code and Qoder CLI compatibility",
|
||||
|
||||
@@ -528,23 +528,6 @@ amp --model "{{model}}"
|
||||
docsUrl: "/docs?section=cli-tools",
|
||||
configType: "custom-builder",
|
||||
},
|
||||
// HIDDEN: gemini-cli
|
||||
// "gemini-cli": {
|
||||
// id: "gemini-cli",
|
||||
// name: "Gemini CLI",
|
||||
// icon: "terminal",
|
||||
// color: "#4285F4",
|
||||
// description: "Google Gemini CLI",
|
||||
// configType: "env",
|
||||
// envVars: {
|
||||
// baseUrl: "GEMINI_API_BASE_URL",
|
||||
// model: "GEMINI_MODEL",
|
||||
// },
|
||||
// defaultModels: [
|
||||
// { id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", alias: "pro" },
|
||||
// { id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", alias: "flash" },
|
||||
// ],
|
||||
// },
|
||||
};
|
||||
|
||||
// Get all provider models for mapping dropdown
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export function normalizeCliCompatProviderId(providerId: string): string {
|
||||
return providerId.toLowerCase() === "copilot" ? "github" : providerId.toLowerCase();
|
||||
const normalized = providerId.toLowerCase();
|
||||
if (normalized === "copilot") return "github";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ test("CLI fingerprint toggles only expose implemented fingerprints and functiona
|
||||
}
|
||||
|
||||
assert.equal(CLI_COMPAT_TOGGLE_IDS.includes("copilot"), true);
|
||||
assert.equal(CLI_COMPAT_TOGGLE_IDS.includes("gemini-cli"), true);
|
||||
assert.equal((CLI_COMPAT_TOGGLE_IDS as readonly string[]).includes("github"), false);
|
||||
});
|
||||
|
||||
@@ -98,6 +99,37 @@ test("CLI fingerprint preserves Codex executor User-Agent and maps legacy Copilo
|
||||
);
|
||||
|
||||
assert.equal(copilot.headers["User-Agent"], "GitHubCopilotChat/0.45.1");
|
||||
|
||||
const geminiCli = applyFingerprint(
|
||||
"gemini-cli",
|
||||
{
|
||||
Authorization: "Bearer token",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "GeminiCLI/0.40.1/gemini-2.5-flash (linux; arm64; terminal) google-api-nodejs-client/9.15.1",
|
||||
"X-Goog-Api-Client": "gl-node/22.22.2",
|
||||
Accept: "*/*",
|
||||
},
|
||||
{
|
||||
request: {},
|
||||
user_prompt_id: "prompt-id",
|
||||
project: "project-id",
|
||||
model: "gemini-2.5-flash",
|
||||
}
|
||||
);
|
||||
|
||||
assert.deepEqual(Object.keys(JSON.parse(geminiCli.bodyString)), [
|
||||
"model",
|
||||
"project",
|
||||
"user_prompt_id",
|
||||
"request",
|
||||
]);
|
||||
assert.deepEqual(Object.keys(geminiCli.headers), [
|
||||
"Content-Type",
|
||||
"User-Agent",
|
||||
"X-Goog-Api-Client",
|
||||
"Accept",
|
||||
"Authorization",
|
||||
]);
|
||||
});
|
||||
|
||||
test("CLI fingerprint keeps legacy Copilot settings functional without exposing duplicate UI toggles", () => {
|
||||
@@ -108,6 +140,9 @@ test("CLI fingerprint keeps legacy Copilot settings functional without exposing
|
||||
setCliCompatProviders(["copilot"]);
|
||||
assert.equal(isCliCompatEnabled("github"), true);
|
||||
assert.equal(isCliCompatEnabled("copilot"), true);
|
||||
setCliCompatProviders(["gemini-cli"]);
|
||||
assert.equal(isCliCompatEnabled("gemini-cli"), true);
|
||||
assert.equal(isCliCompatEnabled("gemini"), false);
|
||||
} finally {
|
||||
setCliCompatProviders([]);
|
||||
}
|
||||
|
||||
@@ -96,6 +96,14 @@ test("AntigravityExecutor.transformRequest normalizes model, project and content
|
||||
|
||||
assert.equal(result.project, "project-1");
|
||||
assert.equal(result.model, "gemini-3.1-pro-low");
|
||||
assert.deepEqual(Object.keys(result), [
|
||||
"project",
|
||||
"model",
|
||||
"userAgent",
|
||||
"requestType",
|
||||
"requestId",
|
||||
"request",
|
||||
]);
|
||||
assert.equal(result.userAgent, "antigravity");
|
||||
assert.ok(result.request.sessionId);
|
||||
assert.deepEqual(result.request.toolConfig, {
|
||||
|
||||
@@ -2,7 +2,16 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { GeminiCLIExecutor } from "../../open-sse/executors/gemini-cli.ts";
|
||||
import { GEMINI_CLI_VERSION } from "../../open-sse/services/antigravityHeaders.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> };
|
||||
|
||||
function getMetadata(body: Record<string, unknown>): Record<string, unknown> {
|
||||
return body.metadata && typeof body.metadata === "object"
|
||||
? (body.metadata as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
test("GeminiCLIExecutor.buildUrl and buildHeaders match the native Gemini CLI fingerprint", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
@@ -16,16 +25,33 @@ test("GeminiCLIExecutor.buildUrl and buildHeaders match the native Gemini CLI fi
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
|
||||
);
|
||||
|
||||
const headers = executor.buildHeaders({ accessToken: "gcli-token" }, true);
|
||||
const headers = executor.buildHeaders(
|
||||
{ accessToken: "gcli-token" },
|
||||
true,
|
||||
undefined,
|
||||
"models/gemini-2.5-flash"
|
||||
);
|
||||
assert.equal(headers.Authorization, "Bearer gcli-token");
|
||||
assert.equal(headers.Accept, "text/event-stream");
|
||||
assert.equal(headers.Accept, "*/*");
|
||||
assert.match(
|
||||
headers["User-Agent"],
|
||||
new RegExp(
|
||||
`^GeminiCLI/${GEMINI_CLI_VERSION.replaceAll(".", "\\.")}/gemini-2\\.5-flash \\((linux|macos|windows); (x64|arm64|x86)\\)$`
|
||||
`^GeminiCLI/${GEMINI_CLI_VERSION.replaceAll(".", "\\.")}/gemini-2\\.5-flash \\((linux|macos|windows); (x64|arm64|x86); terminal\\) google-api-nodejs-client/9\\.15\\.1$`
|
||||
)
|
||||
);
|
||||
assert.match(headers["X-Goog-Api-Client"], /google-genai-sdk/);
|
||||
assert.equal(headers["X-Goog-Api-Client"], `gl-node/${process.versions.node}`);
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.buildHeaders uses JSON accept for non-streaming requests", () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const headers = executor.buildHeaders(
|
||||
{ accessToken: "gcli-token" },
|
||||
false,
|
||||
undefined,
|
||||
"models/gemini-2.5-flash"
|
||||
);
|
||||
|
||||
assert.equal(headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transformRequest updates body.project", async () => {
|
||||
@@ -95,6 +121,44 @@ test("GeminiCLIExecutor.transformRequest preserves thinking config for supported
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.transformRequest does not mutate the caller request body", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const body = {
|
||||
project: "stale-project",
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||||
generationConfig: { temperature: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(JSON.stringify({ cloudaicompanionProject: "fresh-project-id" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
try {
|
||||
const transformed = await executor.transformRequest("gemini-2.5-flash", body, true, {
|
||||
accessToken: "access-token-clone",
|
||||
});
|
||||
|
||||
assert.notEqual(transformed.request, body.request);
|
||||
assert.deepEqual(body, {
|
||||
project: "stale-project",
|
||||
request: {
|
||||
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
||||
generationConfig: { temperature: 0.2 },
|
||||
},
|
||||
});
|
||||
|
||||
transformed.request.contents[0].parts[0].text = "changed";
|
||||
assert.equal(body.request.contents[0].parts[0].text, "Hello");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.refreshProject returns null on failed loadCodeAssist responses", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
@@ -110,10 +174,10 @@ test("GeminiCLIExecutor.refreshProject returns null on failed loadCodeAssist res
|
||||
test("GeminiCLIExecutor.refreshProject onboards a managed project when loadCodeAssist has no project", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls = [];
|
||||
const calls: CapturedFetchCall[] = [];
|
||||
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const body = init.body ? JSON.parse(String(init.body)) : null;
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
const body = init.body ? JSON.parse(String(init.body)) : {};
|
||||
calls.push({ url: String(url), body });
|
||||
|
||||
if (String(url).endsWith("loadCodeAssist")) {
|
||||
@@ -148,12 +212,12 @@ test("GeminiCLIExecutor.refreshProject onboards a managed project when loadCodeA
|
||||
calls.map((call) => call.url.split(":").at(-1)),
|
||||
["loadCodeAssist", "onboardUser"]
|
||||
);
|
||||
assert.equal(calls[0].body.cloudaicompanionProject, "default-project");
|
||||
assert.equal(calls[0].body.metadata.ideType, "ANTIGRAVITY");
|
||||
assert.equal(calls[0].body.metadata.duetProject, "default-project");
|
||||
assert.equal(calls[0].body.cloudaicompanionProject, undefined);
|
||||
assert.equal(getMetadata(calls[0].body).ideType, "IDE_UNSPECIFIED");
|
||||
assert.equal(getMetadata(calls[0].body).duetProject, undefined);
|
||||
assert.equal(calls[1].body.tierId, "free-tier");
|
||||
assert.equal(calls[1].body.metadata.ideType, "ANTIGRAVITY");
|
||||
assert.equal(calls[1].body.metadata.duetProject, "default-project");
|
||||
assert.equal(getMetadata(calls[1].body).ideType, "IDE_UNSPECIFIED");
|
||||
assert.equal(getMetadata(calls[1].body).duetProject, undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
@@ -224,3 +288,68 @@ test("GeminiCLIExecutor.refreshCredentials exchanges refresh tokens via Google O
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GeminiCLIExecutor.execute applies CLI fingerprint to the final Cloud Code request", async () => {
|
||||
const executor = new GeminiCLIExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{ url: string; headers: Record<string, string>; body: string }> = [];
|
||||
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
const requestUrl = String(url);
|
||||
calls.push({
|
||||
url: requestUrl,
|
||||
headers: init.headers as Record<string, string>,
|
||||
body: init.body ? String(init.body) : "",
|
||||
});
|
||||
|
||||
if (requestUrl.endsWith("loadCodeAssist")) {
|
||||
return new Response(JSON.stringify({ cloudaicompanionProject: "project-live" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
'data: {"candidates":[{"content":{"parts":[{"text":"ok"}]}}]}\n\n',
|
||||
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
setCliCompatProviders(["gemini-cli"]);
|
||||
await executor.execute({
|
||||
model: "gemini-3.1-pro-preview",
|
||||
body: { request: { contents: [{ role: "user", parts: [{ text: "hello" }] }] } },
|
||||
stream: true,
|
||||
credentials: { accessToken: "token", projectId: "old-project" } as any,
|
||||
signal: undefined,
|
||||
log: undefined,
|
||||
});
|
||||
|
||||
const finalCall = calls.find((call) => call.url.includes("streamGenerateContent"));
|
||||
assert.ok(finalCall);
|
||||
|
||||
const finalBody = JSON.parse(finalCall.body);
|
||||
assert.deepEqual(Object.keys(finalBody), ["model", "project", "user_prompt_id", "request"]);
|
||||
assert.deepEqual(Object.keys(finalCall.headers), [
|
||||
"Content-Type",
|
||||
"User-Agent",
|
||||
"X-Goog-Api-Client",
|
||||
"Accept",
|
||||
"Accept-Encoding",
|
||||
"Authorization",
|
||||
]);
|
||||
assert.equal(finalBody.model, "gemini-3.1-pro-preview");
|
||||
assert.equal(finalBody.project, "project-live");
|
||||
assert.match(finalBody.user_prompt_id, /^agent-/);
|
||||
assert.match(finalBody.request.session_id, /^-\d+$/);
|
||||
assert.match(
|
||||
finalCall.headers["User-Agent"],
|
||||
/^GeminiCLI\/0\.40\.1\/gemini-3\.1-pro-preview /
|
||||
);
|
||||
assert.equal(finalCall.headers.Accept, "*/*");
|
||||
} finally {
|
||||
setCliCompatProviders([]);
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,8 +2,9 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { antigravityUserAgent, geminiCLIUserAgent, GEMINI_CLI_VERSION } =
|
||||
await import("../../open-sse/services/antigravityHeaders.ts");
|
||||
const { antigravityUserAgent } = await import("../../open-sse/services/antigravityHeaders.ts");
|
||||
const { geminiCliUserAgent, GEMINI_CLI_VERSION } =
|
||||
await import("../../open-sse/services/geminiCliHeaders.ts");
|
||||
|
||||
test("T20: antigravity config has updated User-Agent and sandbox fallback URL", () => {
|
||||
const antigravity = REGISTRY.antigravity;
|
||||
@@ -14,15 +15,15 @@ test("T20: antigravity config has updated User-Agent and sandbox fallback URL",
|
||||
assert.equal(antigravity.headers["User-Agent"], antigravityUserAgent());
|
||||
});
|
||||
|
||||
test("T20: gemini CLI fingerprint uses 0.39.1 and normalizes darwin to macos", () => {
|
||||
assert.equal(GEMINI_CLI_VERSION, "0.39.1");
|
||||
test("T20: gemini CLI fingerprint uses 0.40.1 and normalizes darwin to macos", () => {
|
||||
assert.equal(GEMINI_CLI_VERSION, "0.40.1");
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
try {
|
||||
assert.match(
|
||||
geminiCLIUserAgent("gemini-3-flash"),
|
||||
/^GeminiCLI\/0\.39\.1\/gemini-3-flash \(macos; /
|
||||
geminiCliUserAgent("gemini-3-flash"),
|
||||
/^GeminiCLI\/0\.40\.1\/gemini-3-flash \(macos; .+; terminal\) google-api-nodejs-client\/9\.15\.1$/
|
||||
);
|
||||
} finally {
|
||||
if (descriptor) {
|
||||
|
||||
@@ -351,6 +351,34 @@ test("OpenAI -> Gemini CLI adds thinking config and normalizes namespaced tool n
|
||||
assert.equal(getFunctionResponse(responseTurn.parts[0]).name, "weather");
|
||||
});
|
||||
|
||||
test("OpenAI -> Gemini CLI wraps Cloud Code envelope with native top-level and request keys", async () => {
|
||||
const { getRequestTranslator } = await import("../../open-sse/translator/registry.ts");
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
await import("../../open-sse/translator/request/openai-to-gemini.ts");
|
||||
|
||||
const translate = getRequestTranslator(FORMATS.OPENAI, FORMATS.GEMINI_CLI);
|
||||
assert.ok(translate, "expected Gemini CLI translator to be registered");
|
||||
|
||||
const result = translate(
|
||||
"models/gemini-2.5-flash",
|
||||
{ messages: [{ role: "user", content: "Hello" }] },
|
||||
true,
|
||||
{ projectId: "projects/demo" }
|
||||
) as UnknownRecord;
|
||||
const request = result.request as UnknownRecord;
|
||||
|
||||
assert.deepEqual(Object.keys(result), ["model", "project", "user_prompt_id", "request"]);
|
||||
assert.equal(result.model, "gemini-2.5-flash");
|
||||
assert.equal(result.project, "projects/demo");
|
||||
assert.equal(typeof result.user_prompt_id, "string");
|
||||
assert.equal(result.userAgent, undefined);
|
||||
assert.equal(result.requestId, undefined);
|
||||
assert.equal(result.requestType, undefined);
|
||||
assert.equal(typeof request.session_id, "string");
|
||||
assert.equal(request.sessionId, undefined);
|
||||
assert.ok(Array.isArray(request.contents));
|
||||
});
|
||||
|
||||
test("OpenAI -> Gemini request sanitizes long MCP tool names and strips unsupported schema fields", () => {
|
||||
const longToolName =
|
||||
"mcp__filesystem__read_multiple_files_with_validation_and_metadata_bundle_v2";
|
||||
@@ -502,6 +530,14 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
|
||||
);
|
||||
|
||||
assert.equal(result.project, "proj-1");
|
||||
assert.deepEqual(Object.keys(result), [
|
||||
"project",
|
||||
"model",
|
||||
"userAgent",
|
||||
"requestType",
|
||||
"requestId",
|
||||
"request",
|
||||
]);
|
||||
assert.equal(result.userAgent, "antigravity");
|
||||
assert.equal(result.requestType, "agent");
|
||||
assert.match(result.requestId, /^agent-/);
|
||||
|
||||
Reference in New Issue
Block a user