feat: 增强TLS sidecar支持并更新模型列表
- 扩展TLS sidecar配置,支持按提供商启用和设置上游代理 - 更新gemini-antigravity提供商模型列表至最新版本 - 修复JSON schema转换中数组type的处理以兼容Google Gemini API - 为所有主要提供商集成TLS sidecar支持 - 修复CodexConverter中系统消息重复问题 - 改进gemini-core的错误处理和请求头设置
This commit is contained in:
parent
b37ead525c
commit
602c6be836
20 changed files with 523 additions and 203 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.11.4.2
|
||||
2.11.5
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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': [
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 - 应用配置对象
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -147,7 +147,19 @@
|
|||
<input type="number" id="tlsSidecarPort" class="form-control" min="1024" max="65535" value="9090">
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)</small>
|
||||
<div class="form-group">
|
||||
<label for="tlsSidecarProxyUrl" data-i18n="config.proxy.tlsSidecarProxyUrl">Sidecar 上游代理</label>
|
||||
<input type="text" id="tlsSidecarProxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890">
|
||||
<small class="form-text" data-i18n="config.proxy.urlNote">TLS Sidecar 专用上游代理,留空则不使用代理</small>
|
||||
</div>
|
||||
<div class="form-group pool-section">
|
||||
<label data-i18n="config.proxy.tlsSidecarEnabledProviders">启用 TLS Sidecar 的提供商</label>
|
||||
<div id="tlsSidecarProviders" class="provider-tags">
|
||||
<!-- 动态渲染 -->
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过 TLS Sidecar 访问的提供商</small>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)</small>
|
||||
</div>
|
||||
|
||||
<!-- 服务治理与高可用 -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue