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.
- package/README.md +1 -0
- package/dist/server/fs-database.js +244 -42
- package/dist/server/main.js +7 -1
- package/dist/server/proxy-server.js +247 -6
- package/dist/server/transformers/gemini.js +625 -0
- package/dist/server/transformers/streaming.js +563 -7
- package/dist/types/index.js +1 -0
- package/dist/ui/assets/{index-DgBQpyCC.js → index-DyW-TIXE.js} +81 -76
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/schema/gemini.schema.md +11 -0
|
@@ -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
|
-
|
|
1149
|
-
|
|
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:
|
|
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:
|
|
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
|
// 记录原始响应体
|