feat(antigravity): add model specs and update mapping logic
This commit is contained in:
1 parent
30e38dcb8b
commit
0a60c0eafa
11 files changed
+552
-82
No files matched your search
@@ -1,6 +1,8 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { mapClaudeModelToGemini } from './ModelMapping';
|
import { isPlainObject, isString } from 'lodash-es';
|
||||||
import { cleanJsonSchema } from './JsonSchemaUtils';
|
import { mapClaudeModelToGemini, normalizeGeminiModelAlias } from './ModelMapping';
|
||||||
|
import { getMaxOutputTokens, getThinkingBudget } from './ModelSpecs';
|
||||||
|
import { cleanJsonSchema, normalizeObjectJsonSchema } from './JsonSchemaUtils';
|
||||||
import { SignatureStore } from './SignatureStore';
|
import { SignatureStore } from './SignatureStore';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +17,11 @@ import {
|
|||||||
FunctionDeclaration,
|
FunctionDeclaration,
|
||||||
SafetySetting,
|
SafetySetting,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import {
|
||||||
|
buildUserAgent,
|
||||||
|
FALLBACK_VERSION,
|
||||||
|
resolveLocalInstalledVersion,
|
||||||
|
} from '@/server/modules/proxy/request-user-agent';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request Configuration
|
* Request Configuration
|
||||||
@@ -44,6 +51,7 @@ interface RequestConfig {
|
|||||||
export function transformClaudeRequestIn(
|
export function transformClaudeRequestIn(
|
||||||
claudeReq: ClaudeRequest,
|
claudeReq: ClaudeRequest,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
|
userAgent?: string,
|
||||||
): GeminiInternalRequest {
|
): GeminiInternalRequest {
|
||||||
// Check for networking tools (server tool or built-in tool)
|
// Check for networking tools (server tool or built-in tool)
|
||||||
const hasWebSearchTool = detectsNetworkingTool(claudeReq.tools);
|
const hasWebSearchTool = detectsNetworkingTool(claudeReq.tools);
|
||||||
@@ -55,9 +63,7 @@ export function transformClaudeRequestIn(
|
|||||||
const systemInstruction = buildSystemInstruction(claudeReq.system);
|
const systemInstruction = buildSystemInstruction(claudeReq.system);
|
||||||
|
|
||||||
// Map model name
|
// Map model name
|
||||||
const mappedModel = hasWebSearchTool
|
const mappedModel = hasWebSearchTool ? 'gemini-3-flash' : mapClaudeModelToGemini(claudeReq.model);
|
||||||
? 'gemini-2.5-flash'
|
|
||||||
: mapClaudeModelToGemini(claudeReq.model);
|
|
||||||
|
|
||||||
// Convert Claude tools to Tool array for networking detection
|
// Convert Claude tools to Tool array for networking detection
|
||||||
const toolsVal: Tool[] | undefined = claudeReq.tools
|
const toolsVal: Tool[] | undefined = claudeReq.tools
|
||||||
@@ -70,7 +76,11 @@ export function transformClaudeRequestIn(
|
|||||||
const allowDummyThought = config.finalModel.startsWith('gemini-');
|
const allowDummyThought = config.finalModel.startsWith('gemini-');
|
||||||
|
|
||||||
// 4. Generation Config & Thinking
|
// 4. Generation Config & Thinking
|
||||||
let isThinkingEnabled = claudeReq.thinking?.type === 'enabled';
|
const thinkingType = (claudeReq.thinking?.type ?? '').toLowerCase();
|
||||||
|
const autoThinkingEnabled =
|
||||||
|
!claudeReq.thinking && shouldEnableThinkingByDefault(config.finalModel, claudeReq.model);
|
||||||
|
let isThinkingEnabled =
|
||||||
|
thinkingType === 'enabled' || thinkingType === 'adaptive' || autoThinkingEnabled;
|
||||||
|
|
||||||
if (isThinkingEnabled) {
|
if (isThinkingEnabled) {
|
||||||
const globalSig = SignatureStore.get();
|
const globalSig = SignatureStore.get();
|
||||||
@@ -82,11 +92,18 @@ export function transformClaudeRequestIn(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hasFunctionCalls && !hasValidSignatureForFunctionCalls(claudeReq.messages, globalSig)) {
|
if (hasFunctionCalls && !hasValidSignatureForFunctionCalls(claudeReq.messages, globalSig)) {
|
||||||
isThinkingEnabled = false;
|
if (!isGeminiFlashModel(config.finalModel)) {
|
||||||
|
isThinkingEnabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generationConfig = buildGenerationConfig(claudeReq, hasWebSearchTool, config.finalModel);
|
const generationConfig = buildGenerationConfig(
|
||||||
|
claudeReq,
|
||||||
|
hasWebSearchTool,
|
||||||
|
config.finalModel,
|
||||||
|
isThinkingEnabled,
|
||||||
|
);
|
||||||
// Update thinking config based on the final decision
|
// Update thinking config based on the final decision
|
||||||
if (!isThinkingEnabled && generationConfig.thinkingConfig) {
|
if (!isThinkingEnabled && generationConfig.thinkingConfig) {
|
||||||
delete generationConfig.thinkingConfig;
|
delete generationConfig.thinkingConfig;
|
||||||
@@ -98,10 +115,11 @@ export function transformClaudeRequestIn(
|
|||||||
toolIdToName,
|
toolIdToName,
|
||||||
isThinkingEnabled,
|
isThinkingEnabled,
|
||||||
allowDummyThought,
|
allowDummyThought,
|
||||||
|
config.finalModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Tools
|
// 3. Tools
|
||||||
const tools = buildTools(claudeReq.tools, hasWebSearchTool);
|
const tools = buildTools(claudeReq.tools, hasWebSearchTool, config.finalModel);
|
||||||
|
|
||||||
// 5. Safety Settings
|
// 5. Safety Settings
|
||||||
const safetySettings = [
|
const safetySettings = [
|
||||||
@@ -142,7 +160,7 @@ export function transformClaudeRequestIn(
|
|||||||
|
|
||||||
// Inject googleSearch tool if needed (and not already done by buildTools)
|
// Inject googleSearch tool if needed (and not already done by buildTools)
|
||||||
if (config.injectGoogleSearch && !hasWebSearchTool) {
|
if (config.injectGoogleSearch && !hasWebSearchTool) {
|
||||||
injectGoogleSearchTool(innerRequest);
|
injectGoogleSearchTool(innerRequest, config.finalModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject imageConfig if present (for image generation models)
|
// Inject imageConfig if present (for image generation models)
|
||||||
@@ -165,11 +183,12 @@ export function transformClaudeRequestIn(
|
|||||||
|
|
||||||
const normalizedProjectId = projectId?.trim();
|
const normalizedProjectId = projectId?.trim();
|
||||||
|
|
||||||
|
const discoveryVersion = resolveLocalInstalledVersion() ?? FALLBACK_VERSION;
|
||||||
const body: GeminiInternalRequest = {
|
const body: GeminiInternalRequest = {
|
||||||
requestId: requestId,
|
requestId: requestId,
|
||||||
request: innerRequest as GeminiInternalRequest['request'],
|
request: innerRequest as GeminiInternalRequest['request'],
|
||||||
model: config.finalModel,
|
model: config.finalModel,
|
||||||
userAgent: 'antigravity',
|
userAgent: userAgent?.trim() || buildUserAgent(discoveryVersion),
|
||||||
requestType: config.requestType,
|
requestType: config.requestType,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,7 +213,7 @@ function resolveRequestConfig(
|
|||||||
tools?: Tool[],
|
tools?: Tool[],
|
||||||
): RequestConfig {
|
): RequestConfig {
|
||||||
// 1. Image Generation Check
|
// 1. Image Generation Check
|
||||||
if (mappedModel.startsWith('gemini-3-pro-image')) {
|
if (isGeminiImageModel(mappedModel)) {
|
||||||
const { imageConfig, parsedBaseModel } = parseImageConfig(originalModel);
|
const { imageConfig, parsedBaseModel } = parseImageConfig(originalModel);
|
||||||
return {
|
return {
|
||||||
requestType: 'image_gen',
|
requestType: 'image_gen',
|
||||||
@@ -212,18 +231,11 @@ function resolveRequestConfig(
|
|||||||
const enableNetworking = isOnlineSuffix || hasNetworkingTool;
|
const enableNetworking = isOnlineSuffix || hasNetworkingTool;
|
||||||
|
|
||||||
let finalModel = mappedModel.replace(/-online$/, '');
|
let finalModel = mappedModel.replace(/-online$/, '');
|
||||||
|
finalModel = normalizeGeminiModelAlias(finalModel);
|
||||||
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) {
|
if (enableNetworking) {
|
||||||
if (finalModel !== 'gemini-2.5-flash') {
|
if (finalModel !== 'gemini-3-flash') {
|
||||||
finalModel = 'gemini-2.5-flash';
|
finalModel = 'gemini-3-flash';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +272,50 @@ function parseImageConfig(modelName: string): {
|
|||||||
return { imageConfig: config, parsedBaseModel: 'gemini-3-pro-image' };
|
return { imageConfig: config, parsedBaseModel: 'gemini-3-pro-image' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGeminiImageModel(modelName: string): boolean {
|
||||||
|
const normalized = modelName.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.startsWith('gemini-3-pro-image') || normalized.startsWith('gemini-3.1-pro-image')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGeminiFlashModel(modelName: string): boolean {
|
||||||
|
const normalized = modelName.toLowerCase();
|
||||||
|
return normalized.includes('gemini-3-flash') || normalized.includes('gemini-3.1-flash');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldEnableThinkingByDefault(mappedModel: string, originalModel: string): boolean {
|
||||||
|
const mappedLower = mappedModel.toLowerCase();
|
||||||
|
const originalLower = originalModel.toLowerCase();
|
||||||
|
return (
|
||||||
|
originalLower.includes('claude-opus-4-5') ||
|
||||||
|
originalLower.includes('claude-opus-4-6') ||
|
||||||
|
mappedLower.includes('-thinking') ||
|
||||||
|
mappedLower.includes('gemini-3.1-pro') ||
|
||||||
|
mappedLower.includes('gemini-3-flash') ||
|
||||||
|
mappedLower.includes('gemini-3.1-flash')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeModel(modelName: string): boolean {
|
||||||
|
return modelName.toLowerCase().includes('claude');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAdaptiveThinkingLevel(claudeReq: ClaudeRequest): 'low' | 'medium' | 'high' {
|
||||||
|
const effort = String(claudeReq.thinking?.effort ?? '').toLowerCase();
|
||||||
|
if (effort === 'low') {
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
if (effort === 'medium') {
|
||||||
|
return 'medium';
|
||||||
|
}
|
||||||
|
return 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toToolSchema(schema: unknown): Record<string, unknown> {
|
||||||
|
return normalizeObjectJsonSchema(schema);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects if networking tools are present
|
* Detects if networking tools are present
|
||||||
* Checks tool list for web search related tools
|
* Checks tool list for web search related tools
|
||||||
@@ -274,20 +330,23 @@ function detectsNetworkingTool(tools?: (Tool | GeminiToolDeclaration)[]): boolea
|
|||||||
'google_search',
|
'google_search',
|
||||||
'web_search_20250305',
|
'web_search_20250305',
|
||||||
'google_search_retrieval',
|
'google_search_retrieval',
|
||||||
|
'builtin_web_search',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
// Claude Tool format
|
// Claude Tool format
|
||||||
if ('name' in tool && tool.name && keywords.includes(tool.name)) {
|
const toolName = (tool as { name?: unknown }).name;
|
||||||
|
if (isString(toolName) && keywords.includes(toolName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ('type' in tool && tool.type && keywords.includes(tool.type)) {
|
const toolType = (tool as { type?: unknown }).type;
|
||||||
|
if (isString(toolType) && keywords.includes(toolType)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI nested format (runtime check)
|
// OpenAI nested format (runtime check)
|
||||||
const openaiTool = tool as { function?: { name?: string } };
|
const openaiTool = tool as { function?: { name?: string } };
|
||||||
if (openaiTool.function?.name && keywords.includes(openaiTool.function.name)) {
|
if (isString(openaiTool.function?.name) && keywords.includes(openaiTool.function.name)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,15 +374,25 @@ function detectsNetworkingTool(tools?: (Tool | GeminiToolDeclaration)[]): boolea
|
|||||||
* Inject Google Search Tool
|
* Inject Google Search Tool
|
||||||
* Adds googleSearch tool to the request
|
* Adds googleSearch tool to the request
|
||||||
*/
|
*/
|
||||||
function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }) {
|
function supportsMixedTools(mappedModel?: string): boolean {
|
||||||
|
if (!mappedModel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const modelLower = mappedModel.toLowerCase();
|
||||||
|
return modelLower.includes('gemini-3');
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }, mappedModel?: string) {
|
||||||
if (!body.tools) {
|
if (!body.tools) {
|
||||||
body.tools = [];
|
body.tools = [];
|
||||||
}
|
}
|
||||||
const toolsArr = body.tools;
|
const toolsArr = body.tools;
|
||||||
|
|
||||||
const hasFunctions = toolsArr.some((t) => t.functionDeclarations);
|
const hasFunctions = toolsArr.some((t) => t.functionDeclarations);
|
||||||
if (hasFunctions) {
|
if (hasFunctions && !supportsMixedTools(mappedModel)) {
|
||||||
logger.info('Skipping googleSearch injection due to existing functionDeclarations');
|
logger.info(
|
||||||
|
'Skipping googleSearch injection due to existing functionDeclarations on old model',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +420,7 @@ function buildSystemInstruction(
|
|||||||
let hasIdentityDirective = false;
|
let hasIdentityDirective = false;
|
||||||
|
|
||||||
if (system) {
|
if (system) {
|
||||||
if (typeof system === 'string') {
|
if (isString(system)) {
|
||||||
if (system.includes(identityMarker)) {
|
if (system.includes(identityMarker)) {
|
||||||
hasIdentityDirective = true;
|
hasIdentityDirective = true;
|
||||||
}
|
}
|
||||||
@@ -370,7 +439,7 @@ function buildSystemInstruction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (system) {
|
if (system) {
|
||||||
if (typeof system === 'string') {
|
if (isString(system)) {
|
||||||
parts.push({ text: system });
|
parts.push({ text: system });
|
||||||
} else if (Array.isArray(system)) {
|
} else if (Array.isArray(system)) {
|
||||||
for (const block of system) {
|
for (const block of system) {
|
||||||
@@ -437,6 +506,7 @@ function buildContents(
|
|||||||
toolIdToName: Map<string, string>,
|
toolIdToName: Map<string, string>,
|
||||||
isThinkingEnabled: boolean,
|
isThinkingEnabled: boolean,
|
||||||
allowDummyThought: boolean,
|
allowDummyThought: boolean,
|
||||||
|
mappedModel: string,
|
||||||
): GeminiContent[] {
|
): GeminiContent[] {
|
||||||
const contents: GeminiContent[] = [];
|
const contents: GeminiContent[] = [];
|
||||||
let lastThoughtSignature: string | null = null;
|
let lastThoughtSignature: string | null = null;
|
||||||
@@ -480,12 +550,16 @@ function buildContents(
|
|||||||
cleanJsonSchema(part);
|
cleanJsonSchema(part);
|
||||||
toolIdToName.set(block.id, block.name);
|
toolIdToName.set(block.id, block.name);
|
||||||
const finalSig = block.signature || lastThoughtSignature || SignatureStore.get();
|
const finalSig = block.signature || lastThoughtSignature || SignatureStore.get();
|
||||||
if (finalSig) part.thoughtSignature = finalSig;
|
if (finalSig) {
|
||||||
|
part.thoughtSignature = finalSig;
|
||||||
|
} else if (isThinkingEnabled && isGeminiFlashModel(mappedModel)) {
|
||||||
|
part.thoughtSignature = 'skip_thought_signature_validator';
|
||||||
|
}
|
||||||
parts.push(part);
|
parts.push(part);
|
||||||
} else if (block.type === 'tool_result') {
|
} else if (block.type === 'tool_result') {
|
||||||
const funcName = toolIdToName.get(block.tool_use_id) || block.tool_use_id;
|
const funcName = toolIdToName.get(block.tool_use_id) || block.tool_use_id;
|
||||||
let mergedContent = '';
|
let mergedContent = '';
|
||||||
if (typeof block.content === 'string') mergedContent = block.content;
|
if (isString(block.content)) mergedContent = block.content;
|
||||||
else if (Array.isArray(block.content))
|
else if (Array.isArray(block.content))
|
||||||
mergedContent = block.content
|
mergedContent = block.content
|
||||||
.filter((b: any) => b.type === 'text')
|
.filter((b: any) => b.type === 'text')
|
||||||
@@ -524,6 +598,7 @@ function buildContents(
|
|||||||
function buildTools(
|
function buildTools(
|
||||||
tools: Tool[] | undefined,
|
tools: Tool[] | undefined,
|
||||||
hasWebSearch: boolean,
|
hasWebSearch: boolean,
|
||||||
|
mappedModel: string,
|
||||||
): GeminiToolDeclaration[] | null {
|
): GeminiToolDeclaration[] | null {
|
||||||
if (!tools) {
|
if (!tools) {
|
||||||
return null;
|
return null;
|
||||||
@@ -535,14 +610,15 @@ function buildTools(
|
|||||||
if (
|
if (
|
||||||
tool.name === 'web_search' ||
|
tool.name === 'web_search' ||
|
||||||
tool.name === 'google_search' ||
|
tool.name === 'google_search' ||
|
||||||
tool.type === 'web_search_20250305'
|
tool.name === 'builtin_web_search' ||
|
||||||
|
tool.type === 'web_search_20250305' ||
|
||||||
|
tool.type === 'builtin_web_search'
|
||||||
) {
|
) {
|
||||||
hasGoogleSearch = true;
|
hasGoogleSearch = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (tool.name) {
|
if (tool.name) {
|
||||||
const inputSchema = tool.input_schema || { type: 'object', properties: {} };
|
const inputSchema = toToolSchema(tool.input_schema);
|
||||||
cleanJsonSchema(inputSchema);
|
|
||||||
functionDeclarations.push({
|
functionDeclarations.push({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
@@ -551,15 +627,24 @@ function buildTools(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolObj: GeminiToolDeclaration = {};
|
const toolList: GeminiToolDeclaration[] = [];
|
||||||
if (functionDeclarations.length > 0) {
|
if (functionDeclarations.length > 0) {
|
||||||
toolObj.functionDeclarations = functionDeclarations;
|
toolList.push({ functionDeclarations });
|
||||||
|
if (hasGoogleSearch) {
|
||||||
|
if (supportsMixedTools(mappedModel)) {
|
||||||
|
toolList.push({ googleSearch: {} });
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`[Claude-Request] Skipping googleSearch injection for ${mappedModel} due to existing functionDeclarations on old model`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (hasGoogleSearch) {
|
} else if (hasGoogleSearch) {
|
||||||
toolObj.googleSearch = {};
|
toolList.push({ googleSearch: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(toolObj).length > 0) {
|
if (toolList.length > 0) {
|
||||||
return [toolObj];
|
return toolList;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -572,32 +657,53 @@ function buildGenerationConfig(
|
|||||||
claudeReq: ClaudeRequest,
|
claudeReq: ClaudeRequest,
|
||||||
hasWebSearch: boolean,
|
hasWebSearch: boolean,
|
||||||
mappedModel: string,
|
mappedModel: string,
|
||||||
|
isThinkingEnabled: boolean,
|
||||||
): GenerationConfig {
|
): GenerationConfig {
|
||||||
const source = String(claudeReq.metadata?.source || '').toLowerCase();
|
const source = String(claudeReq.metadata?.source || '').toLowerCase();
|
||||||
const isOpenAIPath = source === 'openai';
|
const isOpenAIPath = source === 'openai';
|
||||||
const config: GenerationConfig = {};
|
const config: GenerationConfig = {};
|
||||||
|
const thinkingType = String(claudeReq.thinking?.type ?? '').toLowerCase();
|
||||||
|
|
||||||
|
const buildThinkingConfig = (): GenerationConfig['thinkingConfig'] => {
|
||||||
|
const thinkingConfig: GenerationConfig['thinkingConfig'] = { includeThoughts: true };
|
||||||
|
if (thinkingType === 'adaptive') {
|
||||||
|
if (isClaudeModel(mappedModel)) {
|
||||||
|
thinkingConfig.thinkingLevel = resolveAdaptiveThinkingLevel(claudeReq);
|
||||||
|
} else {
|
||||||
|
thinkingConfig.thinkingBudget = 24576;
|
||||||
|
}
|
||||||
|
} else if (claudeReq.thinking?.budget_tokens) {
|
||||||
|
let budget = claudeReq.thinking.budget_tokens;
|
||||||
|
const isFlash = hasWebSearch || isGeminiFlashModel(mappedModel);
|
||||||
|
if (isFlash) {
|
||||||
|
budget = Math.min(budget, 24576);
|
||||||
|
}
|
||||||
|
thinkingConfig.thinkingBudget = budget;
|
||||||
|
} else {
|
||||||
|
thinkingConfig.thinkingBudget = getThinkingBudget(mappedModel);
|
||||||
|
}
|
||||||
|
return thinkingConfig;
|
||||||
|
};
|
||||||
|
|
||||||
if (isOpenAIPath) {
|
if (isOpenAIPath) {
|
||||||
config.temperature = claudeReq.temperature ?? 1.0;
|
config.temperature = claudeReq.temperature ?? 1.0;
|
||||||
config.topP = claudeReq.top_p ?? 0.95;
|
config.topP = claudeReq.top_p ?? 0.95;
|
||||||
if (claudeReq.max_tokens !== undefined) {
|
if (claudeReq.max_tokens !== undefined) {
|
||||||
config.maxOutputTokens = claudeReq.max_tokens;
|
config.maxOutputTokens = claudeReq.max_tokens;
|
||||||
|
} else {
|
||||||
|
config.maxOutputTokens = getMaxOutputTokens(mappedModel);
|
||||||
}
|
}
|
||||||
if (claudeReq.stop_sequences && claudeReq.stop_sequences.length > 0) {
|
if (claudeReq.stop_sequences && claudeReq.stop_sequences.length > 0) {
|
||||||
config.stopSequences = claudeReq.stop_sequences;
|
config.stopSequences = claudeReq.stop_sequences;
|
||||||
}
|
}
|
||||||
|
if (isThinkingEnabled) {
|
||||||
|
config.thinkingConfig = buildThinkingConfig();
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (claudeReq.thinking?.type === 'enabled') {
|
if (isThinkingEnabled) {
|
||||||
const thinkingConfig: GenerationConfig['thinkingConfig'] = { includeThoughts: true };
|
config.thinkingConfig = buildThinkingConfig();
|
||||||
if (claudeReq.thinking.budget_tokens) {
|
|
||||||
let budget = claudeReq.thinking.budget_tokens;
|
|
||||||
const isFlash = hasWebSearch || mappedModel.includes('gemini-2.5-flash');
|
|
||||||
if (isFlash) budget = Math.min(budget, 24576);
|
|
||||||
thinkingConfig.thinkingBudget = budget;
|
|
||||||
}
|
|
||||||
config.thinkingConfig = thinkingConfig;
|
|
||||||
}
|
}
|
||||||
if (claudeReq.temperature !== undefined) {
|
if (claudeReq.temperature !== undefined) {
|
||||||
config.temperature = claudeReq.temperature;
|
config.temperature = claudeReq.temperature;
|
||||||
@@ -623,7 +729,7 @@ function buildGenerationConfig(
|
|||||||
function deepCleanUndefined(obj: unknown): void {
|
function deepCleanUndefined(obj: unknown): void {
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
obj.forEach(deepCleanUndefined);
|
obj.forEach(deepCleanUndefined);
|
||||||
} else if (obj && typeof obj === 'object') {
|
} else if (isPlainObject(obj)) {
|
||||||
const record = obj as Record<string, unknown>;
|
const record = obj as Record<string, unknown>;
|
||||||
Object.keys(record).forEach((key) => {
|
Object.keys(record).forEach((key) => {
|
||||||
if (record[key] === undefined) delete record[key];
|
if (record[key] === undefined) delete record[key];
|
||||||
|
|||||||
@@ -165,21 +165,21 @@ class NonStreamingProcessor {
|
|||||||
let groundingText = '';
|
let groundingText = '';
|
||||||
|
|
||||||
if (grounding.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
if (grounding.webSearchQueries && grounding.webSearchQueries.length > 0) {
|
||||||
groundingText += `\n\n---\n**🔍 已为您搜索:** ${grounding.webSearchQueries.join(', ')}`;
|
groundingText += `\n\n---\n**🔍 Searched for you:** ${grounding.webSearchQueries.join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (grounding.groundingChunks) {
|
if (grounding.groundingChunks) {
|
||||||
const links: string[] = [];
|
const links: string[] = [];
|
||||||
grounding.groundingChunks.forEach((chunk, index) => {
|
grounding.groundingChunks.forEach((chunk, index) => {
|
||||||
if (chunk.web) {
|
if (chunk.web) {
|
||||||
const title = chunk.web.title || '网页来源';
|
const title = chunk.web.title || 'Web source';
|
||||||
const uri = chunk.web.uri || '#';
|
const uri = chunk.web.uri || '#';
|
||||||
links.push(`[${index + 1}] [${title}](${uri})`);
|
links.push(`[${index + 1}] [${title}](${uri})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
groundingText += `\n\n**🌐 来源引文:**\n` + links.join('\n');
|
groundingText += `\n\n**🌐 Citations:**\n` + links.join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,19 +173,19 @@ export class StreamingState {
|
|||||||
// Process grounding (web search) -> convert to Markdown text block
|
// Process grounding (web search) -> convert to Markdown text block
|
||||||
let groundingText = '';
|
let groundingText = '';
|
||||||
if (this.webSearchQuery) {
|
if (this.webSearchQuery) {
|
||||||
groundingText += `\n\n---\n**🔍 已为您搜索:** ${this.webSearchQuery}`;
|
groundingText += `\n\n---\n**🔍 Searched for you:** ${this.webSearchQuery}`;
|
||||||
}
|
}
|
||||||
if (this.groundingChunks && this.groundingChunks.length > 0) {
|
if (this.groundingChunks && this.groundingChunks.length > 0) {
|
||||||
const links: string[] = [];
|
const links: string[] = [];
|
||||||
this.groundingChunks.forEach((chunk, i) => {
|
this.groundingChunks.forEach((chunk, i) => {
|
||||||
if (chunk.web) {
|
if (chunk.web) {
|
||||||
const title = chunk.web.title || '网页来源';
|
const title = chunk.web.title || 'Web source';
|
||||||
const uri = chunk.web.uri || '#';
|
const uri = chunk.web.uri || '#';
|
||||||
links.push(`[${i + 1}] [${title}](${uri})`);
|
links.push(`[${i + 1}] [${title}](${uri})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (links.length > 0) {
|
if (links.length > 0) {
|
||||||
groundingText += `\n\n**🌐 来源引文:**\n` + links.join('\n');
|
groundingText += `\n\n**🌐 Citations:**\n` + links.join('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,37 @@ export function cleanJsonSchema(value: any) {
|
|||||||
cleanJsonSchemaRecursive(value);
|
cleanJsonSchemaRecursive(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeObjectJsonSchema(schema: unknown): Record<string, unknown> {
|
||||||
|
const fallbackSchema: Record<string, unknown> = { type: 'object', properties: {} };
|
||||||
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
||||||
|
return fallbackSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSchema = JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
|
||||||
|
cleanJsonSchema(normalizedSchema);
|
||||||
|
|
||||||
|
if (typeof normalizedSchema.type !== 'string') {
|
||||||
|
normalizedSchema.type = 'object';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalizedSchema.type === 'object' &&
|
||||||
|
(!normalizedSchema.properties ||
|
||||||
|
typeof normalizedSchema.properties !== 'object' ||
|
||||||
|
Array.isArray(normalizedSchema.properties))
|
||||||
|
) {
|
||||||
|
normalizedSchema.properties = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSchema;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively expand $ref
|
* Recursively expand $ref
|
||||||
*/
|
*/
|
||||||
function flattenRefs(map: any, defs: Record<string, any>) {
|
function flattenRefs(map: any, defs: Record<string, any>) {
|
||||||
if (!map || typeof map !== 'object') return;
|
if (!map || typeof map !== 'object') return;
|
||||||
|
|
||||||
// 检查并替换 $ref
|
// Check and replace $ref
|
||||||
if (typeof map['$ref'] === 'string') {
|
if (typeof map['$ref'] === 'string') {
|
||||||
const refPath = map['$ref'];
|
const refPath = map['$ref'];
|
||||||
// Parse reference name (e.g. #/$defs/MyType -> MyType)
|
// Parse reference name (e.g. #/$defs/MyType -> MyType)
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
|
const PUBLIC_SUPPORTED_MODELS = [
|
||||||
|
'gemini-3.1-pro-high',
|
||||||
|
'gemini-3.1-pro-low',
|
||||||
|
'gemini-3-flash',
|
||||||
|
'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-opus-4-6-thinking',
|
||||||
|
] as const;
|
||||||
|
|
||||||
const CLAUDE_TO_GEMINI: Record<string, string> = {
|
const CLAUDE_TO_GEMINI: Record<string, string> = {
|
||||||
// Directly supported models
|
// Directly supported models
|
||||||
'claude-opus-4-5-thinking': 'claude-opus-4-5-thinking',
|
'claude-sonnet-4-6-thinking': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-opus-4-6-thinking': 'claude-opus-4-6-thinking',
|
'claude-opus-4-6-thinking': 'claude-opus-4-6-thinking',
|
||||||
'claude-sonnet-4-5': 'claude-sonnet-4-5',
|
'gemini-3.1-pro-low': 'gemini-3.1-pro-low',
|
||||||
'claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking',
|
'gemini-3.1-pro-high': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-3-flash': 'gemini-3-flash',
|
||||||
|
|
||||||
// Alias mappings
|
// Alias mappings
|
||||||
'claude-sonnet-4-5-20250929': 'claude-sonnet-4-5-thinking',
|
'claude-sonnet-4-6': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-3-5-sonnet-20241022': 'claude-sonnet-4-5',
|
'claude-sonnet-4-6-20260219': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-3-5-sonnet-20240620': 'claude-sonnet-4-5',
|
'claude-sonnet-4-5': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-opus-4': 'claude-opus-4-5-thinking',
|
'claude-sonnet-4-5-thinking': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-opus-4-5-20251101': 'claude-opus-4-5-thinking',
|
'claude-sonnet-4-5-20250929': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-3-5-sonnet-20241022': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-3-5-sonnet-20240620': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-opus-4': 'claude-opus-4-6-thinking',
|
||||||
|
'claude-opus-4-5-thinking': 'claude-opus-4-6-thinking',
|
||||||
|
'claude-opus-4-5-20251101': 'claude-opus-4-6-thinking',
|
||||||
'claude-opus-4-6': 'claude-opus-4-6-thinking',
|
'claude-opus-4-6': 'claude-opus-4-6-thinking',
|
||||||
'claude-opus-4-6-20260201': 'claude-opus-4-6-thinking',
|
'claude-opus-4-6-20260201': 'claude-opus-4-6-thinking',
|
||||||
'claude-haiku-4': 'claude-sonnet-4-5',
|
'claude-haiku-4': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-3-haiku-20240307': 'claude-sonnet-4-5',
|
'claude-3-haiku-20240307': 'claude-sonnet-4-6-thinking',
|
||||||
'claude-haiku-4-5-20251001': 'claude-sonnet-4-5',
|
'claude-haiku-4-5-20251001': 'claude-sonnet-4-6-thinking',
|
||||||
|
|
||||||
// OpenAI Protocol Mapping
|
// OpenAI Protocol Mapping
|
||||||
'gpt-4': 'gemini-3-flash',
|
'gpt-4': 'gemini-3-flash',
|
||||||
@@ -42,12 +56,13 @@ const CLAUDE_TO_GEMINI: Record<string, string> = {
|
|||||||
|
|
||||||
// Gemini Protocol Mapping
|
// Gemini Protocol Mapping
|
||||||
'gemini-2.5-flash-lite': 'gemini-3-flash',
|
'gemini-2.5-flash-lite': 'gemini-3-flash',
|
||||||
'gemini-3-pro-low': 'gemini-3-pro-preview',
|
'gemini-3.1-pro-preview': 'gemini-3.1-pro-high',
|
||||||
'gemini-3-pro-high': 'gemini-3-pro-preview',
|
'gemini-3.1-pro': 'gemini-3.1-pro-high',
|
||||||
'gemini-3-pro-preview': 'gemini-3-pro-preview',
|
'gemini-3.0-pro': 'gemini-3.1-pro-high',
|
||||||
'gemini-3-pro': 'gemini-3-pro-preview',
|
|
||||||
'gemini-2.5-flash': 'gemini-3-flash',
|
'gemini-2.5-flash': 'gemini-3-flash',
|
||||||
'gemini-3-flash': 'gemini-3-flash',
|
'gemini-2.5-pro': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-2.0-flash': 'gemini-3-flash',
|
||||||
|
'gemini-2.0-flash-online': 'gemini-3-flash',
|
||||||
'gemini-3-pro-image': 'gemini-3-pro-image',
|
'gemini-3-pro-image': 'gemini-3-pro-image',
|
||||||
'internal-background-task': 'gemini-3-flash',
|
'internal-background-task': 'gemini-3-flash',
|
||||||
};
|
};
|
||||||
@@ -55,17 +70,50 @@ const CLAUDE_TO_GEMINI: Record<string, string> = {
|
|||||||
const DYNAMIC_IMAGE_BASE_MODEL = 'gemini-3-pro-image';
|
const DYNAMIC_IMAGE_BASE_MODEL = 'gemini-3-pro-image';
|
||||||
const DYNAMIC_IMAGE_RESOLUTIONS = ['', '-2k', '-4k'];
|
const DYNAMIC_IMAGE_RESOLUTIONS = ['', '-2k', '-4k'];
|
||||||
const DYNAMIC_IMAGE_RATIOS = ['', '-1x1', '-4x3', '-3x4', '-16x9', '-9x16', '-21x9'];
|
const DYNAMIC_IMAGE_RATIOS = ['', '-1x1', '-4x3', '-3x4', '-16x9', '-9x16', '-21x9'];
|
||||||
const EXTRA_DYNAMIC_MODELS = ['gemini-3-flash', 'gemini-3-pro-high', 'gemini-3-pro-low'];
|
const EXTRA_DYNAMIC_MODELS = [
|
||||||
|
'gemini-3-flash',
|
||||||
|
'gemini-3.1-pro-high',
|
||||||
|
'gemini-3.1-pro-low',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DYNAMIC_MODEL_FORWARDING_RULES = new Map<string, string>();
|
||||||
|
|
||||||
export const MODEL_LIST_CREATED_AT = 1770652800;
|
export const MODEL_LIST_CREATED_AT = 1770652800;
|
||||||
|
|
||||||
export const MODEL_LIST_OWNER = 'antigravity';
|
export const MODEL_LIST_OWNER = 'antigravity';
|
||||||
|
|
||||||
|
const GEMINI_MODEL_ALIASES: Record<string, string> = {
|
||||||
|
'gemini-3.1-pro': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-3.0-pro': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-3.1-pro-preview': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-3-pro-image-preview': 'gemini-3-pro-image',
|
||||||
|
'gemini-3-flash-preview': 'gemini-3-flash',
|
||||||
|
};
|
||||||
|
|
||||||
export function getSupportedModels(): string[] {
|
export function getSupportedModels(): string[] {
|
||||||
return Object.keys(CLAUDE_TO_GEMINI);
|
return [...PUBLIC_SUPPORTED_MODELS];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllDynamicModels(customMapping: Record<string, string> = {}): string[] {
|
export function updateDynamicForwardingRules(oldModel: string, newModel: string): void {
|
||||||
|
const normalizedOld = oldModel.trim();
|
||||||
|
const normalizedNew = newModel.trim();
|
||||||
|
if (!normalizedOld || !normalizedNew) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!DYNAMIC_MODEL_FORWARDING_RULES.has(normalizedOld)) {
|
||||||
|
logger.info(`[Router] Registered dynamic forwarding rule: ${normalizedOld} -> ${normalizedNew}`);
|
||||||
|
}
|
||||||
|
DYNAMIC_MODEL_FORWARDING_RULES.set(normalizedOld, normalizedNew);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDynamicForwardingTarget(modelId: string): string | undefined {
|
||||||
|
return DYNAMIC_MODEL_FORWARDING_RULES.get(modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllDynamicModels(
|
||||||
|
customMapping: Record<string, string> = {},
|
||||||
|
dynamicModelIds?: Iterable<string>,
|
||||||
|
): string[] {
|
||||||
const modelIds = new Set<string>();
|
const modelIds = new Set<string>();
|
||||||
|
|
||||||
for (const modelId of getSupportedModels()) {
|
for (const modelId of getSupportedModels()) {
|
||||||
@@ -86,7 +134,17 @@ export function getAllDynamicModels(customMapping: Record<string, string> = {}):
|
|||||||
modelIds.add(modelId);
|
modelIds.add(modelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...modelIds].filter((id) => !/gemini-[12](\.|$|-)/.test(id)).sort();
|
if (dynamicModelIds) {
|
||||||
|
for (const dynamicModelId of dynamicModelIds) {
|
||||||
|
if (typeof dynamicModelId === 'string' && dynamicModelId.trim() !== '') {
|
||||||
|
modelIds.add(dynamicModelId.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...modelIds]
|
||||||
|
.filter((id) => !shouldHideDeprecatedModelFromList(id))
|
||||||
|
.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapClaudeModelToGemini(input: string): string {
|
export function mapClaudeModelToGemini(input: string): string {
|
||||||
@@ -98,6 +156,11 @@ export function mapClaudeModelToGemini(input: string): string {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeGeminiModelAlias(modelId: string): string {
|
||||||
|
const normalizedModelId = modelId.trim().toLowerCase();
|
||||||
|
return GEMINI_MODEL_ALIASES[normalizedModelId] ?? modelId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core Model Routing Engine
|
* Core Model Routing Engine
|
||||||
* Priority: Custom Mapping (Exact) > Group Mapping (Family) > System Mapping (Built-in Plugin)
|
* Priority: Custom Mapping (Exact) > Group Mapping (Family) > System Mapping (Built-in Plugin)
|
||||||
@@ -108,6 +171,12 @@ export function resolveModelRoute(
|
|||||||
openaiMapping: Record<string, string>,
|
openaiMapping: Record<string, string>,
|
||||||
anthropicMapping: Record<string, string>,
|
anthropicMapping: Record<string, string>,
|
||||||
): string {
|
): string {
|
||||||
|
const dynamicForwarded = getDynamicForwardingTarget(originalModel);
|
||||||
|
if (dynamicForwarded) {
|
||||||
|
logger.info(`[Router] Dynamic deprecated-model forwarding: ${originalModel} -> ${dynamicForwarded}`);
|
||||||
|
return dynamicForwarded;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Check custom exact mapping (Highest priority)
|
// 1. Check custom exact mapping (Highest priority)
|
||||||
if (customMapping[originalModel]) {
|
if (customMapping[originalModel]) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -194,3 +263,31 @@ export function resolveModelRoute(
|
|||||||
// 4. Fall through to system default mapping logic
|
// 4. Fall through to system default mapping logic
|
||||||
return mapClaudeModelToGemini(originalModel);
|
return mapClaudeModelToGemini(originalModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldHideDeprecatedModelFromList(modelId: string): boolean {
|
||||||
|
const normalized = modelId.toLowerCase();
|
||||||
|
|
||||||
|
if (/^gemini-1(\.|$|-)/.test(normalized) || /^gemini-2(\.|$|-)/.test(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'gemini-3.1-pro' || normalized === 'gemini-3.1-pro-preview') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLegacyGeminiPro =
|
||||||
|
/^gemini-3(\.0)?-pro($|-)/.test(normalized) && !normalized.startsWith('gemini-3-pro-image');
|
||||||
|
if (isLegacyGeminiPro) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalized.includes('claude-sonnet-4-5') ||
|
||||||
|
normalized.includes('claude-opus-4-5') ||
|
||||||
|
normalized.includes('claude-haiku-4-5')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import modelSpecsJson from './model-specs';
|
||||||
|
|
||||||
|
type ModelSpec = {
|
||||||
|
max_output_tokens?: number;
|
||||||
|
thinking_budget?: number;
|
||||||
|
is_thinking?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SpecsConfig = {
|
||||||
|
models: Record<string, ModelSpec>;
|
||||||
|
aliases: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65535;
|
||||||
|
const DEFAULT_THINKING_BUDGET = 24576;
|
||||||
|
|
||||||
|
const SPECS = modelSpecsJson as SpecsConfig;
|
||||||
|
|
||||||
|
export function resolveModelAlias(modelId: string): string {
|
||||||
|
const normalized = modelId.trim();
|
||||||
|
return SPECS.aliases[normalized] ?? normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMaxOutputTokens(modelId: string): number {
|
||||||
|
const resolved = resolveModelAlias(modelId);
|
||||||
|
const fromSpec = SPECS.models[resolved]?.max_output_tokens;
|
||||||
|
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec > 0) {
|
||||||
|
return Math.floor(fromSpec);
|
||||||
|
}
|
||||||
|
return DEFAULT_MAX_OUTPUT_TOKENS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThinkingBudget(modelId: string): number {
|
||||||
|
const resolved = resolveModelAlias(modelId);
|
||||||
|
const fromSpec = SPECS.models[resolved]?.thinking_budget;
|
||||||
|
if (typeof fromSpec === 'number' && Number.isFinite(fromSpec) && fromSpec >= 0) {
|
||||||
|
return Math.floor(fromSpec);
|
||||||
|
}
|
||||||
|
return DEFAULT_THINKING_BUDGET;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"models": {
|
||||||
|
"gemini-3-flash": {
|
||||||
|
"max_output_tokens": 65536,
|
||||||
|
"thinking_budget": 32768,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"gemini-3.1-pro-low": {
|
||||||
|
"max_output_tokens": 65536,
|
||||||
|
"thinking_budget": 32768,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"gemini-3.1-pro-high": {
|
||||||
|
"max_output_tokens": 65536,
|
||||||
|
"thinking_budget": 49152,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"gemini-3-pro-image": {
|
||||||
|
"max_output_tokens": 65536,
|
||||||
|
"thinking_budget": 24576,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"claude-sonnet-4-6-thinking": {
|
||||||
|
"max_output_tokens": 64000,
|
||||||
|
"thinking_budget": 32768,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"claude-opus-4-6-thinking": {
|
||||||
|
"max_output_tokens": 64000,
|
||||||
|
"thinking_budget": 32768,
|
||||||
|
"is_thinking": true
|
||||||
|
},
|
||||||
|
"gpt-oss-120b-medium": {
|
||||||
|
"max_output_tokens": 32768,
|
||||||
|
"thinking_budget": 0,
|
||||||
|
"is_thinking": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"gpt-4o": "gemini-3-flash",
|
||||||
|
"gpt-3.5-turbo": "gemini-3-flash",
|
||||||
|
"gemini-2.0-flash": "gemini-3-flash",
|
||||||
|
"gemini-2.5-flash": "gemini-3-flash",
|
||||||
|
"gemini-3.1-pro": "gemini-3.1-pro-high",
|
||||||
|
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
|
||||||
|
"claude-sonnet-4-6": "claude-sonnet-4-6-thinking",
|
||||||
|
"claude-sonnet-4-5": "claude-sonnet-4-6-thinking",
|
||||||
|
"claude-3-5-sonnet": "claude-sonnet-4-6-thinking",
|
||||||
|
"claude-3-7-sonnet": "claude-sonnet-4-6-thinking",
|
||||||
|
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
const modelSpecs = {
|
||||||
|
models: {
|
||||||
|
'gemini-3-flash': {
|
||||||
|
max_output_tokens: 65536,
|
||||||
|
thinking_budget: 32768,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'gemini-3.1-pro-low': {
|
||||||
|
max_output_tokens: 65536,
|
||||||
|
thinking_budget: 32768,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'gemini-3.1-pro-high': {
|
||||||
|
max_output_tokens: 65536,
|
||||||
|
thinking_budget: 49152,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'gemini-3-pro-image': {
|
||||||
|
max_output_tokens: 65536,
|
||||||
|
thinking_budget: 24576,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'claude-sonnet-4-6-thinking': {
|
||||||
|
max_output_tokens: 64000,
|
||||||
|
thinking_budget: 32768,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'claude-opus-4-6-thinking': {
|
||||||
|
max_output_tokens: 64000,
|
||||||
|
thinking_budget: 32768,
|
||||||
|
is_thinking: true,
|
||||||
|
},
|
||||||
|
'gpt-oss-120b-medium': {
|
||||||
|
max_output_tokens: 32768,
|
||||||
|
thinking_budget: 0,
|
||||||
|
is_thinking: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
'gpt-4o': 'gemini-3-flash',
|
||||||
|
'gpt-3.5-turbo': 'gemini-3-flash',
|
||||||
|
'gemini-2.0-flash': 'gemini-3-flash',
|
||||||
|
'gemini-2.5-flash': 'gemini-3-flash',
|
||||||
|
'gemini-3.1-pro': 'gemini-3.1-pro-high',
|
||||||
|
'gemini-3.1-pro-preview': 'gemini-3.1-pro-high',
|
||||||
|
'claude-sonnet-4-6': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-sonnet-4-5': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-3-5-sonnet': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-3-7-sonnet': 'claude-sonnet-4-6-thinking',
|
||||||
|
'claude-opus-4-5-thinking': 'claude-opus-4-6-thinking',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default modelSpecs;
|
||||||
@@ -64,6 +64,7 @@ export interface ClaudeRequest {
|
|||||||
export interface ThinkingConfig {
|
export interface ThinkingConfig {
|
||||||
type: 'enabled' | string;
|
type: 'enabled' | string;
|
||||||
budget_tokens?: number;
|
budget_tokens?: number;
|
||||||
|
effort?: 'low' | 'medium' | 'high' | 'max' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SystemPrompt = string | SystemBlock[];
|
export type SystemPrompt = string | SystemBlock[];
|
||||||
@@ -194,6 +195,8 @@ export interface ThinkingGeminiConfig {
|
|||||||
includeThoughts: boolean;
|
includeThoughts: boolean;
|
||||||
/** Thinking token budget */
|
/** Thinking token budget */
|
||||||
thinkingBudget?: number;
|
thinkingBudget?: number;
|
||||||
|
/** Thinking level for Claude-native adaptive modes */
|
||||||
|
thinkingLevel?: 'low' | 'medium' | 'high' | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,11 +309,22 @@ export interface QuotaData {
|
|||||||
models: Record<string, ModelQuotaInfo>;
|
models: Record<string, ModelQuotaInfo>;
|
||||||
isForbidden: boolean;
|
isForbidden: boolean;
|
||||||
subscriptionTier?: string;
|
subscriptionTier?: string;
|
||||||
|
model_forwarding_rules?: Record<string, string>;
|
||||||
|
is_forbidden?: boolean;
|
||||||
|
subscription_tier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelQuotaInfo {
|
export interface ModelQuotaInfo {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
resetTime: string;
|
resetTime: string;
|
||||||
|
display_name?: string;
|
||||||
|
supports_images?: boolean;
|
||||||
|
supports_thinking?: boolean;
|
||||||
|
thinking_budget?: number;
|
||||||
|
recommended?: boolean;
|
||||||
|
max_tokens?: number;
|
||||||
|
max_output_tokens?: number;
|
||||||
|
supported_mime_types?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadProjectResponse {
|
export interface LoadProjectResponse {
|
||||||
@@ -327,7 +341,21 @@ export interface Tier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface QuotaApiResponse {
|
export interface QuotaApiResponse {
|
||||||
models: Record<string, { quotaInfo?: { remainingFraction?: number; resetTime?: string } }>;
|
models: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
quotaInfo?: { remainingFraction?: number; resetTime?: string };
|
||||||
|
displayName?: string;
|
||||||
|
supportsImages?: boolean;
|
||||||
|
supportsThinking?: boolean;
|
||||||
|
thinkingBudget?: number;
|
||||||
|
recommended?: boolean;
|
||||||
|
maxTokens?: number;
|
||||||
|
maxOutputTokens?: number;
|
||||||
|
supportedMimeTypes?: Record<string, boolean>;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
deprecatedModelIds?: Record<string, { newModelId?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Response Models ---
|
// --- Response Models ---
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ describe('Antigravity Core Features (Business Logic)', () => {
|
|||||||
const requestWithTools = {
|
const requestWithTools = {
|
||||||
...BASE_REQUEST,
|
...BASE_REQUEST,
|
||||||
// Maps to a gemini-3 model potentially
|
// Maps to a gemini-3 model potentially
|
||||||
model: 'gemini-3-pro-preview',
|
model: 'gemini-3.1-pro-preview',
|
||||||
tools: [{ name: 'get_weather' }], // Mock tool structure
|
tools: [{ name: 'get_weather' }], // Mock tool structure
|
||||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||||
} as any;
|
} as any;
|
||||||
@@ -96,7 +96,7 @@ describe('Antigravity Core Features (Business Logic)', () => {
|
|||||||
|
|
||||||
const requestWithTools = {
|
const requestWithTools = {
|
||||||
...BASE_REQUEST,
|
...BASE_REQUEST,
|
||||||
model: 'gemini-3-pro-preview',
|
model: 'gemini-3.1-pro-preview',
|
||||||
tools: [{ name: 'get_weather' }],
|
tools: [{ name: 'get_weather' }],
|
||||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||||
} as any;
|
} as any;
|
||||||
@@ -110,7 +110,7 @@ describe('Antigravity Core Features (Business Logic)', () => {
|
|||||||
it('should KEEP thinking mode if NO function calls exist (pure thinking)', () => {
|
it('should KEEP thinking mode if NO function calls exist (pure thinking)', () => {
|
||||||
const requestPureThinking = {
|
const requestPureThinking = {
|
||||||
...BASE_REQUEST,
|
...BASE_REQUEST,
|
||||||
model: 'gemini-3-pro-preview',
|
model: 'gemini-3.1-pro-preview',
|
||||||
// No tools
|
// No tools
|
||||||
thinking: { type: 'enabled', budget_tokens: 1000 },
|
thinking: { type: 'enabled', budget_tokens: 1000 },
|
||||||
} as any;
|
} as any;
|
||||||
@@ -120,5 +120,60 @@ describe('Antigravity Core Features (Business Logic)', () => {
|
|||||||
// Should keep thinking config
|
// Should keep thinking config
|
||||||
expect(result.request.generationConfig?.thinkingConfig).toBeDefined();
|
expect(result.request.generationConfig?.thinkingConfig).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should auto-enable thinking for gemini-3-flash even without explicit thinking config', () => {
|
||||||
|
const result = transformClaudeRequestIn(
|
||||||
|
{
|
||||||
|
...BASE_REQUEST,
|
||||||
|
model: 'gemini-3-flash',
|
||||||
|
thinking: undefined,
|
||||||
|
} as any,
|
||||||
|
'test-project',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.request.generationConfig?.thinkingConfig).toBeDefined();
|
||||||
|
expect(result.request.generationConfig?.thinkingConfig?.includeThoughts).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should inject flash sentinel thoughtSignature for tool_use without cached signature', () => {
|
||||||
|
const requestWithToolUse = {
|
||||||
|
...BASE_REQUEST,
|
||||||
|
model: 'gemini-3-flash',
|
||||||
|
thinking: undefined,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'call_1',
|
||||||
|
name: 'get_weather',
|
||||||
|
input: { location: 'Beijing' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const result = transformClaudeRequestIn(requestWithToolUse, 'test-project');
|
||||||
|
const part = result.request.contents?.[0]?.parts?.[0] as any;
|
||||||
|
expect(part?.thoughtSignature).toBe('skip_thought_signature_validator');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep mixed tools for gemini 2.x+ when both function and web search are requested', () => {
|
||||||
|
const mixedToolsRequest = {
|
||||||
|
...BASE_REQUEST,
|
||||||
|
model: 'gemini-2.0-flash-online',
|
||||||
|
tools: [{ name: 'get_weather' }, { name: 'web_search' }],
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const result = transformClaudeRequestIn(mixedToolsRequest, 'test-project');
|
||||||
|
const tools = result.request.tools || [];
|
||||||
|
const hasFunctions = tools.some((tool: any) => Array.isArray(tool.functionDeclarations));
|
||||||
|
const hasGoogleSearch = tools.some((tool: any) => tool.googleSearch);
|
||||||
|
|
||||||
|
expect(hasFunctions).toBe(true);
|
||||||
|
expect(hasGoogleSearch).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -8,6 +8,8 @@ const mockTokenManager: any = {
|
|||||||
getNextToken: async () => null,
|
getNextToken: async () => null,
|
||||||
markAsRateLimited: () => undefined,
|
markAsRateLimited: () => undefined,
|
||||||
markAsForbidden: () => undefined,
|
markAsForbidden: () => undefined,
|
||||||
|
markFromUpstreamError: () => undefined,
|
||||||
|
recordParityError: () => undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockGeminiClient: any = {
|
const mockGeminiClient: any = {
|
||||||
@@ -225,7 +227,11 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
|||||||
await realTokenManager.onModuleInit();
|
await realTokenManager.onModuleInit();
|
||||||
|
|
||||||
const tokenManagerProxy = {
|
const tokenManagerProxy = {
|
||||||
getNextToken: async (options?: { sessionKey?: string; excludeAccountIds?: string[] }) => {
|
getNextToken: async (options?: {
|
||||||
|
sessionKey?: string;
|
||||||
|
excludeAccountIds?: string[];
|
||||||
|
model?: string;
|
||||||
|
}) => {
|
||||||
observedGetNextTokenOptions = options ?? null;
|
observedGetNextTokenOptions = options ?? null;
|
||||||
selectedToken = await realTokenManager.getNextToken(options);
|
selectedToken = await realTokenManager.getNextToken(options);
|
||||||
return selectedToken;
|
return selectedToken;
|
||||||
@@ -234,6 +240,14 @@ async function validateRuntimeAnthropicRequestFromRealTokenManager(): Promise<vo
|
|||||||
realTokenManager.markAsRateLimited(accountIdOrEmail),
|
realTokenManager.markAsRateLimited(accountIdOrEmail),
|
||||||
markAsForbidden: (accountIdOrEmail: string) =>
|
markAsForbidden: (accountIdOrEmail: string) =>
|
||||||
realTokenManager.markAsForbidden(accountIdOrEmail),
|
realTokenManager.markAsForbidden(accountIdOrEmail),
|
||||||
|
markFromUpstreamError: (args: {
|
||||||
|
accountIdOrEmail: string;
|
||||||
|
status?: number;
|
||||||
|
retryAfter?: string;
|
||||||
|
body?: string;
|
||||||
|
model?: string;
|
||||||
|
}) => realTokenManager.markFromUpstreamError(args),
|
||||||
|
recordParityError: () => realTokenManager.recordParityError(),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user