feat: implement protocol parity and harden upstream handling (#88)

* feat: implement protocol parity and harden upstream handling

* docs(agents): add lodash-es utility method and named import guideline
This commit is contained in:
vin authored and GitHub committed 2026-02-11 00:42:39 +08:00
1 parent 0f7a629a47
commit 13f10fe32f
23 files changed
+4408 -654

No files matched your search

+41 -91
View File
@@ -52,7 +52,7 @@ export function transformClaudeRequestIn(
const toolIdToName = new Map<string, string>();
// 1. System Instruction
const systemInstruction = buildSystemInstruction(claudeReq.system, claudeReq.model);
const systemInstruction = buildSystemInstruction(claudeReq.system);
// Map model name
const mappedModel = hasWebSearchTool
@@ -200,32 +200,24 @@ function resolveRequestConfig(
}
const hasNetworkingTool = detectsNetworkingTool(tools);
const hasNonNetworking = containsNonNetworkingTool(tools);
// Strip -online suffix
const isOnlineSuffix = originalModel.endsWith('-online');
const isHighQualityModel =
mappedModel === 'gemini-2.5-flash' ||
mappedModel === 'gemini-1.5-pro' ||
mappedModel.startsWith('gemini-1.5-pro-') ||
mappedModel.startsWith('gemini-2.5-flash-') ||
mappedModel.startsWith('gemini-2.0-flash') ||
mappedModel.startsWith('gemini-3-') ||
mappedModel.includes('claude-3-5-sonnet') ||
mappedModel.includes('claude-3-opus') ||
mappedModel.includes('claude-sonnet') ||
mappedModel.includes('claude-opus') ||
mappedModel.includes('claude-4');
// Determine if we should enable networking
const enableNetworking =
isOnlineSuffix || (isHighQualityModel && !hasNonNetworking) || hasNetworkingTool;
const enableNetworking = isOnlineSuffix || hasNetworkingTool;
let finalModel = mappedModel.replace(/-online$/, '');
if (finalModel === 'gemini-3-pro-preview') {
finalModel = 'gemini-3-pro-high';
} else if (finalModel === 'gemini-3-pro-image-preview') {
finalModel = 'gemini-3-pro-image';
} else if (finalModel === 'gemini-3-flash-preview') {
finalModel = 'gemini-3-flash';
}
if (enableNetworking) {
// Fallback for search compatibility
if (finalModel.includes('thinking') || !finalModel.startsWith('gemini-')) {
if (finalModel !== 'gemini-2.5-flash') {
finalModel = 'gemini-2.5-flash';
}
}
@@ -314,59 +306,6 @@ function detectsNetworkingTool(tools?: (Tool | GeminiToolDeclaration)[]): boolea
return false;
}
/**
* Detects if non-networking tools are present
* Checks tool list for tools other than web search
*/
function containsNonNetworkingTool(tools?: (Tool | GeminiToolDeclaration)[]): boolean {
if (!tools) {
return false;
}
const keywords = [
'web_search',
'google_search',
'web_search_20250305',
'google_search_retrieval',
];
for (const tool of tools) {
let isNetworking = false;
// Claude Tool format
if ('name' in tool && tool.name && keywords.includes(tool.name)) {
isNetworking = true;
}
// OpenAI nested format (runtime check)
const openaiTool = tool as { function?: { name?: string } };
if (openaiTool.function?.name && keywords.includes(openaiTool.function.name))
isNetworking = true;
// Gemini search tools
if ('googleSearch' in tool && tool.googleSearch) {
isNetworking = true;
}
if ('googleSearchRetrieval' in tool && tool.googleSearchRetrieval) {
isNetworking = true;
}
// Gemini functionDeclarations
if ('functionDeclarations' in tool && tool.functionDeclarations) {
for (const decl of tool.functionDeclarations) {
if (decl.name && !keywords.includes(decl.name)) {
return true; // Found local
}
}
isNetworking = true; // All are networking
}
if (!isNetworking) {
return true;
}
}
return false;
}
/**
* Inject Google Search Tool
* Adds googleSearch tool to the request
@@ -390,41 +329,39 @@ function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }) {
/**
* Builds system instruction
* Converts Claude system prompts to Gemini format, with Antigravity Identity Injection
* Converts Claude system prompts to Gemini format with a default assistant identity directive.
*/
function buildSystemInstruction(
system: ClaudeRequest['system'],
modelName: string,
): { parts: { text: string }[] } | null {
const antigravity_identity =
'--- [IDENTITY_PATCH] ---\n' +
'Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n' +
const assistantIdentityDirective =
'You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.\n' +
'You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.\n' +
'**Absolute paths only**\n' +
'**Proactiveness**';
const identityMarker = 'You are Antigravity';
const parts: { text: string }[] = [];
// Check if user already provided Antigravity identity
let userHasAntigravity = false;
let hasIdentityDirective = false;
if (system) {
if (typeof system === 'string') {
if (system.includes('You are Antigravity')) userHasAntigravity = true;
if (system.includes(identityMarker)) {
hasIdentityDirective = true;
}
} else if (Array.isArray(system)) {
for (const block of system) {
if (block.type === 'text' && block.text.includes('You are Antigravity')) {
userHasAntigravity = true;
if (block.type === 'text' && block.text.includes(identityMarker)) {
hasIdentityDirective = true;
break;
}
}
}
}
// Inject if missing
if (!userHasAntigravity) {
parts.push({ text: antigravity_identity });
if (!hasIdentityDirective) {
parts.push({ text: assistantIdentityDirective });
}
if (system) {
@@ -437,10 +374,6 @@ function buildSystemInstruction(
}
}
if (!userHasAntigravity) {
parts.push({ text: '\n--- [SYSTEM_PROMPT_END] ---' });
}
// If we pushed at least something
if (parts.length > 0) {
return { parts };
@@ -635,7 +568,22 @@ function buildGenerationConfig(
hasWebSearch: boolean,
mappedModel: string,
): GenerationConfig {
const source = String(claudeReq.metadata?.source || '').toLowerCase();
const isOpenAIPath = source === 'openai';
const config: GenerationConfig = {};
if (isOpenAIPath) {
config.temperature = claudeReq.temperature ?? 1.0;
config.topP = claudeReq.top_p ?? 0.95;
if (claudeReq.max_tokens !== undefined) {
config.maxOutputTokens = claudeReq.max_tokens;
}
if (claudeReq.stop_sequences && claudeReq.stop_sequences.length > 0) {
config.stopSequences = claudeReq.stop_sequences;
}
return config;
}
if (claudeReq.thinking?.type === 'enabled') {
const thinkingConfig: GenerationConfig['thinkingConfig'] = { includeThoughts: true };
if (claudeReq.thinking.budget_tokens) {
@@ -655,7 +603,9 @@ function buildGenerationConfig(
if (claudeReq.top_k !== undefined) {
config.topK = claudeReq.top_k;
}
config.maxOutputTokens = 64000;
if (claudeReq.max_tokens !== undefined) {
config.maxOutputTokens = claudeReq.max_tokens;
}
config.stopSequences = ['<|user|>', '<|endoftext|>', '<|end_of_turn|>', '[DONE]', '\n\nHuman:'];
return config;
}
+2 -1
View File
@@ -226,6 +226,8 @@ class NonStreamingProcessor {
const usage: Usage = {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
return {
@@ -235,7 +237,6 @@ class NonStreamingProcessor {
model: geminiResponse.modelVersion || '',
content: this.contentBlocks,
stop_reason: stopReason,
stop_sequence: null,
usage: usage,
};
}
+64 -22
View File
@@ -3,6 +3,7 @@ import { logger } from '../../utils/logger';
const CLAUDE_TO_GEMINI: Record<string, string> = {
// Directly supported models
'claude-opus-4-5-thinking': 'claude-opus-4-5-thinking',
'claude-opus-4-6-thinking': 'claude-opus-4-6-thinking',
'claude-sonnet-4-5': 'claude-sonnet-4-5',
'claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking',
@@ -12,21 +13,23 @@ const CLAUDE_TO_GEMINI: Record<string, string> = {
'claude-3-5-sonnet-20240620': 'claude-sonnet-4-5',
'claude-opus-4': 'claude-opus-4-5-thinking',
'claude-opus-4-5-20251101': 'claude-opus-4-5-thinking',
'claude-opus-4-6': 'claude-opus-4-6-thinking',
'claude-opus-4-6-20260201': 'claude-opus-4-6-thinking',
'claude-haiku-4': 'claude-sonnet-4-5',
'claude-3-haiku-20240307': 'claude-sonnet-4-5',
'claude-haiku-4-5-20251001': 'claude-sonnet-4-5',
// OpenAI Protocol Mapping
'gpt-4': 'gemini-2.5-pro',
'gpt-4-turbo': 'gemini-2.5-pro',
'gpt-4-turbo-preview': 'gemini-2.5-pro',
'gpt-4-0125-preview': 'gemini-2.5-pro',
'gpt-4-1106-preview': 'gemini-2.5-pro',
'gpt-4-0613': 'gemini-2.5-pro',
'gpt-4': 'gemini-2.5-flash',
'gpt-4-turbo': 'gemini-2.5-flash',
'gpt-4-turbo-preview': 'gemini-2.5-flash',
'gpt-4-0125-preview': 'gemini-2.5-flash',
'gpt-4-1106-preview': 'gemini-2.5-flash',
'gpt-4-0613': 'gemini-2.5-flash',
'gpt-4o': 'gemini-2.5-pro',
'gpt-4o-2024-05-13': 'gemini-2.5-pro',
'gpt-4o-2024-08-06': 'gemini-2.5-pro',
'gpt-4o': 'gemini-2.5-flash',
'gpt-4o-2024-05-13': 'gemini-2.5-flash',
'gpt-4o-2024-08-06': 'gemini-2.5-flash',
'gpt-4o-mini': 'gemini-2.5-flash',
'gpt-4o-mini-2024-07-18': 'gemini-2.5-flash',
@@ -38,29 +41,68 @@ const CLAUDE_TO_GEMINI: Record<string, string> = {
'gpt-3.5-turbo-0613': 'gemini-2.5-flash',
// Gemini Protocol Mapping
'gemini-2.5-flash-lite': 'gemini-2.5-flash-lite',
'gemini-2.5-flash-lite': 'gemini-2.5-flash',
'gemini-2.5-flash-thinking': 'gemini-2.5-flash-thinking',
'gemini-3-pro-low': 'gemini-3-pro-low',
'gemini-3-pro-high': 'gemini-3-pro-high',
'gemini-3-pro-low': 'gemini-3-pro-preview',
'gemini-3-pro-high': 'gemini-3-pro-preview',
'gemini-3-pro-preview': 'gemini-3-pro-preview',
'gemini-3-pro': 'gemini-3-pro-preview',
'gemini-2.5-flash': 'gemini-2.5-flash',
'gemini-3-flash': 'gemini-3-flash',
'gemini-3-pro-image': 'gemini-3-pro-image',
'internal-background-task': 'gemini-2.5-flash',
};
const DYNAMIC_IMAGE_BASE_MODEL = 'gemini-3-pro-image';
const DYNAMIC_IMAGE_RESOLUTIONS = ['', '-2k', '-4k'];
const DYNAMIC_IMAGE_RATIOS = ['', '-1x1', '-4x3', '-3x4', '-16x9', '-9x16', '-21x9'];
const EXTRA_DYNAMIC_MODELS = [
'gemini-2.0-flash-exp',
'gemini-2.5-flash',
'gemini-3-flash',
'gemini-3-pro-high',
'gemini-3-pro-low',
];
export const MODEL_LIST_CREATED_AT = 1770652800;
export const MODEL_LIST_OWNER = 'antigravity';
export function getSupportedModels(): string[] {
return Object.keys(CLAUDE_TO_GEMINI);
}
export function getAllDynamicModels(customMapping: Record<string, string> = {}): string[] {
const modelIds = new Set<string>();
for (const modelId of getSupportedModels()) {
modelIds.add(modelId);
}
for (const customModelId of Object.keys(customMapping)) {
modelIds.add(customModelId);
}
for (const resolution of DYNAMIC_IMAGE_RESOLUTIONS) {
for (const ratio of DYNAMIC_IMAGE_RATIOS) {
modelIds.add(`${DYNAMIC_IMAGE_BASE_MODEL}${resolution}${ratio}`);
}
}
for (const modelId of EXTRA_DYNAMIC_MODELS) {
modelIds.add(modelId);
}
return [...modelIds].sort();
}
export function mapClaudeModelToGemini(input: string): string {
// 1. Check exact match in map
if (CLAUDE_TO_GEMINI[input]) {
return CLAUDE_TO_GEMINI[input];
const mappedModel = CLAUDE_TO_GEMINI[input];
if (mappedModel) {
return mappedModel;
}
// 2. Pass-through known prefixes (gemini-, -thinking) to support dynamic suffixes
if (input.startsWith('gemini-') || input.includes('thinking')) {
return input;
}
// 3. Fallback to default
return 'claude-sonnet-4-5';
return input;
}
/**
+6 -1
View File
@@ -53,6 +53,7 @@ export interface ClaudeRequest {
tools?: Tool[];
stream?: boolean;
max_tokens?: number;
stop_sequences?: string[];
temperature?: number;
top_p?: number;
top_k?: number;
@@ -141,6 +142,8 @@ export interface Tool {
export interface Metadata {
user_id?: string;
source?: string;
[key: string]: unknown;
}
// ============================================================================
@@ -336,13 +339,15 @@ export interface ClaudeResponse {
model: string;
content: ContentBlock[];
stop_reason: string;
stop_sequence: string | null;
stop_sequence?: string | null;
usage: Usage;
}
export interface Usage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
/** Server tool usage stats */
server_tool_use?: ServerToolUse;
}