coding-tool-x 3.8.1 → 3.8.2
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/CHANGELOG.md +2 -0
- package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
- package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
- package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
- package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
- package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
- package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
- package/dist/web/assets/index-D5hyHPj7.js +2 -0
- package/dist/web/index.html +1 -1
- package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
- package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
- package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
- package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
- package/package.json +1 -1
- package/src/commands/update.js +1 -1
- package/src/server/opencode-proxy-server.js +55 -1037
- package/src/server/services/claude-wire.js +694 -0
- package/src/server/services/codex-wire.js +219 -0
- package/src/server/services/gemini-wire.js +759 -0
- package/src/server/services/model-detector.js +49 -131
- package/src/server/services/omp-channels.js +23 -4
- package/src/server/services/omp-gateway.js +29 -1
- package/src/server/services/omp-settings-manager.js +27 -3
- package/src/server/services/opencode-gateway-adapters.js +22 -1147
- package/src/server/services/opencode-normalization.js +546 -0
- package/src/server/services/session-history-index.js +1 -1
- package/src/server/services/session-history-worker.js +2 -1
- package/src/server/services/speed-test.js +85 -303
- package/dist/web/assets/index-DIWJmyL6.js +0 -2
|
@@ -26,8 +26,16 @@ const {
|
|
|
26
26
|
resolveTargetUrl,
|
|
27
27
|
ensureOpenAiStreamUsage
|
|
28
28
|
} = require('./services/base/proxy-utils');
|
|
29
|
-
const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
|
|
30
29
|
const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
|
|
30
|
+
const { buildCodexTargetUrl, createCodexRequest } = require('./services/codex-wire');
|
|
31
|
+
const {
|
|
32
|
+
parseSSEUsage,
|
|
33
|
+
parseNonStreamingUsage,
|
|
34
|
+
mergeUsageIntoTokenData,
|
|
35
|
+
createTokenData
|
|
36
|
+
} = require('./services/base/response-usage-parser');
|
|
37
|
+
const { createClaudeRequest, buildClaudeTargetUrl, buildClaudeCountTokensTargetUrl, buildClaudeCountTokensHeaders } = require('./services/claude-wire');
|
|
38
|
+
const { createGeminiRequest, buildGeminiTargetUrl, shouldUseGeminiCliFormat } = require('./services/gemini-wire');
|
|
31
39
|
|
|
32
40
|
let proxyServer = null;
|
|
33
41
|
let proxyApp = null;
|
|
@@ -58,45 +66,13 @@ const PRICING = {
|
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
const OPENCODE_BASE_PRICING = DEFAULT_CONFIG.pricing.opencode || DEFAULT_CONFIG.pricing.codex;
|
|
61
|
-
const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.59 (external, cli)';
|
|
62
|
-
const CLAUDE_MESSAGES_BETA_FLAGS = Object.freeze([
|
|
63
|
-
'claude-code-20250219',
|
|
64
|
-
'interleaved-thinking-2025-05-14',
|
|
65
|
-
'prompt-caching-scope-2026-01-05',
|
|
66
|
-
'effort-2025-11-24'
|
|
67
|
-
]);
|
|
68
|
-
const CLAUDE_ADVANCED_TOOL_USE_BETA = 'advanced-tool-use-2025-11-20';
|
|
69
|
-
const CLAUDE_COUNT_TOKENS_BETA_FLAGS = Object.freeze([
|
|
70
|
-
'claude-code-20250219',
|
|
71
|
-
'token-counting-2024-11-01'
|
|
72
|
-
]);
|
|
73
|
-
const DEFAULT_CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
74
|
-
const DEFAULT_CLAUDE_CODE_TOOL_NAMES = Object.freeze([
|
|
75
|
-
'Task',
|
|
76
|
-
'Bash',
|
|
77
|
-
'Glob',
|
|
78
|
-
'Grep',
|
|
79
|
-
'Read',
|
|
80
|
-
'Edit',
|
|
81
|
-
'Write',
|
|
82
|
-
'ToolSearch'
|
|
83
|
-
]);
|
|
84
|
-
const CODEX_CLI_VERSION = '0.101.0';
|
|
85
|
-
const CODEX_CLI_USER_AGENT = 'codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464';
|
|
86
|
-
const GEMINI_CLI_USER_AGENT = 'google-api-nodejs-client/9.15.1';
|
|
87
|
-
const GEMINI_CLI_API_CLIENT = 'gl-node/22.17.0';
|
|
88
|
-
const GEMINI_CLI_CLIENT_METADATA = 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI';
|
|
89
69
|
const CLAUDE_SESSION_USER_ID_TTL_MS = 60 * 60 * 1000;
|
|
90
70
|
const CLAUDE_SESSION_USER_ID_CACHE_MAX = 2000;
|
|
91
71
|
const claudeSessionUserIdCache = new Map();
|
|
92
72
|
const CLAUDE_USER_ID_ACCOUNT_RE = /^user_([0-9a-f]{64})_account__session_[a-z0-9._-]+$/i;
|
|
93
73
|
const CLAUDE_USER_ID_FULL_RE = /^user_[0-9a-f]{64}_account__session_[a-z0-9._-]+$/i;
|
|
94
|
-
const CLAUDE_TEMPLATE_SYSTEM_MIN_CHARS = 1000;
|
|
95
|
-
const CLAUDE_TEMPLATE_CACHE_TTL_MS = 30 * 1000;
|
|
96
74
|
let cachedClaudeAccountId = '';
|
|
97
75
|
let cachedClaudeUserId = '';
|
|
98
|
-
let cachedClaudeRequestTemplate = null;
|
|
99
|
-
let cachedClaudeRequestTemplateAt = 0;
|
|
100
76
|
|
|
101
77
|
// detectModelTier, redirectModel, resolveTargetUrl imported from services/base/proxy-utils
|
|
102
78
|
const resolveOpenCodeTarget = resolveTargetUrl;
|
|
@@ -196,7 +172,7 @@ function resolveClaudePreferredUserId() {
|
|
|
196
172
|
return cachedClaudeUserId;
|
|
197
173
|
}
|
|
198
174
|
|
|
199
|
-
const requestTemplate =
|
|
175
|
+
const requestTemplate = loadClaudeRequestTemplate();
|
|
200
176
|
if (requestTemplate && CLAUDE_USER_ID_FULL_RE.test(requestTemplate.userId || '')) {
|
|
201
177
|
cachedClaudeUserId = requestTemplate.userId;
|
|
202
178
|
return cachedClaudeUserId;
|
|
@@ -336,135 +312,6 @@ function normalizeGatewaySourceType(channel) {
|
|
|
336
312
|
return 'codex';
|
|
337
313
|
}
|
|
338
314
|
|
|
339
|
-
function mapStainlessOs() {
|
|
340
|
-
switch (process.platform) {
|
|
341
|
-
case 'darwin':
|
|
342
|
-
return 'MacOS';
|
|
343
|
-
case 'win32':
|
|
344
|
-
return 'Windows';
|
|
345
|
-
case 'linux':
|
|
346
|
-
return 'Linux';
|
|
347
|
-
default:
|
|
348
|
-
return `other::${process.platform}`;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function mapStainlessArch() {
|
|
353
|
-
switch (process.arch) {
|
|
354
|
-
case 'x64':
|
|
355
|
-
return 'x64';
|
|
356
|
-
case 'arm64':
|
|
357
|
-
return 'arm64';
|
|
358
|
-
case 'ia32':
|
|
359
|
-
return 'x86';
|
|
360
|
-
default:
|
|
361
|
-
return `other::${process.arch}`;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function buildClaudeBetaHeader(options = {}) {
|
|
366
|
-
const requestType = options.requestType === 'count_tokens' ? 'count_tokens' : 'messages';
|
|
367
|
-
const hasTools = !!options.hasTools;
|
|
368
|
-
const betaFlags = requestType === 'count_tokens'
|
|
369
|
-
? [...CLAUDE_COUNT_TOKENS_BETA_FLAGS]
|
|
370
|
-
: [...CLAUDE_MESSAGES_BETA_FLAGS];
|
|
371
|
-
|
|
372
|
-
if (requestType === 'messages' && hasTools) {
|
|
373
|
-
betaFlags.push(CLAUDE_ADVANCED_TOOL_USE_BETA);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return betaFlags.join(',');
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
function hasExpectedClaudeToolSet(tools = []) {
|
|
381
|
-
if (!Array.isArray(tools) || tools.length < DEFAULT_CLAUDE_CODE_TOOL_NAMES.length) {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
const names = new Set();
|
|
385
|
-
tools.forEach((tool) => {
|
|
386
|
-
if (tool && typeof tool === 'object' && typeof tool.name === 'string') {
|
|
387
|
-
names.add(tool.name);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
return DEFAULT_CLAUDE_CODE_TOOL_NAMES.every(name => names.has(name));
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function extractClaudeSystemCharCount(system = []) {
|
|
394
|
-
if (!Array.isArray(system)) return 0;
|
|
395
|
-
return system.reduce((sum, block) => {
|
|
396
|
-
if (!block || typeof block !== 'object') return sum;
|
|
397
|
-
const text = typeof block.text === 'string' ? block.text : '';
|
|
398
|
-
return sum + text.length;
|
|
399
|
-
}, 0);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function cloneJson(value) {
|
|
403
|
-
try {
|
|
404
|
-
return JSON.parse(JSON.stringify(value));
|
|
405
|
-
} catch {
|
|
406
|
-
return value;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function resolveClaudeRequestTemplate() {
|
|
411
|
-
const now = Date.now();
|
|
412
|
-
if (cachedClaudeRequestTemplate && now - cachedClaudeRequestTemplateAt < CLAUDE_TEMPLATE_CACHE_TTL_MS) {
|
|
413
|
-
return cachedClaudeRequestTemplate;
|
|
414
|
-
}
|
|
415
|
-
cachedClaudeRequestTemplate = loadClaudeRequestTemplate();
|
|
416
|
-
cachedClaudeRequestTemplateAt = now;
|
|
417
|
-
return cachedClaudeRequestTemplate;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function buildClaudeSystemBlocks(systemText = '', templateSystem = []) {
|
|
421
|
-
const blocks = Array.isArray(templateSystem) && templateSystem.length > 0
|
|
422
|
-
? cloneJson(templateSystem)
|
|
423
|
-
: [];
|
|
424
|
-
|
|
425
|
-
const normalizedSystem = String(systemText || '').trim();
|
|
426
|
-
if (normalizedSystem) {
|
|
427
|
-
blocks.push({
|
|
428
|
-
type: 'text',
|
|
429
|
-
text: normalizedSystem
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (blocks.length === 0) {
|
|
434
|
-
blocks.push({
|
|
435
|
-
type: 'text',
|
|
436
|
-
text: DEFAULT_CLAUDE_CODE_SYSTEM_PROMPT
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return blocks;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function buildClaudeRequestHeaders(apiKey, options = {}) {
|
|
444
|
-
return {
|
|
445
|
-
'x-api-key': apiKey,
|
|
446
|
-
authorization: `Bearer ${apiKey}`,
|
|
447
|
-
'anthropic-version': '2023-06-01',
|
|
448
|
-
'anthropic-beta': buildClaudeBetaHeader(options),
|
|
449
|
-
'anthropic-dangerous-direct-browser-access': 'true',
|
|
450
|
-
'x-app': 'cli',
|
|
451
|
-
'x-stainless-retry-count': '0',
|
|
452
|
-
'x-stainless-timeout': '600',
|
|
453
|
-
'x-stainless-runtime-version': process.version,
|
|
454
|
-
'x-stainless-package-version': '0.74.0',
|
|
455
|
-
'x-stainless-runtime': 'node',
|
|
456
|
-
'x-stainless-lang': 'js',
|
|
457
|
-
'x-stainless-arch': mapStainlessArch(),
|
|
458
|
-
'x-stainless-os': mapStainlessOs(),
|
|
459
|
-
'content-type': 'application/json',
|
|
460
|
-
accept: 'application/json',
|
|
461
|
-
'accept-encoding': 'gzip, deflate',
|
|
462
|
-
'accept-language': '*',
|
|
463
|
-
'sec-fetch-mode': 'cors',
|
|
464
|
-
connection: 'keep-alive',
|
|
465
|
-
'user-agent': CLAUDE_CODE_USER_AGENT
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
315
|
|
|
469
316
|
function getRequestPathname(urlPath = '') {
|
|
470
317
|
try {
|
|
@@ -524,283 +371,12 @@ function isConverterPresetChannel(channel) {
|
|
|
524
371
|
return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
|
|
525
372
|
}
|
|
526
373
|
|
|
527
|
-
function extractTextFragments(value, fragments) {
|
|
528
|
-
if (value === null || value === undefined) return;
|
|
529
|
-
if (typeof value === 'string') {
|
|
530
|
-
if (value.trim()) fragments.push(value);
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
534
|
-
fragments.push(String(value));
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
if (Array.isArray(value)) {
|
|
538
|
-
value.forEach(item => extractTextFragments(item, fragments));
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (typeof value !== 'object') return;
|
|
542
|
-
|
|
543
|
-
if (typeof value.text === 'string') {
|
|
544
|
-
extractTextFragments(value.text, fragments);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (typeof value.input_text === 'string') {
|
|
548
|
-
extractTextFragments(value.input_text, fragments);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
if (typeof value.output_text === 'string') {
|
|
552
|
-
extractTextFragments(value.output_text, fragments);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
if (value.content !== undefined) {
|
|
556
|
-
extractTextFragments(value.content, fragments);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (Array.isArray(value.parts)) {
|
|
560
|
-
extractTextFragments(value.parts, fragments);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function extractText(value) {
|
|
565
|
-
const fragments = [];
|
|
566
|
-
extractTextFragments(value, fragments);
|
|
567
|
-
return fragments.join('\n').trim();
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function normalizeOpenAiRole(role) {
|
|
571
|
-
const value = String(role || '').trim().toLowerCase();
|
|
572
|
-
if (value === 'assistant' || value === 'model') return 'assistant';
|
|
573
|
-
if (value === 'system') return 'system';
|
|
574
|
-
return 'user';
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function normalizeOpenAiToolsToClaude(tools = []) {
|
|
578
|
-
if (!Array.isArray(tools)) return [];
|
|
579
|
-
|
|
580
|
-
const normalized = [];
|
|
581
|
-
for (const tool of tools) {
|
|
582
|
-
if (!tool || typeof tool !== 'object') continue;
|
|
583
|
-
|
|
584
|
-
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
585
|
-
const fn = tool.function;
|
|
586
|
-
if (!fn.name) continue;
|
|
587
|
-
normalized.push({
|
|
588
|
-
name: fn.name,
|
|
589
|
-
description: fn.description || '',
|
|
590
|
-
input_schema: fn.parameters || { type: 'object', properties: {} }
|
|
591
|
-
});
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (tool.type === 'function' && tool.name) {
|
|
596
|
-
normalized.push({
|
|
597
|
-
name: tool.name,
|
|
598
|
-
description: tool.description || '',
|
|
599
|
-
input_schema: tool.parameters || { type: 'object', properties: {} }
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return normalized;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function normalizeToolChoiceToClaude(toolChoice) {
|
|
608
|
-
if (!toolChoice) return undefined;
|
|
609
|
-
|
|
610
|
-
if (typeof toolChoice === 'string') {
|
|
611
|
-
if (toolChoice === 'auto') return { type: 'auto' };
|
|
612
|
-
if (toolChoice === 'required') return { type: 'any' };
|
|
613
|
-
return undefined;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (typeof toolChoice === 'object') {
|
|
617
|
-
if (toolChoice.type === 'function' && toolChoice.function?.name) {
|
|
618
|
-
return { type: 'tool', name: toolChoice.function.name };
|
|
619
|
-
}
|
|
620
|
-
if (toolChoice.type === 'function' && toolChoice.name) {
|
|
621
|
-
return { type: 'tool', name: toolChoice.name };
|
|
622
|
-
}
|
|
623
|
-
if (toolChoice.type === 'auto') return { type: 'auto' };
|
|
624
|
-
if (toolChoice.type === 'required') return { type: 'any' };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return undefined;
|
|
628
|
-
}
|
|
629
374
|
|
|
630
375
|
function generateToolCallId() {
|
|
631
376
|
return `toolu_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
632
377
|
}
|
|
633
378
|
|
|
634
|
-
function parseToolArguments(value) {
|
|
635
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
636
|
-
return value;
|
|
637
|
-
}
|
|
638
|
-
if (typeof value === 'string') {
|
|
639
|
-
const trimmed = value.trim();
|
|
640
|
-
if (!trimmed) return {};
|
|
641
|
-
try {
|
|
642
|
-
const parsed = JSON.parse(trimmed);
|
|
643
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
644
|
-
return parsed;
|
|
645
|
-
}
|
|
646
|
-
} catch {
|
|
647
|
-
return {};
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
return {};
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function normalizeToolResultContent(value) {
|
|
654
|
-
if (typeof value === 'string') return value;
|
|
655
|
-
if (value === null || value === undefined) return '';
|
|
656
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
657
|
-
try {
|
|
658
|
-
return JSON.stringify(value);
|
|
659
|
-
} catch {
|
|
660
|
-
return String(value);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function buildAssistantToolUseMessageFromFunctionCall(item) {
|
|
665
|
-
const functionPayload = (item?.function && typeof item.function === 'object')
|
|
666
|
-
? item.function
|
|
667
|
-
: item;
|
|
668
|
-
const name = functionPayload?.name || item?.name;
|
|
669
|
-
if (!name) return null;
|
|
670
|
-
|
|
671
|
-
const callId = functionPayload?.call_id || item?.call_id || functionPayload?.id || item?.id || generateToolCallId();
|
|
672
|
-
const argumentsSource = functionPayload?.arguments ?? item?.arguments ?? functionPayload?.input ?? item?.input;
|
|
673
|
-
const input = parseToolArguments(argumentsSource);
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
role: 'assistant',
|
|
677
|
-
content: [
|
|
678
|
-
{
|
|
679
|
-
type: 'tool_use',
|
|
680
|
-
id: callId,
|
|
681
|
-
name,
|
|
682
|
-
input
|
|
683
|
-
}
|
|
684
|
-
]
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function buildUserToolResultMessage(item) {
|
|
689
|
-
const callId = item?.call_id || item?.tool_call_id || item?.id || generateToolCallId();
|
|
690
|
-
const outputSource = item?.output ?? item?.content ?? '';
|
|
691
|
-
const content = normalizeToolResultContent(outputSource);
|
|
692
|
-
|
|
693
|
-
return {
|
|
694
|
-
role: 'user',
|
|
695
|
-
content: [
|
|
696
|
-
{
|
|
697
|
-
type: 'tool_result',
|
|
698
|
-
tool_use_id: callId,
|
|
699
|
-
content
|
|
700
|
-
}
|
|
701
|
-
]
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function normalizeOpenCodeMessages(pathname, payload = {}) {
|
|
706
|
-
const systemParts = [];
|
|
707
|
-
const messages = [];
|
|
708
|
-
|
|
709
|
-
if (isResponsesPath(pathname) && typeof payload.instructions === 'string' && payload.instructions.trim()) {
|
|
710
|
-
systemParts.push(payload.instructions.trim());
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const appendMessage = (role, content) => {
|
|
714
|
-
const normalizedRole = normalizeOpenAiRole(role);
|
|
715
|
-
const text = extractText(content);
|
|
716
|
-
if (!text) return;
|
|
717
|
-
if (normalizedRole === 'system') {
|
|
718
|
-
systemParts.push(text);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
messages.push({
|
|
722
|
-
role: normalizedRole === 'assistant' ? 'assistant' : 'user',
|
|
723
|
-
content: [{ type: 'text', text }]
|
|
724
|
-
});
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
if (isResponsesPath(pathname)) {
|
|
728
|
-
if (typeof payload.input === 'string') {
|
|
729
|
-
appendMessage('user', payload.input);
|
|
730
|
-
} else if (Array.isArray(payload.input)) {
|
|
731
|
-
payload.input.forEach(item => {
|
|
732
|
-
if (!item || typeof item !== 'object') return;
|
|
733
|
-
if (item.type === 'function_call') {
|
|
734
|
-
const assistantToolUse = buildAssistantToolUseMessageFromFunctionCall(item);
|
|
735
|
-
if (assistantToolUse) {
|
|
736
|
-
messages.push(assistantToolUse);
|
|
737
|
-
}
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
if (item.type === 'function_call_output') {
|
|
741
|
-
messages.push(buildUserToolResultMessage(item));
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (item.type === 'message' || item.role) {
|
|
745
|
-
appendMessage(item.role, item.content);
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (isChatCompletionsPath(pathname) && Array.isArray(payload.messages)) {
|
|
752
|
-
payload.messages.forEach(message => {
|
|
753
|
-
if (!message || typeof message !== 'object') return;
|
|
754
|
-
if (message.role === 'tool') {
|
|
755
|
-
messages.push(buildUserToolResultMessage(message));
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
if (message.role === 'assistant' && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
759
|
-
const assistantContent = [];
|
|
760
|
-
const text = extractText(message.content);
|
|
761
|
-
if (text) {
|
|
762
|
-
assistantContent.push({ type: 'text', text });
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
message.tool_calls.forEach(toolCall => {
|
|
766
|
-
if (!toolCall || typeof toolCall !== 'object') return;
|
|
767
|
-
const functionPayload = (toolCall.function && typeof toolCall.function === 'object')
|
|
768
|
-
? toolCall.function
|
|
769
|
-
: toolCall;
|
|
770
|
-
const name = functionPayload.name || toolCall.name;
|
|
771
|
-
if (!name) return;
|
|
772
|
-
assistantContent.push({
|
|
773
|
-
type: 'tool_use',
|
|
774
|
-
id: toolCall.id || functionPayload.call_id || generateToolCallId(),
|
|
775
|
-
name,
|
|
776
|
-
input: parseToolArguments(functionPayload.arguments ?? functionPayload.input)
|
|
777
|
-
});
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
if (assistantContent.length > 0) {
|
|
781
|
-
messages.push({
|
|
782
|
-
role: 'assistant',
|
|
783
|
-
content: assistantContent
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
appendMessage(message.role, message.content);
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (messages.length === 0) {
|
|
793
|
-
messages.push({
|
|
794
|
-
role: 'user',
|
|
795
|
-
content: [{ type: 'text', text: 'Hello' }]
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
379
|
|
|
799
|
-
return {
|
|
800
|
-
system: systemParts.join('\n\n').trim(),
|
|
801
|
-
messages
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
380
|
|
|
805
381
|
function buildClaudeCodeUserId(sessionIdSeed = '') {
|
|
806
382
|
const preferredUserId = resolveClaudePreferredUserId();
|
|
@@ -812,548 +388,8 @@ function buildClaudeCodeUserId(sessionIdSeed = '') {
|
|
|
812
388
|
return `user_${accountId}_account__session_${sessionId}`;
|
|
813
389
|
}
|
|
814
390
|
|
|
815
|
-
function normalizeClaudeMetadata(metadata, fallbackUserId = '') {
|
|
816
|
-
const normalized = (metadata && typeof metadata === 'object' && !Array.isArray(metadata))
|
|
817
|
-
? { ...metadata }
|
|
818
|
-
: {};
|
|
819
|
-
const userId = normalizeSessionKeyValue(normalized.user_id);
|
|
820
|
-
const fallback = normalizeSessionKeyValue(fallbackUserId);
|
|
821
|
-
|
|
822
|
-
if (CLAUDE_USER_ID_FULL_RE.test(userId)) {
|
|
823
|
-
normalized.user_id = userId;
|
|
824
|
-
return normalized;
|
|
825
|
-
}
|
|
826
|
-
if (CLAUDE_USER_ID_FULL_RE.test(fallback)) {
|
|
827
|
-
normalized.user_id = fallback;
|
|
828
|
-
return normalized;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
normalized.user_id = buildClaudeCodeUserId(fallback);
|
|
832
|
-
return normalized;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function convertOpenCodePayloadToClaude(pathname, payload = {}, fallbackModel = '', options = {}) {
|
|
836
|
-
const normalized = normalizeOpenCodeMessages(pathname, payload);
|
|
837
|
-
const maxTokens = Number(payload.max_output_tokens ?? payload.max_tokens);
|
|
838
|
-
|
|
839
|
-
const converted = {
|
|
840
|
-
model: payload.model || fallbackModel || 'claude-sonnet-4-20250514',
|
|
841
|
-
max_tokens: Number.isFinite(maxTokens) && maxTokens > 0 ? Math.round(maxTokens) : 4096,
|
|
842
|
-
stream: false,
|
|
843
|
-
messages: normalized.messages
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
const template = resolveClaudeRequestTemplate();
|
|
847
|
-
|
|
848
|
-
converted.system = buildClaudeSystemBlocks(normalized.system, template.system);
|
|
849
|
-
|
|
850
|
-
const tools = normalizeOpenAiToolsToClaude(payload.tools || []);
|
|
851
|
-
converted.tools = tools.length > 0 ? tools : template.tools;
|
|
852
|
-
|
|
853
|
-
const toolChoice = normalizeToolChoiceToClaude(payload.tool_choice);
|
|
854
|
-
if (toolChoice) {
|
|
855
|
-
converted.tool_choice = toolChoice;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
converted.temperature = Number.isFinite(Number(payload.temperature)) ? Number(payload.temperature) : 1;
|
|
859
|
-
if (Number.isFinite(Number(payload.top_p))) {
|
|
860
|
-
converted.top_p = Number(payload.top_p);
|
|
861
|
-
}
|
|
862
|
-
if (Number.isFinite(Number(payload.top_k))) {
|
|
863
|
-
converted.top_k = Number(payload.top_k);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// 某些 Claude relay 会校验 metadata.user_id 以识别 Claude Code 请求
|
|
867
|
-
converted.metadata = normalizeClaudeMetadata(payload.metadata, options.sessionUserId || template.userId || '');
|
|
868
|
-
|
|
869
|
-
return converted;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function normalizeOpenAiToolsToGemini(tools = []) {
|
|
873
|
-
if (!Array.isArray(tools)) return [];
|
|
874
|
-
|
|
875
|
-
const functionDeclarations = [];
|
|
876
|
-
for (const tool of tools) {
|
|
877
|
-
if (!tool || typeof tool !== 'object') continue;
|
|
878
|
-
|
|
879
|
-
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
880
|
-
const fn = tool.function;
|
|
881
|
-
if (!fn.name) continue;
|
|
882
|
-
functionDeclarations.push({
|
|
883
|
-
name: fn.name,
|
|
884
|
-
description: fn.description || '',
|
|
885
|
-
parameters: fn.parameters || { type: 'object', properties: {} }
|
|
886
|
-
});
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (tool.type === 'function' && tool.name) {
|
|
891
|
-
functionDeclarations.push({
|
|
892
|
-
name: tool.name,
|
|
893
|
-
description: tool.description || '',
|
|
894
|
-
parameters: tool.parameters || { type: 'object', properties: {} }
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
if (functionDeclarations.length === 0) return [];
|
|
900
|
-
return [{ functionDeclarations }];
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function normalizeToolChoiceToGemini(toolChoice) {
|
|
904
|
-
if (!toolChoice) return undefined;
|
|
905
391
|
|
|
906
|
-
if (typeof toolChoice === 'string') {
|
|
907
|
-
if (toolChoice === 'auto') {
|
|
908
|
-
return { functionCallingConfig: { mode: 'AUTO' } };
|
|
909
|
-
}
|
|
910
|
-
if (toolChoice === 'required') {
|
|
911
|
-
return { functionCallingConfig: { mode: 'ANY' } };
|
|
912
|
-
}
|
|
913
|
-
if (toolChoice === 'none') {
|
|
914
|
-
return { functionCallingConfig: { mode: 'NONE' } };
|
|
915
|
-
}
|
|
916
|
-
return undefined;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (typeof toolChoice === 'object') {
|
|
920
|
-
const functionName = toolChoice.function?.name || toolChoice.name;
|
|
921
|
-
if (toolChoice.type === 'function' && functionName) {
|
|
922
|
-
return {
|
|
923
|
-
functionCallingConfig: {
|
|
924
|
-
mode: 'ANY',
|
|
925
|
-
allowedFunctionNames: [functionName]
|
|
926
|
-
}
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
if (toolChoice.type === 'auto') {
|
|
930
|
-
return { functionCallingConfig: { mode: 'AUTO' } };
|
|
931
|
-
}
|
|
932
|
-
if (toolChoice.type === 'required') {
|
|
933
|
-
return { functionCallingConfig: { mode: 'ANY' } };
|
|
934
|
-
}
|
|
935
|
-
if (toolChoice.type === 'none') {
|
|
936
|
-
return { functionCallingConfig: { mode: 'NONE' } };
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
return undefined;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function normalizeStopSequences(stopValue) {
|
|
944
|
-
if (!stopValue) return undefined;
|
|
945
|
-
if (typeof stopValue === 'string' && stopValue.trim()) {
|
|
946
|
-
return [stopValue];
|
|
947
|
-
}
|
|
948
|
-
if (Array.isArray(stopValue)) {
|
|
949
|
-
const sequences = stopValue
|
|
950
|
-
.filter(item => typeof item === 'string')
|
|
951
|
-
.map(item => item.trim())
|
|
952
|
-
.filter(Boolean);
|
|
953
|
-
return sequences.length > 0 ? sequences : undefined;
|
|
954
|
-
}
|
|
955
|
-
return undefined;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function normalizeGeminiFunctionResponsePayload(value) {
|
|
959
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
960
|
-
return value;
|
|
961
|
-
}
|
|
962
|
-
if (typeof value === 'string') {
|
|
963
|
-
const trimmed = value.trim();
|
|
964
|
-
if (!trimmed) return { content: '' };
|
|
965
|
-
try {
|
|
966
|
-
const parsed = JSON.parse(trimmed);
|
|
967
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
968
|
-
return parsed;
|
|
969
|
-
}
|
|
970
|
-
} catch {
|
|
971
|
-
return { content: value };
|
|
972
|
-
}
|
|
973
|
-
return { content: value };
|
|
974
|
-
}
|
|
975
|
-
return { content: normalizeToolResultContent(value) };
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
function buildGeminiContents(messages = []) {
|
|
979
|
-
const contents = [];
|
|
980
|
-
const toolNameById = new Map();
|
|
981
|
-
|
|
982
|
-
for (const message of messages) {
|
|
983
|
-
if (!message || typeof message !== 'object') continue;
|
|
984
|
-
const role = message.role === 'assistant' ? 'model' : 'user';
|
|
985
|
-
const contentBlocks = Array.isArray(message.content) ? message.content : [message.content];
|
|
986
|
-
const parts = [];
|
|
987
|
-
|
|
988
|
-
for (const block of contentBlocks) {
|
|
989
|
-
if (!block || typeof block !== 'object') {
|
|
990
|
-
const text = extractText(block);
|
|
991
|
-
if (text) {
|
|
992
|
-
parts.push({ text });
|
|
993
|
-
}
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
if (block.type === 'tool_use' && block.name) {
|
|
998
|
-
const callId = String(block.id || generateToolCallId());
|
|
999
|
-
const args = (block.input && typeof block.input === 'object' && !Array.isArray(block.input))
|
|
1000
|
-
? block.input
|
|
1001
|
-
: {};
|
|
1002
|
-
toolNameById.set(callId, block.name);
|
|
1003
|
-
parts.push({
|
|
1004
|
-
functionCall: {
|
|
1005
|
-
name: block.name,
|
|
1006
|
-
args
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
continue;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (block.type === 'tool_result') {
|
|
1013
|
-
const toolUseId = String(block.tool_use_id || block.id || '');
|
|
1014
|
-
const toolName = block.name || toolNameById.get(toolUseId);
|
|
1015
|
-
if (!toolName) {
|
|
1016
|
-
const text = normalizeToolResultContent(block.content);
|
|
1017
|
-
if (text) {
|
|
1018
|
-
parts.push({ text });
|
|
1019
|
-
}
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
parts.push({
|
|
1024
|
-
functionResponse: {
|
|
1025
|
-
name: toolName,
|
|
1026
|
-
response: normalizeGeminiFunctionResponsePayload(block.content)
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const text = extractText(block);
|
|
1033
|
-
if (text) {
|
|
1034
|
-
parts.push({ text });
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
if (parts.length === 0) continue;
|
|
1039
|
-
contents.push({ role, parts });
|
|
1040
|
-
}
|
|
1041
|
-
return contents;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function cloneJsonCompatible(value) {
|
|
1045
|
-
try {
|
|
1046
|
-
return JSON.parse(JSON.stringify(value));
|
|
1047
|
-
} catch {
|
|
1048
|
-
return value;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
function normalizeCodexResponsesInput(inputValue) {
|
|
1053
|
-
if (typeof inputValue === 'string') {
|
|
1054
|
-
return [
|
|
1055
|
-
{
|
|
1056
|
-
type: 'message',
|
|
1057
|
-
role: 'user',
|
|
1058
|
-
content: [
|
|
1059
|
-
{
|
|
1060
|
-
type: 'input_text',
|
|
1061
|
-
text: inputValue
|
|
1062
|
-
}
|
|
1063
|
-
]
|
|
1064
|
-
}
|
|
1065
|
-
];
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (!Array.isArray(inputValue)) return undefined;
|
|
1069
|
-
return inputValue.map(item => {
|
|
1070
|
-
if (!item || typeof item !== 'object') return item;
|
|
1071
|
-
const clonedItem = cloneJsonCompatible(item);
|
|
1072
|
-
if (String(clonedItem?.role || '').trim().toLowerCase() === 'system') {
|
|
1073
|
-
clonedItem.role = 'developer';
|
|
1074
|
-
}
|
|
1075
|
-
return clonedItem;
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function convertOpenCodePayloadToCodexResponses(payload = {}, fallbackModel = '') {
|
|
1080
|
-
const requestBody = cloneJsonCompatible((payload && typeof payload === 'object') ? payload : {});
|
|
1081
|
-
if (requestBody.model === undefined && fallbackModel) {
|
|
1082
|
-
requestBody.model = fallbackModel;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
const normalizedInput = normalizeCodexResponsesInput(requestBody.input);
|
|
1086
|
-
if (normalizedInput !== undefined) {
|
|
1087
|
-
requestBody.input = normalizedInput;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
requestBody.stream = true;
|
|
1091
|
-
requestBody.store = false;
|
|
1092
|
-
if (requestBody.parallel_tool_calls === undefined) {
|
|
1093
|
-
requestBody.parallel_tool_calls = true;
|
|
1094
|
-
}
|
|
1095
|
-
if (typeof requestBody.instructions !== 'string') {
|
|
1096
|
-
requestBody.instructions = '';
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
const include = Array.isArray(requestBody.include)
|
|
1100
|
-
? requestBody.include.filter(item => typeof item === 'string' && item.trim())
|
|
1101
|
-
: [];
|
|
1102
|
-
if (!include.includes('reasoning.encrypted_content')) {
|
|
1103
|
-
include.push('reasoning.encrypted_content');
|
|
1104
|
-
}
|
|
1105
|
-
requestBody.include = include;
|
|
1106
|
-
|
|
1107
|
-
delete requestBody.max_output_tokens;
|
|
1108
|
-
delete requestBody.max_completion_tokens;
|
|
1109
|
-
delete requestBody.temperature;
|
|
1110
|
-
delete requestBody.top_p;
|
|
1111
|
-
delete requestBody.service_tier;
|
|
1112
|
-
delete requestBody.user;
|
|
1113
|
-
delete requestBody.previous_response_id;
|
|
1114
|
-
delete requestBody.prompt_cache_retention;
|
|
1115
|
-
delete requestBody.safety_identifier;
|
|
1116
|
-
|
|
1117
|
-
return {
|
|
1118
|
-
requestBody,
|
|
1119
|
-
model: requestBody.model || fallbackModel || ''
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function convertOpenCodePayloadToGemini(pathname, payload = {}, fallbackModel = '') {
|
|
1124
|
-
const normalized = normalizeOpenCodeMessages(pathname, payload);
|
|
1125
|
-
const maxTokens = Number(payload.max_output_tokens ?? payload.max_tokens);
|
|
1126
|
-
const stopSequences = normalizeStopSequences(payload.stop);
|
|
1127
|
-
const tools = normalizeOpenAiToolsToGemini(payload.tools || []);
|
|
1128
|
-
const toolConfig = normalizeToolChoiceToGemini(payload.tool_choice);
|
|
1129
|
-
|
|
1130
|
-
const requestBody = {
|
|
1131
|
-
contents: buildGeminiContents(normalized.messages)
|
|
1132
|
-
};
|
|
1133
|
-
|
|
1134
|
-
if (normalized.system) {
|
|
1135
|
-
requestBody.systemInstruction = {
|
|
1136
|
-
parts: [{ text: normalized.system }]
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
const generationConfig = {};
|
|
1141
|
-
if (Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
1142
|
-
generationConfig.maxOutputTokens = Math.round(maxTokens);
|
|
1143
|
-
}
|
|
1144
|
-
if (Number.isFinite(Number(payload.temperature))) {
|
|
1145
|
-
generationConfig.temperature = Number(payload.temperature);
|
|
1146
|
-
}
|
|
1147
|
-
if (Number.isFinite(Number(payload.top_p))) {
|
|
1148
|
-
generationConfig.topP = Number(payload.top_p);
|
|
1149
|
-
}
|
|
1150
|
-
if (Number.isFinite(Number(payload.top_k))) {
|
|
1151
|
-
generationConfig.topK = Number(payload.top_k);
|
|
1152
|
-
}
|
|
1153
|
-
if (stopSequences) {
|
|
1154
|
-
generationConfig.stopSequences = stopSequences;
|
|
1155
|
-
}
|
|
1156
|
-
if (Object.keys(generationConfig).length > 0) {
|
|
1157
|
-
requestBody.generationConfig = generationConfig;
|
|
1158
|
-
}
|
|
1159
392
|
|
|
1160
|
-
if (tools.length > 0) {
|
|
1161
|
-
requestBody.tools = tools;
|
|
1162
|
-
}
|
|
1163
|
-
if (toolConfig) {
|
|
1164
|
-
requestBody.toolConfig = toolConfig;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
return {
|
|
1168
|
-
model: payload.model || fallbackModel || '',
|
|
1169
|
-
requestBody
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function buildClaudeTargetUrl(baseUrl = '') {
|
|
1174
|
-
let targetUrl;
|
|
1175
|
-
try {
|
|
1176
|
-
targetUrl = new URL(String(baseUrl || '').trim() || 'https://api.anthropic.com');
|
|
1177
|
-
} catch {
|
|
1178
|
-
targetUrl = new URL('https://api.anthropic.com');
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
1182
|
-
if (!pathname || pathname === '/') {
|
|
1183
|
-
pathname = '/v1/messages';
|
|
1184
|
-
} else if (pathname.endsWith('/messages')) {
|
|
1185
|
-
// noop
|
|
1186
|
-
} else if (pathname.endsWith('/v1')) {
|
|
1187
|
-
pathname = `${pathname}/messages`;
|
|
1188
|
-
} else {
|
|
1189
|
-
pathname = `${pathname}/v1/messages`;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
targetUrl.pathname = pathname;
|
|
1193
|
-
targetUrl.searchParams.set('beta', 'true');
|
|
1194
|
-
return targetUrl.toString();
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function buildClaudeCountTokensTargetUrl(baseUrl = '') {
|
|
1198
|
-
let targetUrl;
|
|
1199
|
-
try {
|
|
1200
|
-
targetUrl = new URL(String(baseUrl || '').trim() || 'https://api.anthropic.com');
|
|
1201
|
-
} catch {
|
|
1202
|
-
targetUrl = new URL('https://api.anthropic.com');
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
1206
|
-
if (!pathname || pathname === '/') {
|
|
1207
|
-
pathname = '/v1/messages/count_tokens';
|
|
1208
|
-
} else if (pathname.endsWith('/messages/count_tokens')) {
|
|
1209
|
-
// noop
|
|
1210
|
-
} else if (pathname.endsWith('/messages')) {
|
|
1211
|
-
pathname = `${pathname}/count_tokens`;
|
|
1212
|
-
} else if (pathname.endsWith('/v1')) {
|
|
1213
|
-
pathname = `${pathname}/messages/count_tokens`;
|
|
1214
|
-
} else {
|
|
1215
|
-
pathname = `${pathname}/v1/messages/count_tokens`;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
targetUrl.pathname = pathname;
|
|
1219
|
-
targetUrl.searchParams.set('beta', 'true');
|
|
1220
|
-
return targetUrl.toString();
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function shouldUseGeminiCliFormat(baseUrl = '') {
|
|
1224
|
-
let parsedUrl;
|
|
1225
|
-
try {
|
|
1226
|
-
parsedUrl = new URL(String(baseUrl || '').trim() || 'https://generativelanguage.googleapis.com');
|
|
1227
|
-
} catch {
|
|
1228
|
-
return false;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
const host = String(parsedUrl.hostname || '').toLowerCase();
|
|
1232
|
-
const pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
1233
|
-
|
|
1234
|
-
if (pathname.includes('/v1internal') || pathname.endsWith(':generateContent') || pathname.endsWith(':streamGenerateContent')) {
|
|
1235
|
-
return true;
|
|
1236
|
-
}
|
|
1237
|
-
if (pathname.includes('/v1beta') || pathname.includes('/models/')) {
|
|
1238
|
-
return false;
|
|
1239
|
-
}
|
|
1240
|
-
if (host.includes('cloudcode-pa.googleapis.com')) {
|
|
1241
|
-
return true;
|
|
1242
|
-
}
|
|
1243
|
-
if (!pathname || pathname === '/') {
|
|
1244
|
-
return !host.includes('generativelanguage.googleapis.com') && !host.includes('aiplatform.googleapis.com');
|
|
1245
|
-
}
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function buildGeminiCliTargetPath(parsedUrl, stream = false) {
|
|
1250
|
-
let pathname = parsedUrl.pathname.replace(/\/+$/, '');
|
|
1251
|
-
const method = stream ? 'streamGenerateContent' : 'generateContent';
|
|
1252
|
-
|
|
1253
|
-
if (!pathname || pathname === '/') {
|
|
1254
|
-
return `/v1internal:${method}`;
|
|
1255
|
-
}
|
|
1256
|
-
if (pathname.endsWith(':streamGenerateContent')) {
|
|
1257
|
-
return stream
|
|
1258
|
-
? pathname
|
|
1259
|
-
: pathname.replace(/:streamGenerateContent$/, ':generateContent');
|
|
1260
|
-
}
|
|
1261
|
-
if (pathname.endsWith(':generateContent')) {
|
|
1262
|
-
return stream
|
|
1263
|
-
? pathname.replace(/:generateContent$/, ':streamGenerateContent')
|
|
1264
|
-
: pathname;
|
|
1265
|
-
}
|
|
1266
|
-
if (pathname.endsWith('/v1internal')) {
|
|
1267
|
-
return `${pathname}:${method}`;
|
|
1268
|
-
}
|
|
1269
|
-
return `${pathname}/v1internal:${method}`;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
function buildGeminiCliTargetUrl(baseUrl = '', options = {}) {
|
|
1273
|
-
const stream = !!options.stream;
|
|
1274
|
-
|
|
1275
|
-
let targetUrl;
|
|
1276
|
-
try {
|
|
1277
|
-
targetUrl = new URL(String(baseUrl || '').trim() || 'https://cloudcode-pa.googleapis.com');
|
|
1278
|
-
} catch {
|
|
1279
|
-
targetUrl = new URL('https://cloudcode-pa.googleapis.com');
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
targetUrl.pathname = buildGeminiCliTargetPath(targetUrl, stream);
|
|
1283
|
-
if (stream) {
|
|
1284
|
-
targetUrl.searchParams.set('alt', 'sse');
|
|
1285
|
-
}
|
|
1286
|
-
return targetUrl.toString();
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function buildGeminiNativeTargetUrl(baseUrl = '', model = '', apiKey = '', options = {}) {
|
|
1290
|
-
const modelName = String(model || '').trim();
|
|
1291
|
-
if (!modelName) return '';
|
|
1292
|
-
const stream = !!options.stream;
|
|
1293
|
-
|
|
1294
|
-
let targetUrl;
|
|
1295
|
-
try {
|
|
1296
|
-
targetUrl = new URL(String(baseUrl || '').trim() || 'https://generativelanguage.googleapis.com');
|
|
1297
|
-
} catch {
|
|
1298
|
-
targetUrl = new URL('https://generativelanguage.googleapis.com');
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
1302
|
-
const modelsIndex = pathname.indexOf('/models');
|
|
1303
|
-
if (modelsIndex >= 0) {
|
|
1304
|
-
pathname = pathname.slice(0, modelsIndex);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
let apiBasePath;
|
|
1308
|
-
if (!pathname || pathname === '/') {
|
|
1309
|
-
apiBasePath = '/v1beta';
|
|
1310
|
-
} else if (pathname.endsWith('/v1beta') || pathname.endsWith('/v1')) {
|
|
1311
|
-
apiBasePath = pathname;
|
|
1312
|
-
} else {
|
|
1313
|
-
apiBasePath = `${pathname}/v1beta`;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
const method = stream ? 'streamGenerateContent' : 'generateContent';
|
|
1317
|
-
targetUrl.pathname = `${apiBasePath}/models/${encodeURIComponent(modelName)}:${method}`;
|
|
1318
|
-
if (apiKey) {
|
|
1319
|
-
targetUrl.searchParams.set('key', apiKey);
|
|
1320
|
-
}
|
|
1321
|
-
if (stream) {
|
|
1322
|
-
targetUrl.searchParams.set('alt', 'sse');
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
return targetUrl.toString();
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function buildGeminiTargetUrl(baseUrl = '', model = '', apiKey = '', options = {}) {
|
|
1329
|
-
if (options.useCli) {
|
|
1330
|
-
return buildGeminiCliTargetUrl(baseUrl, options);
|
|
1331
|
-
}
|
|
1332
|
-
return buildGeminiNativeTargetUrl(baseUrl, model, apiKey, options);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
function buildCodexTargetUrl(baseUrl = '') {
|
|
1336
|
-
let targetUrl;
|
|
1337
|
-
try {
|
|
1338
|
-
targetUrl = new URL(String(baseUrl || '').trim());
|
|
1339
|
-
} catch {
|
|
1340
|
-
return '';
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
let pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
1344
|
-
if (!pathname || pathname === '/') {
|
|
1345
|
-
pathname = '/responses';
|
|
1346
|
-
} else if (pathname.endsWith('/responses') || pathname.endsWith('/v1/responses')) {
|
|
1347
|
-
// noop
|
|
1348
|
-
} else if (pathname.endsWith('/v1')) {
|
|
1349
|
-
pathname = `${pathname}/responses`;
|
|
1350
|
-
} else {
|
|
1351
|
-
pathname = `${pathname}/responses`;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
targetUrl.pathname = pathname;
|
|
1355
|
-
return targetUrl.toString();
|
|
1356
|
-
}
|
|
1357
393
|
|
|
1358
394
|
function createDecodedStream(res) {
|
|
1359
395
|
const encoding = String(res.headers['content-encoding'] || '').toLowerCase();
|
|
@@ -1472,9 +508,11 @@ function buildClaudeCountTokensPayload(claudePayload = {}) {
|
|
|
1472
508
|
|
|
1473
509
|
async function preflightClaudeCountTokens(baseUrl, apiKey, claudePayload, options = {}) {
|
|
1474
510
|
const countTokensPayload = buildClaudeCountTokensPayload(claudePayload);
|
|
1475
|
-
const countTokensHeaders =
|
|
1476
|
-
|
|
1477
|
-
hasTools: options.hasTools
|
|
511
|
+
const countTokensHeaders = buildClaudeCountTokensHeaders(apiKey, {
|
|
512
|
+
baseUrl,
|
|
513
|
+
hasTools: !!options.hasTools,
|
|
514
|
+
headers: options.headers,
|
|
515
|
+
providerConfig: options.providerConfig
|
|
1478
516
|
});
|
|
1479
517
|
try {
|
|
1480
518
|
await postJson(buildClaudeCountTokensTargetUrl(baseUrl), countTokensHeaders, countTokensPayload, 30000);
|
|
@@ -2916,19 +1954,24 @@ async function handleClaudeGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
2916
1954
|
: sessionKey;
|
|
2917
1955
|
const preferredUserId = normalizeSessionKeyValue(originalPayload?.metadata?.user_id);
|
|
2918
1956
|
const sessionUserId = resolveClaudeUserIdBySession(scopedSessionKey, preferredUserId);
|
|
2919
|
-
const
|
|
2920
|
-
|
|
1957
|
+
const converted = createClaudeRequest(pathname, originalPayload, {
|
|
1958
|
+
apiKey: effectiveKey,
|
|
1959
|
+
baseUrl: channel.baseUrl,
|
|
1960
|
+
fallbackModel: channel.model,
|
|
1961
|
+
stream: streamResponses,
|
|
1962
|
+
sessionUserId,
|
|
1963
|
+
providerConfig: channel.providerConfig
|
|
2921
1964
|
});
|
|
2922
|
-
claudePayload
|
|
1965
|
+
const claudePayload = converted.body;
|
|
1966
|
+
const headers = converted.headers;
|
|
2923
1967
|
const hasTools = Array.isArray(claudePayload.tools) && claudePayload.tools.length > 0;
|
|
2924
1968
|
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
1969
|
+
await preflightClaudeCountTokens(channel.baseUrl, effectiveKey, claudePayload, {
|
|
1970
|
+
hasTools,
|
|
1971
|
+
headers: channel.providerConfig?.headers,
|
|
1972
|
+
providerConfig: channel.providerConfig
|
|
2928
1973
|
});
|
|
2929
1974
|
|
|
2930
|
-
await preflightClaudeCountTokens(channel.baseUrl, effectiveKey, claudePayload, { hasTools });
|
|
2931
|
-
|
|
2932
1975
|
if (streamResponses) {
|
|
2933
1976
|
let streamUpstream;
|
|
2934
1977
|
try {
|
|
@@ -3107,7 +2150,12 @@ async function handleCodexGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
3107
2150
|
const startTime = Date.now();
|
|
3108
2151
|
const originalPayload = (req.body && typeof req.body === 'object') ? req.body : {};
|
|
3109
2152
|
const wantsStream = !!originalPayload.stream;
|
|
3110
|
-
const
|
|
2153
|
+
const sessionId = extractSessionIdFromRequest(req, originalPayload);
|
|
2154
|
+
const converted = createCodexRequest(originalPayload, {
|
|
2155
|
+
apiKey: effectiveKey,
|
|
2156
|
+
fallbackModel: channel.model,
|
|
2157
|
+
sessionId
|
|
2158
|
+
});
|
|
3111
2159
|
const targetModel = converted.model;
|
|
3112
2160
|
|
|
3113
2161
|
if (!targetModel) {
|
|
@@ -3133,29 +2181,9 @@ async function handleCodexGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
3133
2181
|
});
|
|
3134
2182
|
}
|
|
3135
2183
|
|
|
3136
|
-
const codexSessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 15)}`;
|
|
3137
|
-
const promptCacheKey = (typeof converted.requestBody.prompt_cache_key === 'string' && converted.requestBody.prompt_cache_key.trim())
|
|
3138
|
-
? converted.requestBody.prompt_cache_key.trim()
|
|
3139
|
-
: codexSessionId;
|
|
3140
|
-
converted.requestBody.prompt_cache_key = promptCacheKey;
|
|
3141
|
-
|
|
3142
|
-
const headers = {
|
|
3143
|
-
authorization: `Bearer ${effectiveKey}`,
|
|
3144
|
-
'openai-beta': 'responses=experimental',
|
|
3145
|
-
accept: 'text/event-stream',
|
|
3146
|
-
'accept-encoding': 'gzip, deflate, br',
|
|
3147
|
-
connection: 'Keep-Alive',
|
|
3148
|
-
'content-type': 'application/json',
|
|
3149
|
-
Version: CODEX_CLI_VERSION,
|
|
3150
|
-
Session_id: promptCacheKey,
|
|
3151
|
-
Conversation_id: promptCacheKey,
|
|
3152
|
-
Originator: 'codex_cli_rs',
|
|
3153
|
-
'user-agent': CODEX_CLI_USER_AGENT
|
|
3154
|
-
};
|
|
3155
|
-
|
|
3156
2184
|
let streamUpstream;
|
|
3157
2185
|
try {
|
|
3158
|
-
streamUpstream = await postJsonStream(targetUrl, headers, converted.
|
|
2186
|
+
streamUpstream = await postJsonStream(targetUrl, converted.headers, converted.body, 120000);
|
|
3159
2187
|
} catch (error) {
|
|
3160
2188
|
return reportOpenCodeGatewayFailure({
|
|
3161
2189
|
req,
|
|
@@ -3169,6 +2197,7 @@ async function handleCodexGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
3169
2197
|
});
|
|
3170
2198
|
}
|
|
3171
2199
|
|
|
2200
|
+
|
|
3172
2201
|
const statusCode = Number(streamUpstream.statusCode) || 500;
|
|
3173
2202
|
if (statusCode < 200 || statusCode >= 300) {
|
|
3174
2203
|
let rawBody = '';
|
|
@@ -3926,9 +2955,26 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
3926
2955
|
const originalPayload = (req.body && typeof req.body === 'object') ? req.body : {};
|
|
3927
2956
|
const wantsStream = !!originalPayload.stream;
|
|
3928
2957
|
const streamResponses = wantsStream && isResponsesPath(pathname);
|
|
3929
|
-
|
|
2958
|
+
// Resolve useCli by canonical providerApi precedence:
|
|
2959
|
+
// explicit google-gemini-cli → true; google-generative-ai/google-vertex → false;
|
|
2960
|
+
// missing/non-canonical → URL-based detection
|
|
2961
|
+
const explicitApi = String(channel?.providerApi || '').trim().toLowerCase();
|
|
2962
|
+
const useGeminiCli = explicitApi === 'google-gemini-cli'
|
|
2963
|
+
? true
|
|
2964
|
+
: (explicitApi === 'google-generative-ai' || explicitApi === 'google-vertex'
|
|
2965
|
+
? false
|
|
2966
|
+
: shouldUseGeminiCliFormat(channel.baseUrl));
|
|
2967
|
+
|
|
2968
|
+
const converted = createGeminiRequest(pathname, originalPayload, {
|
|
2969
|
+
apiKey: effectiveKey,
|
|
2970
|
+
fallbackModel: channel.model,
|
|
2971
|
+
stream: streamResponses,
|
|
2972
|
+
useCli: useGeminiCli,
|
|
2973
|
+
providerConfig: channel.providerConfig
|
|
2974
|
+
});
|
|
3930
2975
|
const targetModel = converted.model;
|
|
3931
|
-
const
|
|
2976
|
+
const geminiPayload = converted.body;
|
|
2977
|
+
const headers = converted.headers;
|
|
3932
2978
|
|
|
3933
2979
|
if (!targetModel) {
|
|
3934
2980
|
return reportOpenCodeGatewayFailure({
|
|
@@ -3956,34 +3002,6 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
|
|
|
3956
3002
|
});
|
|
3957
3003
|
}
|
|
3958
3004
|
|
|
3959
|
-
const geminiPayload = useGeminiCli
|
|
3960
|
-
? {
|
|
3961
|
-
project: '',
|
|
3962
|
-
model: targetModel,
|
|
3963
|
-
request: converted.requestBody
|
|
3964
|
-
}
|
|
3965
|
-
: converted.requestBody;
|
|
3966
|
-
|
|
3967
|
-
const headers = useGeminiCli
|
|
3968
|
-
? {
|
|
3969
|
-
'x-goog-api-key': effectiveKey,
|
|
3970
|
-
'authorization': `Bearer ${effectiveKey}`,
|
|
3971
|
-
'content-type': 'application/json',
|
|
3972
|
-
'accept': streamResponses ? 'text/event-stream' : 'application/json',
|
|
3973
|
-
'accept-encoding': 'gzip, deflate, br',
|
|
3974
|
-
'user-agent': GEMINI_CLI_USER_AGENT,
|
|
3975
|
-
'x-goog-api-client': GEMINI_CLI_API_CLIENT,
|
|
3976
|
-
'client-metadata': GEMINI_CLI_CLIENT_METADATA
|
|
3977
|
-
}
|
|
3978
|
-
: {
|
|
3979
|
-
'x-goog-api-key': effectiveKey,
|
|
3980
|
-
'authorization': `Bearer ${effectiveKey}`,
|
|
3981
|
-
'content-type': 'application/json',
|
|
3982
|
-
'accept': streamResponses ? 'text/event-stream' : 'application/json',
|
|
3983
|
-
'accept-encoding': 'gzip, deflate, br',
|
|
3984
|
-
'user-agent': 'google-genai-sdk/0.8.0'
|
|
3985
|
-
};
|
|
3986
|
-
|
|
3987
3005
|
if (streamResponses) {
|
|
3988
3006
|
let streamUpstream;
|
|
3989
3007
|
try {
|