aicodeswitch 3.0.3 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,7 @@ const streaming_1 = require("./transformers/streaming");
53
53
  const chunk_collector_1 = require("./transformers/chunk-collector");
54
54
  const rules_status_service_1 = require("./rules-status-service");
55
55
  const claude_openai_1 = require("./transformers/claude-openai");
56
+ const gemini_1 = require("./transformers/gemini");
56
57
  const types_1 = require("../types");
57
58
  const SUPPORTED_TARGETS = ['claude-code', 'codex'];
58
59
  class ProxyServer {
@@ -1039,8 +1040,26 @@ class ProxyServer {
1039
1040
  isOpenAIChatSource(sourceType) {
1040
1041
  return sourceType === 'openai-chat' || sourceType === 'openai-responses' || sourceType === 'deepseek-reasoning-chat';
1041
1042
  }
1043
+ /** 判断是否为 Gemini 类型 */
1044
+ isGeminiSource(sourceType) {
1045
+ return sourceType === 'gemini';
1046
+ }
1042
1047
  isChatType(sourceType) {
1043
- return sourceType.endsWith('-chat');
1048
+ return sourceType.endsWith('-chat') || sourceType === 'gemini';
1049
+ }
1050
+ /**
1051
+ * 构建 Gemini API 的完整 URL
1052
+ * 用户只填写 base 地址(如 https://generativelanguage.googleapis.com)
1053
+ * 需要根据模型名称拼接成完整的 URL
1054
+ */
1055
+ buildGeminiUrl(baseUrl, model, streamRequested) {
1056
+ // 移除末尾的斜杠
1057
+ const base = baseUrl.replace(/\/$/, '');
1058
+ // 移除模型名称中可能包含的 models/ 前缀
1059
+ const modelName = model.replace(/^models\//, '');
1060
+ // 根据是否流式选择 endpoint
1061
+ const endpoint = streamRequested ? 'streamGenerateContent' : 'generateContent';
1062
+ return `${base}/v1beta/models/${modelName}:${endpoint}`;
1044
1063
  }
1045
1064
  /**
1046
1065
  * 判断模型是否应该使用 max_completion_tokens 字段
@@ -1130,7 +1149,7 @@ class ProxyServer {
1130
1149
  const headers = {};
1131
1150
  for (const [key, value] of Object.entries(req.headers)) {
1132
1151
  // 排除原始认证头,防止与代理设置的认证头冲突
1133
- if (['host', 'connection', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key'].includes(key.toLowerCase())) {
1152
+ if (['host', 'connection', 'content-length', 'authorization', 'x-api-key', 'x-anthropic-api-key', 'anthropic-api-key', 'x-goog-api-key'].includes(key.toLowerCase())) {
1134
1153
  continue;
1135
1154
  }
1136
1155
  if (typeof value === 'string') {
@@ -1145,8 +1164,11 @@ class ProxyServer {
1145
1164
  }
1146
1165
  // 确定认证方式:优先使用服务配置的 authType,否则根据 sourceType 自动判断
1147
1166
  const authType = service.authType || types_1.AuthType.AUTO;
1148
- const useXApiKey = authType === types_1.AuthType.API_KEY || (authType === types_1.AuthType.AUTO && this.isClaudeSource(sourceType));
1149
- if (useXApiKey) {
1167
+ if (authType === types_1.AuthType.G_API_KEY || (authType === types_1.AuthType.AUTO && this.isGeminiSource(sourceType))) {
1168
+ // 使用 x-goog-api-key 认证(适用于 Google Gemini API)
1169
+ headers['x-goog-api-key'] = service.apiKey;
1170
+ }
1171
+ else if (authType === types_1.AuthType.API_KEY || (authType === types_1.AuthType.AUTO && this.isClaudeSource(sourceType))) {
1150
1172
  // 使用 x-api-key 认证(适用于 claude-chat, claude-code 及某些需要 x-api-key 的 openai-chat 兼容 API)
1151
1173
  headers['x-api-key'] = service.apiKey;
1152
1174
  if (this.isClaudeSource(sourceType) || authType === types_1.AuthType.API_KEY) {
@@ -1511,6 +1533,9 @@ class ProxyServer {
1511
1533
  else if (this.isOpenAIChatSource(sourceType)) {
1512
1534
  requestBody = (0, claude_openai_1.transformClaudeRequestToOpenAIChat)(requestBody, rule.targetModel);
1513
1535
  }
1536
+ else if (this.isGeminiSource(sourceType)) {
1537
+ requestBody = (0, gemini_1.transformClaudeRequestToGemini)(requestBody);
1538
+ }
1514
1539
  else {
1515
1540
  res.status(400).json({ error: 'Unsupported source type for Claude Code.' });
1516
1541
  yield finalizeLog(400, 'Unsupported source type for Claude Code');
@@ -1524,6 +1549,9 @@ class ProxyServer {
1524
1549
  else if (this.isClaudeSource(sourceType)) {
1525
1550
  requestBody = (0, claude_openai_1.transformClaudeRequestToOpenAIChat)(requestBody, rule.targetModel);
1526
1551
  }
1552
+ else if (this.isGeminiSource(sourceType)) {
1553
+ requestBody = (0, gemini_1.transformOpenAIChatRequestToGemini)(requestBody);
1554
+ }
1527
1555
  else {
1528
1556
  res.status(400).json({ error: 'Unsupported source type for Codex.' });
1529
1557
  yield finalizeLog(400, 'Unsupported source type for Codex');
@@ -1543,9 +1571,22 @@ class ProxyServer {
1543
1571
  }
1544
1572
  // 根据源工具类型和目标API类型,映射请求路径
1545
1573
  const mappedPath = this.mapRequestPath(route.targetType, sourceType, pathToAppend);
1574
+ // 构建上游 URL
1575
+ let upstreamUrl;
1576
+ if (this.isGeminiSource(sourceType)) {
1577
+ // Gemini 类型需要特殊处理:根据模型拼接完整 URL
1578
+ const model = requestBody.model || rule.targetModel || 'gemini-pro';
1579
+ upstreamUrl = this.buildGeminiUrl(service.apiUrl, model, streamRequested);
1580
+ }
1581
+ else if (this.isChatType(sourceType)) {
1582
+ upstreamUrl = service.apiUrl;
1583
+ }
1584
+ else {
1585
+ upstreamUrl = `${service.apiUrl}${mappedPath}`;
1586
+ }
1546
1587
  const config = {
1547
1588
  method: req.method,
1548
- url: this.isChatType(sourceType) ? service.apiUrl : `${service.apiUrl}${mappedPath}`,
1589
+ url: upstreamUrl,
1549
1590
  headers: this.buildUpstreamHeaders(req, service, sourceType, streamRequested),
1550
1591
  timeout: rule.timeout || 3000000, // 默认300秒
1551
1592
  validateStatus: () => true,
@@ -1592,7 +1633,7 @@ class ProxyServer {
1592
1633
  // const actualMaxTokens = requestBody?.[maxTokensFieldName] || requestBody?.max_tokens;
1593
1634
  const upstreamHeaders = this.buildUpstreamHeaders(req, service, sourceType, streamRequested);
1594
1635
  upstreamRequestForLog = {
1595
- url: this.isChatType(sourceType) ? service.apiUrl : `${service.apiUrl}${mappedPath}`,
1636
+ url: upstreamUrl,
1596
1637
  // model: actualModel,
1597
1638
  // [maxTokensFieldName]: actualMaxTokens,
1598
1639
  headers: upstreamHeaders,
@@ -1809,6 +1850,194 @@ class ProxyServer {
1809
1850
  }));
1810
1851
  return;
1811
1852
  }
1853
+ // Gemini -> Claude Code 流式转换
1854
+ if (targetType === 'claude-code' && this.isGeminiSource(sourceType)) {
1855
+ res.setHeader('Content-Type', 'text/event-stream');
1856
+ res.setHeader('Cache-Control', 'no-cache');
1857
+ res.setHeader('Connection', 'keep-alive');
1858
+ const parser = new streaming_1.SSEParserTransform();
1859
+ const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
1860
+ const converter = new streaming_1.GeminiToClaudeEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
1861
+ const serializer = new streaming_1.SSESerializerTransform();
1862
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
1863
+ const finalizeChunks = () => {
1864
+ const usage = converter.getUsage();
1865
+ if (usage) {
1866
+ usageForLog = {
1867
+ inputTokens: usage.input_tokens,
1868
+ outputTokens: usage.output_tokens,
1869
+ cacheReadInputTokens: usage.cache_read_input_tokens,
1870
+ };
1871
+ }
1872
+ else {
1873
+ const extractedUsage = eventCollector.extractUsage();
1874
+ if (extractedUsage) {
1875
+ usageForLog = this.extractTokenUsage(extractedUsage);
1876
+ }
1877
+ }
1878
+ streamChunksForLog = eventCollector.getChunks();
1879
+ responseBodyForLog = streamChunksForLog.join('\n');
1880
+ console.log('[Proxy] Gemini stream request finished (claude-code), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
1881
+ void finalizeLog(res.statusCode);
1882
+ };
1883
+ eventCollector.on('finish', () => {
1884
+ console.log('[Proxy] EventCollector finished (gemini->claude-code), collecting chunks...');
1885
+ finalizeChunks();
1886
+ });
1887
+ res.on('finish', () => {
1888
+ console.log('[Proxy] Response finished (gemini->claude-code)');
1889
+ if (!streamChunksForLog) {
1890
+ console.log('[Proxy] Chunks not collected yet, forcing collection...');
1891
+ finalizeChunks();
1892
+ }
1893
+ });
1894
+ res.on('error', (err) => {
1895
+ console.error('[Proxy] Response stream error:', err);
1896
+ });
1897
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1898
+ var _a;
1899
+ if (error) {
1900
+ console.error('[Proxy] Pipeline error for gemini->claude-code:', error);
1901
+ try {
1902
+ const vendors = this.dbManager.getVendors();
1903
+ const vendor = vendors.find(v => v.id === service.vendorId);
1904
+ yield this.dbManager.addErrorLog({
1905
+ timestamp: Date.now(),
1906
+ method: req.method,
1907
+ path: req.path,
1908
+ statusCode: 500,
1909
+ errorMessage: error.message || 'Stream processing error',
1910
+ errorStack: error.stack,
1911
+ requestHeaders: this.normalizeHeaders(req.headers),
1912
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
1913
+ upstreamRequest: upstreamRequestForLog,
1914
+ ruleId: rule.id,
1915
+ targetType,
1916
+ targetServiceId: service.id,
1917
+ targetServiceName: service.name,
1918
+ targetModel: rule.targetModel,
1919
+ vendorId: service.vendorId,
1920
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
1921
+ requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
1922
+ responseTime: Date.now() - startTime,
1923
+ });
1924
+ }
1925
+ catch (logError) {
1926
+ console.error('[Proxy] Failed to log error:', logError);
1927
+ }
1928
+ try {
1929
+ if (!res.writableEnded) {
1930
+ const errorEvent = `event: error\ndata: ${JSON.stringify({
1931
+ type: 'error',
1932
+ error: {
1933
+ type: 'api_error',
1934
+ message: 'Stream processing error occurred'
1935
+ }
1936
+ })}\n\n`;
1937
+ res.write(errorEvent);
1938
+ res.end();
1939
+ }
1940
+ }
1941
+ catch (writeError) {
1942
+ console.error('[Proxy] Failed to send error event:', writeError);
1943
+ }
1944
+ yield finalizeLog(500, error.message);
1945
+ }
1946
+ }));
1947
+ return;
1948
+ }
1949
+ // Gemini -> Codex 流式转换
1950
+ if (targetType === 'codex' && this.isGeminiSource(sourceType)) {
1951
+ res.setHeader('Content-Type', 'text/event-stream');
1952
+ res.setHeader('Cache-Control', 'no-cache');
1953
+ res.setHeader('Connection', 'keep-alive');
1954
+ const parser = new streaming_1.SSEParserTransform();
1955
+ const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
1956
+ const converter = new streaming_1.GeminiToOpenAIChatEventTransform({ model: requestBody === null || requestBody === void 0 ? void 0 : requestBody.model });
1957
+ const serializer = new streaming_1.SSESerializerTransform();
1958
+ responseHeadersForLog = this.normalizeResponseHeaders(responseHeaders);
1959
+ const finalizeChunks = () => {
1960
+ const usage = converter.getUsage();
1961
+ if (usage) {
1962
+ usageForLog = {
1963
+ inputTokens: usage.prompt_tokens,
1964
+ outputTokens: usage.completion_tokens,
1965
+ totalTokens: usage.total_tokens,
1966
+ };
1967
+ }
1968
+ else {
1969
+ const extractedUsage = eventCollector.extractUsage();
1970
+ if (extractedUsage) {
1971
+ usageForLog = this.extractTokenUsage(extractedUsage);
1972
+ }
1973
+ }
1974
+ streamChunksForLog = eventCollector.getChunks();
1975
+ responseBodyForLog = streamChunksForLog.join('\n');
1976
+ console.log('[Proxy] Gemini stream request finished (codex), collected chunks:', (streamChunksForLog === null || streamChunksForLog === void 0 ? void 0 : streamChunksForLog.length) || 0);
1977
+ void finalizeLog(res.statusCode);
1978
+ };
1979
+ eventCollector.on('finish', () => {
1980
+ console.log('[Proxy] EventCollector finished (gemini->codex), collecting chunks...');
1981
+ finalizeChunks();
1982
+ });
1983
+ res.on('finish', () => {
1984
+ console.log('[Proxy] Response finished (gemini->codex)');
1985
+ if (!streamChunksForLog) {
1986
+ console.log('[Proxy] Chunks not collected yet, forcing collection...');
1987
+ finalizeChunks();
1988
+ }
1989
+ });
1990
+ res.on('error', (err) => {
1991
+ console.error('[Proxy] Response stream error:', err);
1992
+ });
1993
+ (0, stream_1.pipeline)(response.data, parser, eventCollector, converter, serializer, res, (error) => __awaiter(this, void 0, void 0, function* () {
1994
+ var _a;
1995
+ if (error) {
1996
+ console.error('[Proxy] Pipeline error for gemini->codex:', error);
1997
+ try {
1998
+ const vendors = this.dbManager.getVendors();
1999
+ const vendor = vendors.find(v => v.id === service.vendorId);
2000
+ yield this.dbManager.addErrorLog({
2001
+ timestamp: Date.now(),
2002
+ method: req.method,
2003
+ path: req.path,
2004
+ statusCode: 500,
2005
+ errorMessage: error.message || 'Stream processing error',
2006
+ errorStack: error.stack,
2007
+ requestHeaders: this.normalizeHeaders(req.headers),
2008
+ requestBody: req.body ? JSON.stringify(req.body) : undefined,
2009
+ upstreamRequest: upstreamRequestForLog,
2010
+ ruleId: rule.id,
2011
+ targetType,
2012
+ targetServiceId: service.id,
2013
+ targetServiceName: service.name,
2014
+ targetModel: rule.targetModel,
2015
+ vendorId: service.vendorId,
2016
+ vendorName: vendor === null || vendor === void 0 ? void 0 : vendor.name,
2017
+ requestModel: (_a = req.body) === null || _a === void 0 ? void 0 : _a.model,
2018
+ responseTime: Date.now() - startTime,
2019
+ });
2020
+ }
2021
+ catch (logError) {
2022
+ console.error('[Proxy] Failed to log error:', logError);
2023
+ }
2024
+ try {
2025
+ if (!res.writableEnded) {
2026
+ const errorEvent = `data: ${JSON.stringify({
2027
+ error: 'Stream processing error occurred'
2028
+ })}\n\n`;
2029
+ res.write(errorEvent);
2030
+ res.end();
2031
+ }
2032
+ }
2033
+ catch (writeError) {
2034
+ console.error('[Proxy] Failed to send error event:', writeError);
2035
+ }
2036
+ yield finalizeLog(500, error.message);
2037
+ }
2038
+ }));
2039
+ return;
2040
+ }
1812
2041
  // 默认stream处理(无转换)
1813
2042
  const parser = new streaming_1.SSEParserTransform();
1814
2043
  const eventCollector = new chunk_collector_1.SSEEventCollectorTransform();
@@ -1940,12 +2169,24 @@ class ProxyServer {
1940
2169
  responseBodyForLog = JSON.stringify(converted);
1941
2170
  res.status(response.status).json(converted);
1942
2171
  }
2172
+ else if (targetType === 'claude-code' && this.isGeminiSource(sourceType)) {
2173
+ const converted = (0, gemini_1.transformGeminiResponseToClaude)(responseData, rule.targetModel);
2174
+ usageForLog = (0, gemini_1.extractTokenUsageFromGeminiUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usageMetadata);
2175
+ responseBodyForLog = JSON.stringify(converted);
2176
+ res.status(response.status).json(converted);
2177
+ }
1943
2178
  else if (targetType === 'codex' && this.isClaudeSource(sourceType)) {
1944
2179
  const converted = (0, claude_openai_1.transformClaudeResponseToOpenAIChat)(responseData);
1945
2180
  usageForLog = (0, claude_openai_1.extractTokenUsageFromClaudeUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
1946
2181
  responseBodyForLog = JSON.stringify(converted);
1947
2182
  res.status(response.status).json(converted);
1948
2183
  }
2184
+ else if (targetType === 'codex' && this.isGeminiSource(sourceType)) {
2185
+ const converted = (0, gemini_1.transformGeminiResponseToOpenAIChat)(responseData, rule.targetModel);
2186
+ usageForLog = (0, gemini_1.extractTokenUsageFromGeminiUsage)(responseData === null || responseData === void 0 ? void 0 : responseData.usageMetadata);
2187
+ responseBodyForLog = JSON.stringify(converted);
2188
+ res.status(response.status).json(converted);
2189
+ }
1949
2190
  else {
1950
2191
  usageForLog = this.extractTokenUsage(responseData === null || responseData === void 0 ? void 0 : responseData.usage);
1951
2192
  // 记录原始响应体