diff --git a/VERSION b/VERSION index 10cad67..497a78c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.11.4.2 +2.11.5 diff --git a/src/converters/strategies/CodexConverter.js b/src/converters/strategies/CodexConverter.js index 060ccd9..ddc1666 100644 --- a/src/converters/strategies/CodexConverter.js +++ b/src/converters/strategies/CodexConverter.js @@ -132,7 +132,13 @@ export class CodexConverter extends BaseConverter { // 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色 if (codexRequest.input && Array.isArray(codexRequest.input)) { - codexRequest.input = codexRequest.input.map(item => { + codexRequest.input = codexRequest.input.filter(item => { + // 如果 instructions 已存在,过滤掉 input 中的 system/developer 消息以避免重复 + if (codexRequest.instructions && (item.role === 'system' || item.role === 'developer')) { + return false; + } + return true; + }).map(item => { // 如果没有 type 或者 type 不是 message,则添加 type: "message" if (!item.type || item.type !== 'message') { item = { type: "message", ...item }; @@ -160,7 +166,7 @@ export class CodexConverter extends BaseConverter { const codexRequest = { model: data.model, instructions: this.buildInstructions(data), - input: this.convertMessages(data.messages || []), + input: this.convertMessages((data.messages || []).filter(m => m.role !== 'system' && m.role !== 'developer')), stream: true, store: false, metadata: data.metadata || {}, @@ -193,7 +199,7 @@ export class CodexConverter extends BaseConverter { if (data.input && Array.isArray(data.input) && codexRequest.input.length === 0) { // 如果是 OpenAI Responses 格式的 input for (const item of data.input) { - if (item.type === 'message') { + if (item.type === 'message' && item.role !== 'system' && item.role !== 'developer') { codexRequest.input.push({ type: 'message', role: item.role === 'system' ? 'developer' : item.role, diff --git a/src/converters/utils.js b/src/converters/utils.js index 04e0eba..1b5933c 100644 --- a/src/converters/utils.js +++ b/src/converters/utils.js @@ -191,6 +191,22 @@ export function cleanJsonSchemaProperties(schema) { sanitized[key] = cleanProperties; } else if (key === 'items') { sanitized[key] = cleanJsonSchemaProperties(value); + } else if (key === 'type') { + // Google Gemini API 不支持数组形式的 type (如 ["string", "null"]) + // 必须是单个字符串,且通常需要大写 (STRING, NUMBER, OBJECT, ARRAY, BOOLEAN, INTEGER) + if (Array.isArray(value)) { + // 如果包含 null,设置 nullable 为 true + if (value.includes('null')) { + sanitized.nullable = true; + } + // 取第一个非 null 类型 + const actualType = value.find(t => t !== 'null'); + if (actualType) { + sanitized[key] = actualType.toUpperCase(); + } + } else if (typeof value === 'string') { + sanitized[key] = value.toUpperCase(); + } } else { sanitized[key] = value; } diff --git a/src/core/config-manager.js b/src/core/config-manager.js index 3514f68..364dd99 100644 --- a/src/core/config-manager.js +++ b/src/core/config-manager.js @@ -86,8 +86,10 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP LOG_MAX_FILE_SIZE: 10485760, LOG_MAX_FILES: 10, TLS_SIDECAR_ENABLED: false, // 启用 Go uTLS sidecar(需要编译 tls-sidecar 二进制) + TLS_SIDECAR_ENABLED_PROVIDERS: [], // 启用 TLS Sidecar 的提供商列表 TLS_SIDECAR_PORT: 9090, // sidecar 监听端口 - TLS_SIDECAR_BINARY_PATH: null // 自定义二进制路径(默认自动搜索) + TLS_SIDECAR_BINARY_PATH: null, // 自定义二进制路径(默认自动搜索) + TLS_SIDECAR_PROXY_URL: null // TLS Sidecar 专用的上游代理地址 }; let currentConfig = { ...defaultConfig }; diff --git a/src/providers/claude/claude-core.js b/src/providers/claude/claude-core.js index eae966b..e95e3b5 100644 --- a/src/providers/claude/claude-core.js +++ b/src/providers/claude/claude-core.js @@ -2,8 +2,8 @@ import axios from 'axios'; import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError } from '../../utils/common.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; /** * Claude API Core Service Class. @@ -63,11 +63,15 @@ export class ClaudeApiService { } // 配置自定义代理 - configureAxiosProxy(axiosConfig, this.config, 'claude-custom'); + configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.CLAUDE_CUSTOM); return axios.create(axiosConfig); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.CLAUDE_CUSTOM, this.baseUrl); + } + /** * Generic method to call the Claude API, with retry mechanism. * @param {string} endpoint - API endpoint, e.g., '/messages'. @@ -81,7 +85,13 @@ export class ClaudeApiService { const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { - const response = await this.client.post(endpoint, body); + const axiosConfig = { + method: 'post', + url: endpoint, + data: body + }; + this._applySidecar(axiosConfig); + const response = await this.client.request(axiosConfig); return response.data; } catch (error) { const status = error.response?.status; @@ -140,7 +150,14 @@ export class ClaudeApiService { const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { - const response = await this.client.post(endpoint, { ...body, stream: true }, { responseType: 'stream' }); + const axiosConfig = { + method: 'post', + url: endpoint, + data: { ...body, stream: true }, + responseType: 'stream' + }; + this._applySidecar(axiosConfig); + const response = await this.client.request(axiosConfig); const reader = response.data; let buffer = ''; diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index 79f3f11..1858e15 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -15,7 +15,7 @@ import { processContent as processContentUtil, getContentText as getContentTextUtil } from '../../utils/token-utils.js'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; @@ -487,6 +487,10 @@ export class KiroApiService { this.isInitialized = true; } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.KIRO_API); + } + /** * 加载凭证信息(不执行刷新) */ @@ -684,11 +688,20 @@ async saveCredentialsToFile(filePath, newData) { let response = null; // 使用更短的超时时间进行 token 刷新,避免阻塞其他请求 const refreshConfig = { timeout: KIRO_CONSTANTS.TOKEN_REFRESH_TIMEOUT }; + + const axiosConfig = { + method: 'post', + url: refreshUrl, + data: requestBody, + ...refreshConfig + }; + this._applySidecar(axiosConfig); + if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { - response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody, refreshConfig); + response = await this.axiosSocialRefreshInstance.request(axiosConfig); logger.info('[Kiro Auth] Token refresh social response: ok'); } else { - response = await this.axiosInstance.post(refreshUrl, requestBody, refreshConfig); + response = await this.axiosInstance.request(axiosConfig); logger.info('[Kiro Auth] Token refresh idc response: ok'); } @@ -1484,7 +1497,14 @@ async saveCredentialsToFile(filePath, newData) { // 当 model 以 kiro-amazonq 开头时,使用 amazonQUrl,否则使用 baseUrl const requestUrl = model.startsWith('amazonq') ? this.amazonQUrl : this.baseUrl; - const response = await this.axiosInstance.post(requestUrl, requestData, { headers }); + const axiosConfig = { + method: 'post', + url: requestUrl, + data: requestData, + headers + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response; } catch (error) { const status = error.response?.status; @@ -1980,10 +2000,15 @@ async saveCredentialsToFile(filePath, newData) { let stream = null; try { - const response = await this.axiosInstance.post(requestUrl, requestData, { + const axiosConfig = { + method: 'post', + url: requestUrl, + data: requestData, headers, responseType: 'stream' - }); + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); stream = response.data; let buffer = ''; @@ -2961,8 +2986,15 @@ async saveCredentialsToFile(filePath, newData) { 'Connection': 'close' }; + const axiosConfig = { + method: 'get', + url: fullUrl, + headers + }; + this._applySidecar(axiosConfig); + try { - const response = await this.axiosInstance.get(fullUrl, { headers }); + const response = await this.axiosInstance.request(axiosConfig); logger.info('[Kiro] Usage limits fetched successfully'); return response.data; } catch (error) { diff --git a/src/providers/forward/forward-core.js b/src/providers/forward/forward-core.js index a1206d3..8730ac3 100644 --- a/src/providers/forward/forward-core.js +++ b/src/providers/forward/forward-core.js @@ -2,8 +2,8 @@ import axios from 'axios'; import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError } from '../../utils/common.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; /** * ForwardApiService - A provider that forwards requests to a specified API endpoint. @@ -56,17 +56,27 @@ export class ForwardApiService { axiosConfig.proxy = false; } - configureAxiosProxy(axiosConfig, config, 'forward-custom'); + configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.FORWARD_API); this.axiosInstance = axios.create(axiosConfig); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.FORWARD_API, this.baseUrl); + } + async callApi(endpoint, body, isRetry = false, retryCount = 0) { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; try { - const response = await this.axiosInstance.post(endpoint, body); + const axiosConfig = { + method: 'post', + url: endpoint, + data: body + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response.data; } catch (error) { const status = error.response?.status; @@ -97,9 +107,14 @@ export class ForwardApiService { const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; try { - const response = await this.axiosInstance.post(endpoint, body, { + const axiosConfig = { + method: 'post', + url: endpoint, + data: body, responseType: 'stream' - }); + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); const stream = response.data; let buffer = ''; @@ -176,7 +191,12 @@ export class ForwardApiService { async listModels() { try { - const response = await this.axiosInstance.get('/models'); + const axiosConfig = { + method: 'get', + url: '/models' + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response.data; } catch (error) { logger.error(`Error listing Forward models:`, error.message); @@ -184,4 +204,3 @@ export class ForwardApiService { } } } - diff --git a/src/providers/gemini/antigravity-core.js b/src/providers/gemini/antigravity-core.js index 83aaadb..73e7714 100644 --- a/src/providers/gemini/antigravity-core.js +++ b/src/providers/gemini/antigravity-core.js @@ -10,6 +10,7 @@ import * as os from 'os'; import * as readline from 'readline'; import { v4 as uuidv4 } from 'uuid'; import open from 'open'; +import { configureTLSSidecar } from '../../utils/proxy-utils.js'; import { formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiAntigravityOAuth } from '../../auth/oauth-handlers.js'; @@ -18,20 +19,6 @@ import { cleanJsonSchemaProperties } from '../../converters/utils.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; import { MODEL_PROVIDER } from '../../utils/common.js'; -// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 -const httpAgent = new http.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, -}); -const httpsAgent = new https.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, -}); - // --- Constants --- const CREDENTIALS_DIR = '.antigravity'; const CREDENTIALS_FILE = 'oauth_creds.json'; @@ -57,53 +44,6 @@ const DEFAULT_THINKING_MAX = 100000; // 获取 Antigravity 模型列表 const ANTIGRAVITY_MODELS = getProviderModels(MODEL_PROVIDER.ANTIGRAVITY); -// 模型别名映射 - 别名 -> 真实模型名 -const MODEL_ALIAS_MAP = { - 'gemini-2.5-computer-use-preview-10-2025': 'rev19-uic3-1p', - 'gemini-3-pro-image-preview': 'gemini-3-pro-image', - 'gemini-3-pro-preview': 'gemini-3-pro-high', - 'gemini-3.1-pro-preview': 'gemini-3.1-pro-high', - 'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview', - 'gemini-3-flash-preview': 'gemini-3-flash', - 'gemini-2.5-flash-preview': 'gemini-2.5-flash', - 'gemini-claude-sonnet-4-5': 'claude-sonnet-4-5', - 'gemini-claude-sonnet-4-5-thinking': 'claude-sonnet-4-5-thinking', - 'gemini-claude-opus-4-5-thinking': 'claude-opus-4-5-thinking', - 'gemini-claude-opus-4-6-thinking': 'claude-opus-4-6-thinking' -}; - -// 真实模型名 -> 别名 -const MODEL_NAME_MAP = { - 'rev19-uic3-1p': 'gemini-2.5-computer-use-preview-10-2025', - 'gemini-3-pro-image': 'gemini-3-pro-image-preview', - 'gemini-3-pro-high': 'gemini-3-pro-preview', - 'gemini-3.1-pro-high': 'gemini-3.1-pro-preview', - 'gemini-3.1-flash-lite-preview': 'gemini-3.1-flash-lite-preview', - 'gemini-3-flash': 'gemini-3-flash-preview', - 'gemini-2.5-flash': 'gemini-2.5-flash-preview', - 'claude-sonnet-4-5': 'gemini-claude-sonnet-4-5', - 'claude-sonnet-4-5-thinking': 'gemini-claude-sonnet-4-5-thinking', - 'claude-opus-4-5-thinking': 'gemini-claude-opus-4-5-thinking', - 'claude-opus-4-6-thinking': 'gemini-claude-opus-4-6-thinking' -}; - -/** - * 将别名转换为真实模型名 - * @param {string} modelName - 模型别名 - * @returns {string} 真实模型名 - */ -function alias2ModelName(modelName) { - return MODEL_ALIAS_MAP[modelName]; -} - -/** - * 将真实模型名转换为别名 - * @param {string} modelName - 真实模型名 - * @returns {string|null} 模型别名,如果不支持则返回 null - */ -function modelName2Alias(modelName) { - return MODEL_NAME_MAP[modelName]; -} /** * 检查模型是否为 Claude 模型 @@ -145,6 +85,14 @@ function generateRequestID() { return 'agent-' + uuidv4(); } +/** + * 生成随机图像生成请求ID + * @returns {string} + */ +function generateImageGenRequestID() { + return `image_gen/${Date.now()}/${uuidv4()}/12`; +} + /** * 生成随机会话ID * @returns {string} @@ -165,7 +113,7 @@ function generateStableSessionID(payload) { const contents = payload?.request?.contents; if (Array.isArray(contents)) { for (const content of contents) { - if (content.role === 'user') { + if (content && content.role === 'user' && Array.isArray(content.parts)) { const text = content.parts?.[0]?.text; if (text) { const hash = crypto.createHash('sha256').update(text).digest(); @@ -272,33 +220,44 @@ function geminiToAntigravity(modelName, payload, projectId) { let template = JSON.parse(JSON.stringify(payload)); const isClaudeModel = isClaude(modelName); + const isImgModel = isImageModel(modelName); // 设置基本字段 template.model = modelName; template.userAgent = 'antigravity'; - template.requestType = 'agent'; + + // 设置请求类型 + template.requestType = isImgModel ? 'image_gen' : 'agent'; + template.project = projectId || generateProjectID(); - template.requestId = generateRequestID(); - // 确保 request 对象存在 - if (!template.request) { - template.request = {}; + // 设置请求ID和会话ID + if (isImgModel) { + template.requestId = generateImageGenRequestID(); + } else { + template.requestId = generateRequestID(); + // 确保 request 对象存在 + if (!template.request) { + template.request = {}; + } + // 设置会话ID - 使用稳定的会话ID + template.request.sessionId = generateStableSessionID(template); } - // 设置会话ID - 使用稳定的会话ID - template.request.sessionId = generateStableSessionID(template); - // 删除安全设置 if (template.request.safetySettings) { delete template.request.safetySettings; } // 设置工具配置 + // 如果根部有 toolConfig,且 request 内部没有,则移动进去 if (template.request.toolConfig) { if (!template.request.toolConfig.functionCallingConfig) { template.request.toolConfig.functionCallingConfig = {}; } - template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED'; + if (isClaudeModel) { + template.request.toolConfig.functionCallingConfig.mode = 'VALIDATED'; + } } // 当模型是 Claude 时,禁止使用 tools @@ -331,22 +290,22 @@ function geminiToAntigravity(modelName, payload, projectId) { } // 清理所有工具声明中的 JSON Schema 属性(移除 Google API 不支持的属性如 exclusiveMinimum 等) - if (template.request.tools && Array.isArray(template.request.tools)) { + if (template.request.tools && Array.isArray(template.request.tools)) { template.request.tools.forEach((tool) => { - if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) { + if (tool.functionDeclarations && Array.isArray(tool.functionDeclarations)) { tool.functionDeclarations.forEach((funcDecl) => { // 对于 Claude 模型,处理 parametersJsonSchema if (isClaudeModel && funcDecl.parametersJsonSchema) { funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parametersJsonSchema); - delete funcDecl.parameters.$schema; - delete funcDecl.parametersJsonSchema; + delete funcDecl.parameters.$schema; + delete funcDecl.parametersJsonSchema; } else if (funcDecl.parameters) { funcDecl.parameters = cleanJsonSchemaProperties(funcDecl.parameters); - } - }); - } - }); - } + } + }); + } + }); + } // 如果是图像模型,增加参数 "generationConfig.imageConfig.imageSize": "4K" if (isImageModel(modelName)) { @@ -712,6 +671,20 @@ function ensureRolesInContents(requestBody, modelName) { export class AntigravityApiService { constructor(config) { + // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 + this.httpAgent = new http.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + this.httpsAgent = new https.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + // 检查是否需要使用代理 const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-antigravity'); @@ -726,7 +699,7 @@ export class AntigravityApiService { logger.info('[Antigravity] Using proxy for OAuth2Client'); } else { oauth2Options.transporterOptions = { - agent: httpsAgent, + agent: this.httpsAgent, }; } @@ -748,6 +721,10 @@ export class AntigravityApiService { this.proxyConfig = getProxyConfigForProvider(config, 'gemini-antigravity'); } + _applySidecar(requestOptions) { + return configureTLSSidecar(requestOptions, this.config, MODEL_PROVIDER.ANTIGRAVITY); + } + /** * 获取 Base URL 降级顺序 * @param {Object} config - 配置对象 @@ -1025,9 +1002,9 @@ export class AntigravityApiService { if (res.data && res.data.models) { const models = Object.keys(res.data.models); this.availableModels = models - .map(modelName2Alias) .filter(alias => alias !== undefined && alias !== '' && alias !== null) - .filter(alias => ANTIGRAVITY_MODELS.includes(alias)); + .filter(alias => ANTIGRAVITY_MODELS.includes(alias) || alias.startsWith('claude-')) + .map(alias => alias.startsWith('claude-') ? `gemini-${alias}` : alias); logger.info(`[Antigravity] Available models: [${this.availableModels.join(', ')}]`); return; @@ -1101,6 +1078,7 @@ export class AntigravityApiService { body: JSON.stringify(body) }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { @@ -1194,6 +1172,7 @@ export class AntigravityApiService { body: JSON.stringify(body) }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); if (res.status !== 200) { @@ -1337,7 +1316,9 @@ export class AntigravityApiService { selectedModel = this.availableModels[0]; } - const actualModelName = alias2ModelName(selectedModel); + // 移除 gemini- 前缀以获取实际模型名称(针对 claude 模型) + const actualModelName = selectedModel.startsWith('gemini-claude-') ? selectedModel.replace('gemini-claude-', 'claude-') : selectedModel; + logger.info(`[Antigravity] Selected model: ${actualModelName}`); // 深拷贝请求体 const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)), actualModelName); const isClaudeModel = isClaude(actualModelName); @@ -1413,7 +1394,9 @@ export class AntigravityApiService { selectedModel = this.availableModels[0]; } - const actualModelName = alias2ModelName(selectedModel); + // 移除 gemini- 前缀以获取实际模型名称(针对 claude 模型) + const actualModelName = selectedModel.startsWith('gemini-claude-') ? selectedModel.replace('gemini-claude-', 'claude-') : selectedModel; + logger.info(`[Antigravity] Selected model: ${actualModelName}`); // 深拷贝请求体 const processedRequestBody = ensureRolesInContents(JSON.parse(JSON.stringify(requestBody)), actualModelName); @@ -1491,6 +1474,7 @@ export class AntigravityApiService { body: JSON.stringify({ project: this.projectId }) }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); // logger.info(`[Antigravity] fetchAvailableModels success: ${JSON.stringify(res.data)}`); if (res.data) { diff --git a/src/providers/gemini/gemini-core.js b/src/providers/gemini/gemini-core.js index 05992dc..aa5690a 100644 --- a/src/providers/gemini/gemini-core.js +++ b/src/providers/gemini/gemini-core.js @@ -7,6 +7,7 @@ import * as path from 'path'; import * as os from 'os'; import * as readline from 'readline'; import open from 'open'; +import { configureTLSSidecar } from '../../utils/proxy-utils.js'; import { API_ACTIONS, formatExpiryTime, isRetryableNetworkError, formatExpiryLog } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; import { handleGeminiCliOAuth } from '../../auth/oauth-handlers.js'; @@ -14,20 +15,6 @@ import { getProxyConfigForProvider, getGoogleAuthProxyConfig } from '../../utils import { getProviderPoolManager } from '../../services/service-manager.js'; import { MODEL_PROVIDER } from '../../utils/common.js'; -// 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 -const httpAgent = new http.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, -}); -const httpsAgent = new https.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, -}); - // --- Constants --- const AUTH_REDIRECT_PORT = 8085; const CREDENTIALS_DIR = '.gemini'; @@ -38,6 +25,67 @@ const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.goog const OAUTH_CLIENT_SECRET = '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'; const GEMINI_MODELS = getProviderModels(MODEL_PROVIDER.GEMINI_CLI); const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`); +const GEMINI_CLI_VERSION = '0.31.0'; +const GEMINI_CLI_API_CLIENT_HEADER = 'google-genai-sdk/1.41.0 gl-node/v22.19.0'; + +/** + * 设置 Gemini CLI 所需的特定请求头 + * @param {Object} headers - 请求头对象 + * @param {string} model - 模型名称 + */ +function applyGeminiCLIHeaders(headers, model) { + const platform = os.platform(); + let arch = os.arch(); + if (arch === 'ia32') arch = 'x86'; + const modelName = model || 'unknown'; + if (model !== 'load-code-assist' && model !== 'onboard-user') { + headers['User-Agent'] = `GeminiCLI/${GEMINI_CLI_VERSION}/${modelName} (${platform}; ${arch})`; + } + headers['X-Goog-Api-Client'] = GEMINI_CLI_API_CLIENT_HEADER; +} + + +/** + * 从 Google API 的 429 错误响应中提取重试延迟 + * @param {Object|string} errorBody - 错误响应体 + * @returns {number|null} 延迟毫秒数 + */ +function parseRetryDelay(errorBody) { + try { + const data = typeof errorBody === 'string' ? JSON.parse(errorBody) : errorBody; + const details = data?.error?.details; + if (Array.isArray(details)) { + for (const detail of details) { + if (detail['@type'] === 'type.googleapis.com/google.rpc.RetryInfo') { + const retryDelay = detail.retryDelay; + if (retryDelay) { + const match = retryDelay.match(/^([\d.]+)s$/); + if (match) return parseFloat(match[1]) * 1000; + } + } + } + for (const detail of details) { + if (detail['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo') { + const quotaResetDelay = detail.metadata?.quotaResetDelay; + if (quotaResetDelay) { + const match = quotaResetDelay.match(/^([\d.]+)(ms|s)$/); + if (match) { + let ms = parseFloat(match[1]); + if (match[2] === 's') ms *= 1000; + return ms; + } + } + } + } + } + const message = data?.error?.message; + if (message) { + const match = message.match(/after\s+(\d+)s\.?/); + if (match) return parseInt(match[1]) * 1000; + } + } catch (e) {} + return null; +} function is_anti_truncation_model(model) { return ANTI_TRUNCATION_MODELS.some(antiModel => model.includes(antiModel)); @@ -134,7 +182,7 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) { project: service.projectId, request: currentRequest }; - const stream = service.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest); + const stream = service.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, model); let lastChunk = null; let hasContent = false; @@ -155,9 +203,9 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) { lastChunk.candidates[0].finishReason === 'MAX_TOKENS') { // 提取已生成的文本内容 - if (lastChunk.candidates[0].content && lastChunk.candidates[0].content.parts) { + if (lastChunk.candidates[0].content && Array.isArray(lastChunk.candidates[0].content.parts)) { const generatedParts = lastChunk.candidates[0].content.parts - .filter(part => part.text) + .filter(part => part?.text) .map(part => part.text); if (generatedParts.length > 0) { @@ -197,6 +245,20 @@ async function* apply_anti_truncation_to_stream(service, model, requestBody) { export class GeminiApiService { constructor(config) { + // 配置 HTTP/HTTPS agent 限制连接池大小,避免资源泄漏 + this.httpAgent = new http.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + this.httpsAgent = new https.Agent({ + keepAlive: true, + maxSockets: 100, + maxFreeSockets: 5, + timeout: 120000, + }); + // 检查是否需要使用代理 const proxyConfig = getGoogleAuthProxyConfig(config, 'gemini-cli-oauth'); @@ -211,7 +273,7 @@ export class GeminiApiService { logger.info('[Gemini] Using proxy for OAuth2Client'); } else { oauth2Options.transporterOptions = { - agent: httpsAgent, + agent: this.httpsAgent, }; } @@ -253,6 +315,10 @@ export class GeminiApiService { logger.info(`[Gemini] Initialization complete. Project ID: ${this.projectId}`); } + _applySidecar(requestOptions) { + return configureTLSSidecar(requestOptions, this.config, MODEL_PROVIDER.GEMINI_CLI); + } + /** * 加载凭证信息(不执行刷新) */ @@ -412,7 +478,7 @@ export class GeminiApiService { metadata: clientMetadata, } - const loadResponse = await this.callApi('loadCodeAssist', loadRequest); + const loadResponse = await this.callApi('loadCodeAssist', loadRequest, false, 0, 'load-code-assist'); // Check if we already have a project ID from the response if (loadResponse.cloudaicompanionProject) { @@ -429,7 +495,7 @@ export class GeminiApiService { metadata: clientMetadata, }; - let lroResponse = await this.callApi('onboardUser', onboardRequest); + let lroResponse = await this.callApi('onboardUser', onboardRequest, false, 0, 'onboard-user'); // Poll until operation is complete with timeout protection const MAX_RETRIES = 30; // Maximum number of retries (60 seconds total) @@ -437,7 +503,7 @@ export class GeminiApiService { while (!lroResponse.done && retryCount < MAX_RETRIES) { await new Promise(resolve => setTimeout(resolve, 2000)); - lroResponse = await this.callApi('onboardUser', onboardRequest); + lroResponse = await this.callApi('onboardUser', onboardRequest, false, 0, 'onboard-user'); retryCount++; } @@ -467,18 +533,22 @@ export class GeminiApiService { return { models: formattedModels }; } - async callApi(method, body, isRetry = false, retryCount = 0) { + async callApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { + const headers = { "Content-Type": "application/json" }; + applyGeminiCLIHeaders(headers, model); + const requestOptions = { url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`, method: "POST", - headers: { "Content-Type": "application/json" }, + headers: headers, responseType: "json", body: JSON.stringify(body), }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); return res.data; } catch (error) { @@ -513,10 +583,10 @@ export class GeminiApiService { // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); + const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount)); logger.info(`[Gemini API] Received 429 (Too Many Requests). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - return this.callApi(method, body, isRetry, retryCount + 1); + return this.callApi(method, body, isRetry, retryCount + 1, model); } // Handle other retryable errors (5xx server errors) @@ -524,7 +594,7 @@ export class GeminiApiService { const delay = baseDelay * Math.pow(2, retryCount); logger.info(`[Gemini API] Received ${status} server error. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - return this.callApi(method, body, isRetry, retryCount + 1); + return this.callApi(method, body, isRetry, retryCount + 1, model); } // Handle network errors (ECONNRESET, ETIMEDOUT, etc.) with exponential backoff @@ -533,26 +603,30 @@ export class GeminiApiService { const errorIdentifier = errorCode || errorMessage.substring(0, 50); logger.info(`[Gemini API] Network error (${errorIdentifier}). Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - return this.callApi(method, body, isRetry, retryCount + 1); + return this.callApi(method, body, isRetry, retryCount + 1, model); } throw error; } } - async * streamApi(method, body, isRetry = false, retryCount = 0) { + async * streamApi(method, body, isRetry = false, retryCount = 0, model = 'unknown') { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { + const headers = { "Content-Type": "application/json" }; + applyGeminiCLIHeaders(headers, model); + const requestOptions = { url: `${this.codeAssistEndpoint}/${this.apiVersion}:${method}`, method: "POST", params: { alt: "sse" }, - headers: { "Content-Type": "application/json" }, + headers: headers, responseType: "stream", body: JSON.stringify(body), }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); if (res.status !== 200) { let errorBody = ''; @@ -592,10 +666,10 @@ export class GeminiApiService { // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); + const delay = parseRetryDelay(error.response?.data) || (baseDelay * Math.pow(2, retryCount)); logger.info(`[Gemini API] Received 429 (Too Many Requests) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - yield* this.streamApi(method, body, isRetry, retryCount + 1); + yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } @@ -604,7 +678,7 @@ export class GeminiApiService { const delay = baseDelay * Math.pow(2, retryCount); logger.info(`[Gemini API] Received ${status} server error during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - yield* this.streamApi(method, body, isRetry, retryCount + 1); + yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } @@ -614,7 +688,7 @@ export class GeminiApiService { const errorIdentifier = errorCode || errorMessage.substring(0, 50); logger.info(`[Gemini API] Network error (${errorIdentifier}) during stream. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); - yield* this.streamApi(method, body, isRetry, retryCount + 1); + yield* this.streamApi(method, body, isRetry, retryCount + 1, model); return; } @@ -660,14 +734,16 @@ export class GeminiApiService { } } - let selectedModel = model; + let baseModel = model; if (!GEMINI_MODELS.includes(model)) { logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); - selectedModel = GEMINI_MODELS[0]; + baseModel = GEMINI_MODELS[0]; } - const processedRequestBody = ensureRolesInContents(requestBody); - const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody }; - const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest); + + const processedRequestBody = ensureRolesInContents({ ...requestBody }); + const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody }; + + const response = await this.callApi(API_ACTIONS.GENERATE_CONTENT, apiRequest, false, 0, baseModel); return toGeminiApiResponse(response.response); } @@ -699,21 +775,23 @@ export class GeminiApiService { // 从防截断模型名中提取实际模型名 const actualModel = extract_model_from_anti_model(model); // 使用防截断流处理 - const processedRequestBody = ensureRolesInContents(requestBody); + const processedRequestBody = ensureRolesInContents({ ...requestBody }); yield* apply_anti_truncation_to_stream(this, actualModel, processedRequestBody); - } else { - // 正常流处理 - let selectedModel = model; - if (!GEMINI_MODELS.includes(model)) { - logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); - selectedModel = GEMINI_MODELS[0]; - } - const processedRequestBody = ensureRolesInContents(requestBody); - const apiRequest = { model: selectedModel, project: this.projectId, request: processedRequestBody }; - const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest); - for await (const chunk of stream) { - yield toGeminiApiResponse(chunk.response); - } + return; + } + + let baseModel = model; + if (!GEMINI_MODELS.includes(model)) { + logger.warn(`[Gemini] Model '${model}' not found. Using default model: '${GEMINI_MODELS[0]}'`); + baseModel = GEMINI_MODELS[0]; + } + + const processedRequestBody = ensureRolesInContents({ ...requestBody }); + const apiRequest = { model: baseModel, project: this.projectId, request: processedRequestBody }; + + const stream = this.streamApi(API_ACTIONS.STREAM_GENERATE_CONTENT, apiRequest, false, 0, baseModel); + for await (const chunk of stream) { + yield toGeminiApiResponse(chunk.response); } } @@ -799,6 +877,7 @@ export class GeminiApiService { body: JSON.stringify(requestBody) }; + this._applySidecar(requestOptions); const res = await this.authClient.request(requestOptions); // logger.info(`[Gemini] retrieveUserQuota success`, JSON.stringify(res.data)); if (res.data && res.data.buckets) { diff --git a/src/providers/grok/grok-core.js b/src/providers/grok/grok-core.js index 7dad484..50c6433 100644 --- a/src/providers/grok/grok-core.js +++ b/src/providers/grok/grok-core.js @@ -5,8 +5,7 @@ import * as https from 'https'; import { v4 as uuidv4 } from 'uuid'; import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js'; import { getProviderModels } from '../provider-models.js'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { getTLSSidecar } from '../../utils/tls-sidecar.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; import { MODEL_PROVIDER } from '../../utils/common.js'; import { ConverterFactory } from '../../converters/ConverterFactory.js'; import * as readline from 'readline'; @@ -145,12 +144,7 @@ export class GrokApiService { } _applySidecar(axiosConfig) { - const sidecar = getTLSSidecar(); - if (sidecar.isReady()) { - const proxyUrl = this.config.PROXY_URL && this.config.PROXY_ENABLED_PROVIDERS?.includes(MODEL_PROVIDER.GROK_CUSTOM) ? this.config.PROXY_URL : null; - sidecar.wrapAxiosConfig(axiosConfig, proxyUrl); - } - return axiosConfig; + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM); } async initialize() { diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index a29b67d..3eceb68 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -6,6 +6,7 @@ import path from 'path'; import os from 'os'; import { refreshCodexTokensWithRetry } from '../../auth/oauth-handlers.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; +import { configureTLSSidecar } from '../../utils/proxy-utils.js'; import { MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProxyConfigForProvider } from '../../utils/proxy-utils.js'; import { getProviderModels } from '../provider-models.js'; @@ -38,6 +39,10 @@ export class CodexApiService { this.startCacheCleanup(); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.CODEX_API, this.baseUrl); + } + /** * 初始化服务(加载凭据) */ @@ -196,7 +201,15 @@ export class CodexApiService { config.httpsAgent = proxyConfig.httpsAgent; } - const response = await axios.post(url, body, config); + const axiosRequestConfig = { + method: 'post', + url, + data: body, + ...config + }; + this._applySidecar(axiosRequestConfig); + + const response = await axios.request(axiosRequestConfig); return this.parseNonStreamResponse(response.data); } catch (error) { @@ -265,7 +278,15 @@ export class CodexApiService { config.httpsAgent = proxyConfig.httpsAgent; } - const response = await axios.post(url, body, config); + const axiosRequestConfig = { + method: 'post', + url, + data: body, + ...config + }; + this._applySidecar(axiosRequestConfig); + + const response = await axios.request(axiosRequestConfig); yield* this.parseSSEStream(response.data); } catch (error) { @@ -673,7 +694,14 @@ export class CodexApiService { config.httpsAgent = proxyConfig.httpsAgent; } - const response = await axios.get(url, config); + const axiosRequestConfig = { + method: 'get', + url, + ...config + }; + this._applySidecar(axiosRequestConfig); + + const response = await axios.request(axiosRequestConfig); // 解析响应数据并转换为通用格式 const data = response.data; diff --git a/src/providers/openai/openai-core.js b/src/providers/openai/openai-core.js index ca22489..6fe800a 100644 --- a/src/providers/openai/openai-core.js +++ b/src/providers/openai/openai-core.js @@ -2,8 +2,8 @@ import axios from 'axios'; import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError } from '../../utils/common.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; +import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; // Assumed OpenAI API specification service for interacting with third-party models export class OpenAIApiService { @@ -47,17 +47,27 @@ export class OpenAIApiService { } // 配置自定义代理 - configureAxiosProxy(axiosConfig, config, 'openai-custom'); + configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.OPENAI_CUSTOM); this.axiosInstance = axios.create(axiosConfig); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.OPENAI_CUSTOM, this.baseUrl); + } + async callApi(endpoint, body, isRetry = false, retryCount = 0) { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { - const response = await this.axiosInstance.post(endpoint, body); + const axiosConfig = { + method: 'post', + url: endpoint, + data: body + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response.data; } catch (error) { const status = error.response?.status; @@ -111,9 +121,14 @@ export class OpenAIApiService { const streamRequestBody = { ...body, stream: true }; try { - const response = await this.axiosInstance.post(endpoint, streamRequestBody, { + const axiosConfig = { + method: 'post', + url: endpoint, + data: streamRequestBody, responseType: 'stream' - }); + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); const stream = response.data; let buffer = ''; diff --git a/src/providers/openai/openai-responses-core.js b/src/providers/openai/openai-responses-core.js index d1992bf..d12cbad 100644 --- a/src/providers/openai/openai-responses-core.js +++ b/src/providers/openai/openai-responses-core.js @@ -2,7 +2,8 @@ import axios from 'axios'; import logger from '../../utils/logger.js'; import * as http from 'http'; import * as https from 'https'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; +import { MODEL_PROVIDER } from '../../utils/common.js'; // OpenAI Responses API specification service for interacting with third-party models export class OpenAIResponsesApiService { @@ -46,17 +47,27 @@ export class OpenAIResponsesApiService { } // 配置自定义代理 (使用 openai-custom 的代理配置) - configureAxiosProxy(axiosConfig, config, 'openai-custom'); + configureAxiosProxy(axiosConfig, config, MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES); this.axiosInstance = axios.create(axiosConfig); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES, this.baseUrl); + } + async callApi(endpoint, body, isRetry = false, retryCount = 0) { const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay try { - const response = await this.axiosInstance.post(endpoint, body); + const axiosConfig = { + method: 'post', + url: endpoint, + data: body + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response.data; } catch (error) { const status = error.response?.status; @@ -95,9 +106,14 @@ export class OpenAIResponsesApiService { const streamRequestBody = { ...body, stream: true }; try { - const response = await this.axiosInstance.post(endpoint, streamRequestBody, { + const axiosConfig = { + method: 'post', + url: endpoint, + data: streamRequestBody, responseType: 'stream' - }); + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); const stream = response.data; let buffer = ''; @@ -184,7 +200,12 @@ export class OpenAIResponsesApiService { async listModels() { try { - const response = await this.axiosInstance.get('/models'); + const axiosConfig = { + method: 'get', + url: '/models' + }; + this._applySidecar(axiosConfig); + const response = await this.axiosInstance.request(axiosConfig); return response.data; } catch (error) { const status = error.response?.status; diff --git a/src/providers/openai/qwen-core.js b/src/providers/openai/qwen-core.js index 59c8195..8d31638 100644 --- a/src/providers/openai/qwen-core.js +++ b/src/providers/openai/qwen-core.js @@ -11,7 +11,7 @@ import { EventEmitter } from 'events'; import { randomUUID } from 'node:crypto'; import { getProviderModels } from '../provider-models.js'; import { handleQwenOAuth } from '../../auth/oauth-handlers.js'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; +import { configureAxiosProxy, configureTLSSidecar } from '../../utils/proxy-utils.js'; import { isRetryableNetworkError, MODEL_PROVIDER, formatExpiryLog } from '../../utils/common.js'; import { getProviderPoolManager } from '../../services/service-manager.js'; @@ -238,6 +238,10 @@ export class QwenApiService { logger.info('[Qwen] Initialization complete.'); } + _applySidecar(axiosConfig) { + return configureTLSSidecar(axiosConfig, this.config, MODEL_PROVIDER.QWEN_API, this.baseUrl); + } + /** * 加载凭证信息(不执行刷新) */ @@ -597,8 +601,16 @@ export class QwenApiService { const mergedTools = processedBody.tools ? [...defaultTools, ...processedBody.tools] : defaultTools; const requestBody = isStream ? { ...processedBody, stream: true, tools: mergedTools } : { ...processedBody, tools: mergedTools }; - const options = isStream ? { responseType: 'stream' } : {}; - const response = await this.currentAxiosInstance.post(endpoint, requestBody, options); + + const axiosRequestConfig = { + method: 'post', + url: endpoint, + data: requestBody, + ...(isStream ? { responseType: 'stream' } : {}) + }; + this._applySidecar(axiosRequestConfig); + + const response = await this.currentAxiosInstance.request(axiosRequestConfig); return response.data; } catch (error) { diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js index b0d9606..53b871e 100644 --- a/src/providers/provider-models.js +++ b/src/providers/provider-models.js @@ -16,17 +16,13 @@ export const PROVIDER_MODELS = { 'gemini-3.1-flash-lite-preview', ], 'gemini-antigravity': [ - 'gemini-2.5-computer-use-preview-10-2025', - 'gemini-3-pro-image-preview', - 'gemini-3.1-pro-preview', - 'gemini-3.1-flash-lite-preview', - 'gemini-3-pro-preview', - 'gemini-3-flash-preview', - 'gemini-2.5-flash-preview', - 'gemini-claude-sonnet-4-5', - 'gemini-claude-sonnet-4-5-thinking', - 'gemini-claude-opus-4-5-thinking', - 'gemini-claude-opus-4-6-thinking' + 'gemini-3-flash', + 'gemini-3.1-pro-high', + 'gemini-3.1-pro-low', + 'gemini-3.1-flash-image', + 'gemini-3-flash-agent', + 'gemini-claude-sonnet-4-6', + 'gemini-claude-opus-4-6-thinking', ], 'claude-custom': [], 'claude-kiro-oauth': [ diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index d37750e..eb43add 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -101,7 +101,9 @@ export async function handleUpdateConfig(req, res, currentConfig) { // TLS Sidecar settings if (newConfig.TLS_SIDECAR_ENABLED !== undefined) currentConfig.TLS_SIDECAR_ENABLED = newConfig.TLS_SIDECAR_ENABLED; + if (newConfig.TLS_SIDECAR_ENABLED_PROVIDERS !== undefined) currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS = newConfig.TLS_SIDECAR_ENABLED_PROVIDERS; if (newConfig.TLS_SIDECAR_PORT !== undefined) currentConfig.TLS_SIDECAR_PORT = newConfig.TLS_SIDECAR_PORT; + if (newConfig.TLS_SIDECAR_PROXY_URL !== undefined) currentConfig.TLS_SIDECAR_PROXY_URL = newConfig.TLS_SIDECAR_PROXY_URL; // Log settings if (newConfig.LOG_ENABLED !== undefined) currentConfig.LOG_ENABLED = newConfig.LOG_ENABLED; @@ -171,7 +173,9 @@ export async function handleUpdateConfig(req, res, currentConfig) { LOG_MAX_FILE_SIZE: currentConfig.LOG_MAX_FILE_SIZE, LOG_MAX_FILES: currentConfig.LOG_MAX_FILES, TLS_SIDECAR_ENABLED: currentConfig.TLS_SIDECAR_ENABLED, - TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT + TLS_SIDECAR_ENABLED_PROVIDERS: currentConfig.TLS_SIDECAR_ENABLED_PROVIDERS, + TLS_SIDECAR_PORT: currentConfig.TLS_SIDECAR_PORT, + TLS_SIDECAR_PROXY_URL: currentConfig.TLS_SIDECAR_PROXY_URL }; writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8'); diff --git a/src/utils/proxy-utils.js b/src/utils/proxy-utils.js index c173d38..45bd9ec 100644 --- a/src/utils/proxy-utils.js +++ b/src/utils/proxy-utils.js @@ -7,6 +7,7 @@ import { HttpsProxyAgent } from 'https-proxy-agent'; import logger from './logger.js'; import { HttpProxyAgent } from 'http-proxy-agent'; import { SocksProxyAgent } from 'socks-proxy-agent'; +import { getTLSSidecar } from './tls-sidecar.js'; /** * 解析代理URL并返回相应的代理配置 @@ -110,6 +111,52 @@ export function configureAxiosProxy(axiosConfig, config, providerType) { return axiosConfig; } +/** + * 检查指定的提供商是否启用了 TLS Sidecar + * @param {Object} config - 配置对象 + * @param {string} providerType - 提供商类型 + * @returns {boolean} 是否启用 TLS Sidecar + */ +export function isTLSSidecarEnabledForProvider(config, providerType) { + if (!config || !config.TLS_SIDECAR_ENABLED || !config.TLS_SIDECAR_ENABLED_PROVIDERS) { + return false; + } + + const enabledProviders = config.TLS_SIDECAR_ENABLED_PROVIDERS; + if (!Array.isArray(enabledProviders)) { + return false; + } + + return enabledProviders.includes(providerType); +} + +/** + * 为 axios 配置 TLS Sidecar + * @param {Object} axiosConfig - axios 配置对象 + * @param {Object} config - 应用配置对象 + * @param {string} providerType - 提供商类型 + * @param {string} [defaultBaseUrl] - 默认基础 URL(用于处理相对路径) + * @returns {Object} 更新后的 axios 配置 + */ +export function configureTLSSidecar(axiosConfig, config, providerType, defaultBaseUrl = null) { + const sidecar = getTLSSidecar(); + if (sidecar.isReady() && isTLSSidecarEnabledForProvider(config, providerType)) { + const proxyUrl = config.TLS_SIDECAR_PROXY_URL || null; + + // 处理相对路径 + if (axiosConfig.url && !axiosConfig.url.startsWith('http')) { + const baseUrl = (axiosConfig.baseURL || defaultBaseUrl || '').replace(/\/$/, ''); + if (baseUrl) { + const path = axiosConfig.url.startsWith('/') ? axiosConfig.url : '/' + axiosConfig.url; + axiosConfig.url = baseUrl + path; + } + } + + sidecar.wrapAxiosConfig(axiosConfig, proxyUrl); + } + return axiosConfig; +} + /** * 为 google-auth-library 配置代理 * @param {Object} config - 应用配置对象 diff --git a/static/app/config-manager.js b/static/app/config-manager.js index b214396..68e99cc 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -26,6 +26,12 @@ function updateConfigProviderConfigs(configs) { if (proxyProvidersEl) { renderProviderTags(proxyProvidersEl, configs, false); } + + // 渲染 TLS Sidecar 设置中的提供商选择 + const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders'); + if (tlsSidecarProvidersEl) { + renderProviderTags(tlsSidecarProvidersEl, configs, false); + } // 重新加载当前配置以恢复选中状态 loadConfiguration(); @@ -210,8 +216,25 @@ async function loadConfiguration() { // TLS Sidecar 配置 const tlsSidecarEnabledEl = document.getElementById('tlsSidecarEnabled'); const tlsSidecarPortEl = document.getElementById('tlsSidecarPort'); + const tlsSidecarProxyUrlEl = document.getElementById('tlsSidecarProxyUrl'); + const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders'); + if (tlsSidecarEnabledEl) tlsSidecarEnabledEl.checked = data.TLS_SIDECAR_ENABLED || false; if (tlsSidecarPortEl) tlsSidecarPortEl.value = data.TLS_SIDECAR_PORT || 9090; + if (tlsSidecarProxyUrlEl) tlsSidecarProxyUrlEl.value = data.TLS_SIDECAR_PROXY_URL || ''; + + if (tlsSidecarProvidersEl) { + const enabledProviders = data.TLS_SIDECAR_ENABLED_PROVIDERS || []; + const tags = tlsSidecarProvidersEl.querySelectorAll('.provider-tag'); + tags.forEach(tag => { + const value = tag.getAttribute('data-value'); + if (enabledProviders.includes(value)) { + tag.classList.add('selected'); + } else { + tag.classList.remove('selected'); + } + }); + } } catch (error) { console.error('Failed to load configuration:', error); @@ -314,6 +337,15 @@ async function saveConfiguration() { // TLS Sidecar 配置 config.TLS_SIDECAR_ENABLED = document.getElementById('tlsSidecarEnabled')?.checked || false; config.TLS_SIDECAR_PORT = parseInt(document.getElementById('tlsSidecarPort')?.value || 9090); + config.TLS_SIDECAR_PROXY_URL = document.getElementById('tlsSidecarProxyUrl')?.value?.trim() || null; + + const tlsSidecarProvidersEl = document.getElementById('tlsSidecarProviders'); + if (tlsSidecarProvidersEl) { + config.TLS_SIDECAR_ENABLED_PROVIDERS = Array.from(tlsSidecarProvidersEl.querySelectorAll('.provider-tag.selected')) + .map(tag => tag.getAttribute('data-value')); + } else { + config.TLS_SIDECAR_ENABLED_PROVIDERS = []; + } try { await window.apiClient.post('/config', config); diff --git a/static/app/i18n.js b/static/app/i18n.js index 0762e37..2d1739b 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -340,7 +340,9 @@ const translations = { 'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接', 'config.proxy.tlsSidecarEnabled': 'TLS 指纹伪装 (uTLS Sidecar)', 'config.proxy.tlsSidecarPort': 'Sidecar 端口', - 'config.proxy.tlsSidecarNote': '启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)', + 'config.proxy.tlsSidecarProxyUrl': 'Sidecar 上游代理', + 'config.proxy.tlsSidecarEnabledProviders': '启用 TLS Sidecar 的提供商', + 'config.proxy.tlsSidecarNote': '启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)', 'config.log.title': '日志设置', 'config.log.enabled': '启用日志', 'config.log.outputMode': '日志输出模式', @@ -1183,7 +1185,9 @@ const translations = { 'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly', 'config.proxy.tlsSidecarEnabled': 'TLS Fingerprint Spoofing (uTLS Sidecar)', 'config.proxy.tlsSidecarPort': 'Sidecar Port', - 'config.proxy.tlsSidecarNote': 'When enabled, Grok requests are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)', + 'config.proxy.tlsSidecarProxyUrl': 'Sidecar Upstream Proxy', + 'config.proxy.tlsSidecarEnabledProviders': 'Providers Using TLS Sidecar', + 'config.proxy.tlsSidecarNote': 'When enabled, requests for selected providers are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)', 'config.log.title': 'Log Settings', 'config.log.enabled': 'Enable Logging', 'config.log.outputMode': 'Log Output Mode', diff --git a/static/components/section-config.html b/static/components/section-config.html index 8822d86..37e0fe1 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -147,7 +147,19 @@ - 启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务) +
+ + + TLS Sidecar 专用上游代理,留空则不使用代理 +
+
+ +
+ +
+ 点击选择需要通过 TLS Sidecar 访问的提供商 +
+ 启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)