clawmarket 0.6.0 → 0.6.1
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/dist/local-proxy.js +3226 -109
- package/dist/local-proxy.js.map +1 -1
- package/dist/seller-daemon.js +222 -39
- package/dist/seller-daemon.js.map +1 -1
- package/package.json +1 -1
package/dist/local-proxy.js
CHANGED
|
@@ -51,7 +51,10 @@ const MIN_BALANCE = BigInt(process.env.MIN_BALANCE || DEFAULT_CONFIG.minBalance.
|
|
|
51
51
|
const CHANNEL_DEPOSIT = BigInt(process.env.CHANNEL_DEPOSIT || DEFAULT_CONFIG.channelDeposit.toString());
|
|
52
52
|
const CHANNEL_DURATION = BigInt(7 * 24 * 60 * 60);
|
|
53
53
|
const COST_PER_REQUEST = 100000n; // 初始 ticket 0.1 USDC(预授权)
|
|
54
|
+
const RECOVER_NONCE_STEP = 10000n; // 0.01 USDC,恢复时用于生成安全的 nonce 基线
|
|
54
55
|
const SKIP_SELLER_STAKE_CHECK = false;
|
|
56
|
+
// 默认关闭自动恢复旧通道,避免链上 settledAmount 与卖家本地票据状态不一致导致 nonce/increment 错误。
|
|
57
|
+
const ENABLE_CHANNEL_RECOVERY = process.env.CLAW_ENABLE_CHANNEL_RECOVERY === 'true';
|
|
55
58
|
// ============ 模型路由配置 ============
|
|
56
59
|
const MODEL_FALLBACK = {
|
|
57
60
|
'claude-opus-4-6': ['MiniMax-M2.5'],
|
|
@@ -60,6 +63,7 @@ const MODEL_FALLBACK = {
|
|
|
60
63
|
// 默认禁用跨模型/静态模型降级,避免把用户请求切到其它模型。
|
|
61
64
|
const ENABLE_CROSS_MODEL_FALLBACK = process.env.CLAW_ENABLE_CROSS_MODEL_FALLBACK === 'true';
|
|
62
65
|
const ENABLE_STATIC_MODEL_FALLBACK = process.env.CLAW_ENABLE_STATIC_MODEL_FALLBACK === 'true';
|
|
66
|
+
const ENABLE_FORCED_BROWSER_OPEN = process.env.CLAW_ENABLE_FORCED_BROWSER_OPEN === 'true';
|
|
63
67
|
const MARKET_CACHE_TTL_MS = 10_000;
|
|
64
68
|
const MARKET_FETCH_TIMEOUT_MS = 4_000;
|
|
65
69
|
const RELAY_CONNECT_TIMEOUT_MS = 8_000;
|
|
@@ -67,9 +71,11 @@ const RELAY_ACCEPT_TIMEOUT_MS = 12_000;
|
|
|
67
71
|
const STREAM_FIRST_CHUNK_TIMEOUT_MS = readMsEnv('CLAW_STREAM_FIRST_CHUNK_TIMEOUT_MS', 300_000);
|
|
68
72
|
const STREAM_IDLE_TIMEOUT_MS = readMsEnv('CLAW_STREAM_IDLE_TIMEOUT_MS', 300_000);
|
|
69
73
|
const STREAM_MAX_DURATION_MS = readMsEnv('CLAW_STREAM_MAX_DURATION_MS', 300_000);
|
|
74
|
+
const SSE_HEARTBEAT_MS = readMsEnv('CLAW_SSE_HEARTBEAT_MS', 10_000);
|
|
70
75
|
const SELLER_FAILURE_BACKOFF_BASE_MS = 15_000;
|
|
71
76
|
const SELLER_FAILURE_BACKOFF_MAX_MS = 300_000;
|
|
72
77
|
const SHOW_MODEL_FALLBACK_NOTICE = process.env.CLAW_SHOW_MODEL_FALLBACK_NOTICE !== 'false';
|
|
78
|
+
const DEBUG_BRIDGE = process.env.CLAW_DEBUG_BRIDGE === '1';
|
|
73
79
|
const CANDIDATE_ROUTE_TIMEOUT_MS = readMsEnv('CLAW_CANDIDATE_ROUTE_TIMEOUT_MS', 20_000);
|
|
74
80
|
const MODEL_CANDIDATE_ROUTE_TIMEOUT_MS = {
|
|
75
81
|
'gpt-5.2': readMsEnv('CLAW_GPT52_CANDIDATE_ROUTE_TIMEOUT_MS', 300_000),
|
|
@@ -102,6 +108,18 @@ let defaultSeller = '0x3137cE5612af147f1BA17eBba7e8B46594ed3e26';
|
|
|
102
108
|
let marketCache = { fetchedAt: 0, sellers: [] };
|
|
103
109
|
let sellerRuntime = new Map();
|
|
104
110
|
const ticketSignLocks = new Map();
|
|
111
|
+
const channelOpenLocks = new Map();
|
|
112
|
+
const sseHeartbeats = new WeakMap();
|
|
113
|
+
function debugBridge(tag, data) {
|
|
114
|
+
if (!DEBUG_BRIDGE)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
console.log(`[bridge:${tag}] ${JSON.stringify(data)}`);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
console.log(`[bridge:${tag}]`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
105
123
|
async function withTicketSignLock(channelId, fn) {
|
|
106
124
|
const key = channelId.toLowerCase();
|
|
107
125
|
const previous = ticketSignLocks.get(key) ?? Promise.resolve();
|
|
@@ -339,14 +357,29 @@ function getSellerCandidates(sellers, model) {
|
|
|
339
357
|
}
|
|
340
358
|
async function ensureChannelForSeller(sellerAddress) {
|
|
341
359
|
const key = sellerAddress.toLowerCase();
|
|
342
|
-
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
360
|
+
const existingLock = channelOpenLocks.get(key);
|
|
361
|
+
if (existingLock) {
|
|
362
|
+
return existingLock;
|
|
363
|
+
}
|
|
364
|
+
const task = (async () => {
|
|
365
|
+
let channel = channels.get(key);
|
|
366
|
+
if (channel && (channel.expiresAt < Date.now() / 1000 || channel.spent + COST_PER_REQUEST > channel.deposit)) {
|
|
367
|
+
channels.delete(key);
|
|
368
|
+
channel = undefined;
|
|
369
|
+
}
|
|
370
|
+
if (channel)
|
|
371
|
+
return channel;
|
|
372
|
+
return openChannel(sellerAddress);
|
|
373
|
+
})();
|
|
374
|
+
channelOpenLocks.set(key, task);
|
|
375
|
+
try {
|
|
376
|
+
return await task;
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
if (channelOpenLocks.get(key) === task) {
|
|
380
|
+
channelOpenLocks.delete(key);
|
|
381
|
+
}
|
|
346
382
|
}
|
|
347
|
-
if (channel)
|
|
348
|
-
return channel;
|
|
349
|
-
return openChannel(sellerAddress);
|
|
350
383
|
}
|
|
351
384
|
function isBuyerMismatchError(reason) {
|
|
352
385
|
const normalized = reason.toLowerCase();
|
|
@@ -354,6 +387,15 @@ function isBuyerMismatchError(reason) {
|
|
|
354
387
|
normalized.includes('signer mismatch') ||
|
|
355
388
|
normalized.includes('channel buyer mismatch'));
|
|
356
389
|
}
|
|
390
|
+
function isChannelStateSyncError(reason) {
|
|
391
|
+
const normalized = reason.toLowerCase();
|
|
392
|
+
return (normalized.includes('nonce not increasing') ||
|
|
393
|
+
normalized.includes('nonce too low') ||
|
|
394
|
+
normalized.includes('invalid nonce') ||
|
|
395
|
+
normalized.includes('increment too small') ||
|
|
396
|
+
normalized.includes('amount not increasing') ||
|
|
397
|
+
normalized.includes('ticket amount must increase'));
|
|
398
|
+
}
|
|
357
399
|
function invalidateSellerChannel(sellerAddress, reason) {
|
|
358
400
|
const key = sellerAddress.toLowerCase();
|
|
359
401
|
const existing = channels.get(key);
|
|
@@ -396,10 +438,25 @@ async function recoverChannelFromChain(sellerAddress) {
|
|
|
396
438
|
const channelData = await publicClient.readContract({ address: CHANNEL_CONTRACT, abi: CHANNEL_ABI, functionName: 'channels', args: [channelId] });
|
|
397
439
|
if (!channelData[6])
|
|
398
440
|
continue;
|
|
441
|
+
const deposit = channelData[2];
|
|
442
|
+
const settledAmount = channelData[3];
|
|
443
|
+
// 已经没有可用额度的通道不要恢复,避免反复命中 Amount > deposit。
|
|
444
|
+
if (settledAmount + COST_PER_REQUEST > deposit)
|
|
445
|
+
continue;
|
|
399
446
|
const expiresAt = Number(channelData[4]);
|
|
400
447
|
if (expiresAt < Math.floor(Date.now() / 1000))
|
|
401
448
|
continue;
|
|
402
|
-
|
|
449
|
+
// 卖家侧只要求 nonce 递增,不要求从 1 开始;恢复时给一个保守较大的基线,
|
|
450
|
+
// 避免本地丢状态后出现 Nonce not increasing。
|
|
451
|
+
const recoveredNonce = settledAmount / RECOVER_NONCE_STEP;
|
|
452
|
+
const channel = {
|
|
453
|
+
channelId,
|
|
454
|
+
seller: sellerAddress,
|
|
455
|
+
deposit,
|
|
456
|
+
spent: settledAmount,
|
|
457
|
+
nonce: recoveredNonce,
|
|
458
|
+
expiresAt,
|
|
459
|
+
};
|
|
403
460
|
channels.set(sellerAddress.toLowerCase(), channel);
|
|
404
461
|
saveChannels();
|
|
405
462
|
console.log('[恢复] 通道:', channelId.slice(0, 18) + '...');
|
|
@@ -414,9 +471,11 @@ async function recoverChannelFromChain(sellerAddress) {
|
|
|
414
471
|
}
|
|
415
472
|
async function openChannel(sellerAddress) {
|
|
416
473
|
console.log('[链上] 开通道到:', sellerAddress);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
474
|
+
if (ENABLE_CHANNEL_RECOVERY) {
|
|
475
|
+
const recovered = await recoverChannelFromChain(sellerAddress);
|
|
476
|
+
if (recovered)
|
|
477
|
+
return recovered;
|
|
478
|
+
}
|
|
420
479
|
const ethBalance = await getETHBalance();
|
|
421
480
|
if (ethBalance < parseUnits('0.001', 18))
|
|
422
481
|
throw new Error('ETH 不足');
|
|
@@ -486,6 +545,180 @@ function parseRelayErrorPayload(payload) {
|
|
|
486
545
|
}
|
|
487
546
|
return 'unknown relay error';
|
|
488
547
|
}
|
|
548
|
+
function parseRelayAnthropicEventChunk(payload) {
|
|
549
|
+
let parsed = payload;
|
|
550
|
+
if (typeof parsed === 'string') {
|
|
551
|
+
try {
|
|
552
|
+
parsed = JSON.parse(parsed);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!parsed || typeof parsed !== 'object')
|
|
559
|
+
return null;
|
|
560
|
+
const chunk = parsed;
|
|
561
|
+
if (chunk.__clawmarket_stream_v1 !== true)
|
|
562
|
+
return null;
|
|
563
|
+
if (chunk.kind !== 'anthropic_event')
|
|
564
|
+
return null;
|
|
565
|
+
if (typeof chunk.event !== 'string' || !chunk.event.trim())
|
|
566
|
+
return null;
|
|
567
|
+
if (!chunk.data || typeof chunk.data !== 'object')
|
|
568
|
+
return null;
|
|
569
|
+
return {
|
|
570
|
+
event: chunk.event,
|
|
571
|
+
data: chunk.data,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function parseRelayOpenAIEventChunk(payload) {
|
|
575
|
+
let parsed = payload;
|
|
576
|
+
if (typeof parsed === 'string') {
|
|
577
|
+
try {
|
|
578
|
+
parsed = JSON.parse(parsed);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!parsed || typeof parsed !== 'object')
|
|
585
|
+
return null;
|
|
586
|
+
const chunk = parsed;
|
|
587
|
+
if (chunk.__clawmarket_stream_v1 !== true)
|
|
588
|
+
return null;
|
|
589
|
+
if (chunk.kind !== 'openai_event')
|
|
590
|
+
return null;
|
|
591
|
+
if (!chunk.data || typeof chunk.data !== 'object')
|
|
592
|
+
return null;
|
|
593
|
+
return {
|
|
594
|
+
data: chunk.data,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function extractJsonObjectsFromConcatenatedText(text) {
|
|
598
|
+
const out = [];
|
|
599
|
+
if (!text || typeof text !== 'string')
|
|
600
|
+
return out;
|
|
601
|
+
let depth = 0;
|
|
602
|
+
let inString = false;
|
|
603
|
+
let escaped = false;
|
|
604
|
+
let start = -1;
|
|
605
|
+
for (let i = 0; i < text.length; i++) {
|
|
606
|
+
const ch = text[i];
|
|
607
|
+
if (inString) {
|
|
608
|
+
if (escaped) {
|
|
609
|
+
escaped = false;
|
|
610
|
+
}
|
|
611
|
+
else if (ch === '\\') {
|
|
612
|
+
escaped = true;
|
|
613
|
+
}
|
|
614
|
+
else if (ch === '"') {
|
|
615
|
+
inString = false;
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (ch === '"') {
|
|
620
|
+
inString = true;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (ch === '{') {
|
|
624
|
+
if (depth === 0)
|
|
625
|
+
start = i;
|
|
626
|
+
depth++;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (ch === '}') {
|
|
630
|
+
if (depth <= 0)
|
|
631
|
+
continue;
|
|
632
|
+
depth--;
|
|
633
|
+
if (depth === 0 && start >= 0) {
|
|
634
|
+
const candidate = text.slice(start, i + 1).trim();
|
|
635
|
+
start = -1;
|
|
636
|
+
if (!candidate)
|
|
637
|
+
continue;
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(candidate);
|
|
640
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
641
|
+
out.push(parsed);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// ignore invalid segment
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function parseRelayStructuredChunks(payload) {
|
|
653
|
+
const chunks = [];
|
|
654
|
+
const anthropicSingle = parseRelayAnthropicEventChunk(payload);
|
|
655
|
+
if (anthropicSingle) {
|
|
656
|
+
chunks.push({
|
|
657
|
+
kind: 'anthropic',
|
|
658
|
+
event: anthropicSingle.event,
|
|
659
|
+
data: anthropicSingle.data,
|
|
660
|
+
});
|
|
661
|
+
return chunks;
|
|
662
|
+
}
|
|
663
|
+
const openAISingle = parseRelayOpenAIEventChunk(payload);
|
|
664
|
+
if (openAISingle) {
|
|
665
|
+
chunks.push({
|
|
666
|
+
kind: 'openai',
|
|
667
|
+
data: openAISingle.data,
|
|
668
|
+
});
|
|
669
|
+
return chunks;
|
|
670
|
+
}
|
|
671
|
+
if (typeof payload !== 'string')
|
|
672
|
+
return chunks;
|
|
673
|
+
const text = payload.trim();
|
|
674
|
+
if (!text.includes('__clawmarket_stream_v1'))
|
|
675
|
+
return chunks;
|
|
676
|
+
const objects = extractJsonObjectsFromConcatenatedText(text);
|
|
677
|
+
for (const obj of objects) {
|
|
678
|
+
const anthropicChunk = parseRelayAnthropicEventChunk(obj);
|
|
679
|
+
if (anthropicChunk) {
|
|
680
|
+
chunks.push({
|
|
681
|
+
kind: 'anthropic',
|
|
682
|
+
event: anthropicChunk.event,
|
|
683
|
+
data: anthropicChunk.data,
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const openAIChunk = parseRelayOpenAIEventChunk(obj);
|
|
688
|
+
if (openAIChunk) {
|
|
689
|
+
chunks.push({
|
|
690
|
+
kind: 'openai',
|
|
691
|
+
data: openAIChunk.data,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return chunks;
|
|
696
|
+
}
|
|
697
|
+
function extractAnthropicTextDelta(eventName, eventData) {
|
|
698
|
+
if (eventName !== 'content_block_delta')
|
|
699
|
+
return '';
|
|
700
|
+
const delta = eventData.delta;
|
|
701
|
+
if (!delta || typeof delta !== 'object')
|
|
702
|
+
return '';
|
|
703
|
+
const anyDelta = delta;
|
|
704
|
+
const deltaType = typeof anyDelta.type === 'string' ? anyDelta.type : '';
|
|
705
|
+
if (deltaType && deltaType !== 'text_delta')
|
|
706
|
+
return '';
|
|
707
|
+
return typeof anyDelta.text === 'string' ? anyDelta.text : '';
|
|
708
|
+
}
|
|
709
|
+
function extractOpenAITextDelta(eventData) {
|
|
710
|
+
const choices = eventData.choices;
|
|
711
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
712
|
+
return '';
|
|
713
|
+
const first = choices[0];
|
|
714
|
+
if (!first || typeof first !== 'object')
|
|
715
|
+
return '';
|
|
716
|
+
const delta = first.delta;
|
|
717
|
+
if (!delta || typeof delta !== 'object')
|
|
718
|
+
return '';
|
|
719
|
+
const content = delta.content;
|
|
720
|
+
return typeof content === 'string' ? content : '';
|
|
721
|
+
}
|
|
489
722
|
function isFundingError(message) {
|
|
490
723
|
return message.includes('USDC 不足') || message.includes('ETH 不足');
|
|
491
724
|
}
|
|
@@ -497,9 +730,45 @@ function isRetryableRouteError(reason) {
|
|
|
497
730
|
|| normalized === 'relay_connect_timeout'
|
|
498
731
|
|| normalized === 'relay_closed_before_stream'
|
|
499
732
|
|| normalized === 'empty_response_before_content'
|
|
733
|
+
|| normalized.includes('nonce not increasing')
|
|
734
|
+
|| normalized.includes('nonce too low')
|
|
735
|
+
|| normalized.includes('invalid nonce')
|
|
736
|
+
|| normalized.includes('seller busy')
|
|
737
|
+
|| normalized.includes('upstream api error')
|
|
738
|
+
|| normalized.includes('upstream done without visible content')
|
|
739
|
+
|| normalized.includes('codex_exec_silence_timeout')
|
|
500
740
|
|| normalized.startsWith('relay_ws_error');
|
|
501
741
|
}
|
|
502
|
-
function
|
|
742
|
+
function stopSSEHeartbeat(res) {
|
|
743
|
+
const timer = sseHeartbeats.get(res);
|
|
744
|
+
if (!timer)
|
|
745
|
+
return;
|
|
746
|
+
clearInterval(timer);
|
|
747
|
+
sseHeartbeats.delete(res);
|
|
748
|
+
}
|
|
749
|
+
function startSSEHeartbeat(res) {
|
|
750
|
+
if (!Number.isFinite(SSE_HEARTBEAT_MS) || SSE_HEARTBEAT_MS <= 0)
|
|
751
|
+
return;
|
|
752
|
+
stopSSEHeartbeat(res);
|
|
753
|
+
const timer = setInterval(() => {
|
|
754
|
+
if (res.writableEnded || res.destroyed) {
|
|
755
|
+
stopSSEHeartbeat(res);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
// SSE 注释行作为 keepalive,不影响上层解析。
|
|
760
|
+
res.write(': keep-alive\n\n');
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
stopSSEHeartbeat(res);
|
|
764
|
+
}
|
|
765
|
+
}, SSE_HEARTBEAT_MS);
|
|
766
|
+
if (typeof timer.unref === 'function') {
|
|
767
|
+
timer.unref();
|
|
768
|
+
}
|
|
769
|
+
sseHeartbeats.set(res, timer);
|
|
770
|
+
}
|
|
771
|
+
function startAnthropicSSE(res, requestedModel, servedModel, sellerId) {
|
|
503
772
|
const downgraded = requestedModel !== servedModel;
|
|
504
773
|
res.writeHead(200, {
|
|
505
774
|
'Content-Type': 'text/event-stream',
|
|
@@ -515,6 +784,17 @@ function startAnthropicSSE(res, messageId, requestedModel, servedModel, sellerId
|
|
|
515
784
|
served_model: servedModel,
|
|
516
785
|
seller_id: sellerId,
|
|
517
786
|
})}\n\n`);
|
|
787
|
+
startSSEHeartbeat(res);
|
|
788
|
+
}
|
|
789
|
+
function ensureAnthropicTextStreamStarted(res, messageId, servedModel) {
|
|
790
|
+
writeAnthropicMessageStart(res, messageId, servedModel);
|
|
791
|
+
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
792
|
+
type: 'content_block_start',
|
|
793
|
+
index: 0,
|
|
794
|
+
content_block: { type: 'text', text: '' },
|
|
795
|
+
})}\n\n`);
|
|
796
|
+
}
|
|
797
|
+
function writeAnthropicMessageStart(res, messageId, servedModel) {
|
|
518
798
|
res.write(`event: message_start\ndata: ${JSON.stringify({
|
|
519
799
|
type: 'message_start',
|
|
520
800
|
message: {
|
|
@@ -526,14 +806,6 @@ function startAnthropicSSE(res, messageId, requestedModel, servedModel, sellerId
|
|
|
526
806
|
usage: { input_tokens: 0, output_tokens: 0 },
|
|
527
807
|
},
|
|
528
808
|
})}\n\n`);
|
|
529
|
-
res.write(`event: content_block_start\ndata: ${JSON.stringify({
|
|
530
|
-
type: 'content_block_start',
|
|
531
|
-
index: 0,
|
|
532
|
-
content_block: { type: 'text', text: '' },
|
|
533
|
-
})}\n\n`);
|
|
534
|
-
if (SHOW_MODEL_FALLBACK_NOTICE && downgraded) {
|
|
535
|
-
writeAnthropicDelta(res, `[ClawMarket 测试提示] 请求模型 ${requestedModel} 当前不可用,已自动切换到 ${servedModel}。\n\n`);
|
|
536
|
-
}
|
|
537
809
|
}
|
|
538
810
|
function writeAnthropicDelta(res, text) {
|
|
539
811
|
res.write(`event: content_block_delta\ndata: ${JSON.stringify({
|
|
@@ -542,7 +814,12 @@ function writeAnthropicDelta(res, text) {
|
|
|
542
814
|
delta: { type: 'text_delta', text },
|
|
543
815
|
})}\n\n`);
|
|
544
816
|
}
|
|
817
|
+
function writeAnthropicEvent(res, eventName, eventData) {
|
|
818
|
+
const safeName = eventName.trim() || 'message_delta';
|
|
819
|
+
res.write(`event: ${safeName}\ndata: ${JSON.stringify(eventData)}\n\n`);
|
|
820
|
+
}
|
|
545
821
|
function endAnthropicSSE(res, outputTokens) {
|
|
822
|
+
stopSSEHeartbeat(res);
|
|
546
823
|
res.write(`event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: 0 })}\n\n`);
|
|
547
824
|
res.write(`event: message_delta\ndata: ${JSON.stringify({
|
|
548
825
|
type: 'message_delta',
|
|
@@ -553,6 +830,7 @@ function endAnthropicSSE(res, outputTokens) {
|
|
|
553
830
|
res.end();
|
|
554
831
|
}
|
|
555
832
|
function failAnthropicSSE(res, message) {
|
|
833
|
+
stopSSEHeartbeat(res);
|
|
556
834
|
if (!res.headersSent) {
|
|
557
835
|
res.writeHead(200, {
|
|
558
836
|
'Content-Type': 'text/event-stream',
|
|
@@ -594,6 +872,7 @@ function startOpenAISSE(res, completionId, createdAt, requestedModel, servedMode
|
|
|
594
872
|
},
|
|
595
873
|
],
|
|
596
874
|
});
|
|
875
|
+
startSSEHeartbeat(res);
|
|
597
876
|
if (SHOW_MODEL_FALLBACK_NOTICE && downgraded) {
|
|
598
877
|
writeOpenAIDelta(res, completionId, createdAt, servedModel, `[ClawMarket 测试提示] 请求模型 ${requestedModel} 当前不可用,已自动切换到 ${servedModel}。\n\n`);
|
|
599
878
|
}
|
|
@@ -613,7 +892,91 @@ function writeOpenAIDelta(res, completionId, createdAt, servedModel, text) {
|
|
|
613
892
|
],
|
|
614
893
|
});
|
|
615
894
|
}
|
|
616
|
-
function
|
|
895
|
+
function sanitizeOpenAIToolCallDelta(raw, fallbackIndex) {
|
|
896
|
+
if (!raw || typeof raw !== 'object')
|
|
897
|
+
return null;
|
|
898
|
+
const tool = raw;
|
|
899
|
+
const index = Number.isFinite(Number(tool.index)) ? Number(tool.index) : fallbackIndex;
|
|
900
|
+
const functionData = tool.function && typeof tool.function === 'object'
|
|
901
|
+
? tool.function
|
|
902
|
+
: {};
|
|
903
|
+
const rawArguments = functionData.arguments;
|
|
904
|
+
let argsText = '';
|
|
905
|
+
if (typeof rawArguments === 'string') {
|
|
906
|
+
argsText = rawArguments;
|
|
907
|
+
}
|
|
908
|
+
else if (rawArguments !== undefined) {
|
|
909
|
+
try {
|
|
910
|
+
argsText = JSON.stringify(rawArguments);
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
argsText = String(rawArguments);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const name = typeof functionData.name === 'string' ? functionData.name : undefined;
|
|
917
|
+
const id = typeof tool.id === 'string' ? tool.id : undefined;
|
|
918
|
+
return {
|
|
919
|
+
index,
|
|
920
|
+
...(id ? { id } : {}),
|
|
921
|
+
...(name ? { name } : {}),
|
|
922
|
+
...(argsText ? { arguments: argsText } : {}),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, calls) {
|
|
926
|
+
if (calls.length === 0)
|
|
927
|
+
return;
|
|
928
|
+
const toolCalls = calls.map((call) => ({
|
|
929
|
+
index: call.index,
|
|
930
|
+
...(call.id ? { id: call.id } : {}),
|
|
931
|
+
type: 'function',
|
|
932
|
+
function: {
|
|
933
|
+
...(call.name ? { name: call.name } : {}),
|
|
934
|
+
arguments: call.arguments ?? '',
|
|
935
|
+
},
|
|
936
|
+
}));
|
|
937
|
+
writeOpenAIChunk(res, {
|
|
938
|
+
id: completionId,
|
|
939
|
+
object: 'chat.completion.chunk',
|
|
940
|
+
created: createdAt,
|
|
941
|
+
model: servedModel,
|
|
942
|
+
choices: [
|
|
943
|
+
{
|
|
944
|
+
index: 0,
|
|
945
|
+
delta: { tool_calls: toolCalls },
|
|
946
|
+
finish_reason: null,
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function applyOpenAIToolCallDelta(toolCalls, delta) {
|
|
952
|
+
const current = toolCalls.get(delta.index) || {
|
|
953
|
+
index: delta.index,
|
|
954
|
+
id: delta.id || `call_${delta.index}`,
|
|
955
|
+
name: delta.name || 'tool',
|
|
956
|
+
arguments: '',
|
|
957
|
+
};
|
|
958
|
+
if (delta.id)
|
|
959
|
+
current.id = delta.id;
|
|
960
|
+
if (delta.name)
|
|
961
|
+
current.name = delta.name;
|
|
962
|
+
if (delta.arguments)
|
|
963
|
+
current.arguments += delta.arguments;
|
|
964
|
+
toolCalls.set(delta.index, current);
|
|
965
|
+
}
|
|
966
|
+
function toolCallStateToOpenAIList(toolCalls) {
|
|
967
|
+
return [...toolCalls.values()]
|
|
968
|
+
.sort((a, b) => a.index - b.index)
|
|
969
|
+
.map((call) => ({
|
|
970
|
+
id: call.id,
|
|
971
|
+
type: 'function',
|
|
972
|
+
function: {
|
|
973
|
+
name: call.name,
|
|
974
|
+
arguments: call.arguments || '',
|
|
975
|
+
},
|
|
976
|
+
}));
|
|
977
|
+
}
|
|
978
|
+
function endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens, finishReason = 'stop') {
|
|
979
|
+
stopSSEHeartbeat(res);
|
|
617
980
|
writeOpenAIChunk(res, {
|
|
618
981
|
id: completionId,
|
|
619
982
|
object: 'chat.completion.chunk',
|
|
@@ -623,7 +986,7 @@ function endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens) {
|
|
|
623
986
|
{
|
|
624
987
|
index: 0,
|
|
625
988
|
delta: {},
|
|
626
|
-
finish_reason:
|
|
989
|
+
finish_reason: finishReason,
|
|
627
990
|
},
|
|
628
991
|
],
|
|
629
992
|
usage: {
|
|
@@ -636,6 +999,7 @@ function endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens) {
|
|
|
636
999
|
res.end();
|
|
637
1000
|
}
|
|
638
1001
|
function failOpenAISSE(res, message) {
|
|
1002
|
+
stopSSEHeartbeat(res);
|
|
639
1003
|
if (!res.headersSent) {
|
|
640
1004
|
res.writeHead(200, {
|
|
641
1005
|
'Content-Type': 'text/event-stream',
|
|
@@ -679,11 +1043,34 @@ function normalizeOpenAIMessages(rawMessages, rawInput) {
|
|
|
679
1043
|
if (!raw || typeof raw !== 'object')
|
|
680
1044
|
continue;
|
|
681
1045
|
const msg = raw;
|
|
682
|
-
const
|
|
683
|
-
|
|
1046
|
+
const role = typeof msg.role === 'string' ? msg.role : 'user';
|
|
1047
|
+
const normalizedMsg = { role };
|
|
1048
|
+
if ('content' in msg) {
|
|
1049
|
+
normalizedMsg.content = msg.content ?? null;
|
|
1050
|
+
}
|
|
1051
|
+
if (typeof msg.tool_call_id === 'string' && msg.tool_call_id.trim()) {
|
|
1052
|
+
normalizedMsg.tool_call_id = msg.tool_call_id.trim();
|
|
1053
|
+
}
|
|
1054
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
1055
|
+
normalizedMsg.tool_calls = msg.tool_calls;
|
|
1056
|
+
}
|
|
1057
|
+
if (msg.function_call && typeof msg.function_call === 'object') {
|
|
1058
|
+
normalizedMsg.function_call = msg.function_call;
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof msg.name === 'string' && msg.name.trim()) {
|
|
1061
|
+
normalizedMsg.name = msg.name.trim();
|
|
1062
|
+
}
|
|
1063
|
+
if (!('content' in normalizedMsg)) {
|
|
1064
|
+
const text = extractTextFromOpenAIContent(msg.content).trim();
|
|
1065
|
+
if (text)
|
|
1066
|
+
normalizedMsg.content = text;
|
|
1067
|
+
}
|
|
1068
|
+
if (!('content' in normalizedMsg)
|
|
1069
|
+
&& !('tool_calls' in normalizedMsg)
|
|
1070
|
+
&& !('function_call' in normalizedMsg)) {
|
|
684
1071
|
continue;
|
|
685
|
-
|
|
686
|
-
normalized.push(
|
|
1072
|
+
}
|
|
1073
|
+
normalized.push(normalizedMsg);
|
|
687
1074
|
}
|
|
688
1075
|
}
|
|
689
1076
|
if (normalized.length === 0) {
|
|
@@ -713,6 +1100,7 @@ function normalizeOpenAIRequest(rawRequest) {
|
|
|
713
1100
|
model: requestedModel,
|
|
714
1101
|
stream: true,
|
|
715
1102
|
messages,
|
|
1103
|
+
_openai_compat: true,
|
|
716
1104
|
};
|
|
717
1105
|
const maxCompletionTokens = Number(rawRequest.max_completion_tokens);
|
|
718
1106
|
const maxTokens = Number(rawRequest.max_tokens);
|
|
@@ -730,6 +1118,18 @@ function normalizeOpenAIRequest(rawRequest) {
|
|
|
730
1118
|
if (Number.isFinite(topP)) {
|
|
731
1119
|
requestPayload.top_p = topP;
|
|
732
1120
|
}
|
|
1121
|
+
if (Array.isArray(rawRequest.tools)) {
|
|
1122
|
+
requestPayload.tools = rawRequest.tools;
|
|
1123
|
+
}
|
|
1124
|
+
if (rawRequest.tool_choice !== undefined) {
|
|
1125
|
+
requestPayload.tool_choice = rawRequest.tool_choice;
|
|
1126
|
+
}
|
|
1127
|
+
if (typeof rawRequest.parallel_tool_calls === 'boolean') {
|
|
1128
|
+
requestPayload.parallel_tool_calls = rawRequest.parallel_tool_calls;
|
|
1129
|
+
}
|
|
1130
|
+
if (rawRequest.response_format && typeof rawRequest.response_format === 'object') {
|
|
1131
|
+
requestPayload.response_format = rawRequest.response_format;
|
|
1132
|
+
}
|
|
733
1133
|
return {
|
|
734
1134
|
requestedModel,
|
|
735
1135
|
stream,
|
|
@@ -809,6 +1209,233 @@ function isCursorOpenAIChallenge(rawRequest) {
|
|
|
809
1209
|
const content = extractTextFromOpenAIContent(last.content).toLowerCase();
|
|
810
1210
|
return content.includes('test prompt using gpt-3.5-turbo');
|
|
811
1211
|
}
|
|
1212
|
+
function isAnthropicLikeModelName(model) {
|
|
1213
|
+
const lower = model.toLowerCase();
|
|
1214
|
+
return (lower.includes('claude')
|
|
1215
|
+
|| lower.includes('anthropic')
|
|
1216
|
+
|| lower.includes('minimax')
|
|
1217
|
+
|| lower.includes('m2'));
|
|
1218
|
+
}
|
|
1219
|
+
function parseToolArguments(raw) {
|
|
1220
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
1221
|
+
return raw;
|
|
1222
|
+
}
|
|
1223
|
+
if (typeof raw === 'string') {
|
|
1224
|
+
const text = raw.trim();
|
|
1225
|
+
if (!text)
|
|
1226
|
+
return {};
|
|
1227
|
+
try {
|
|
1228
|
+
const parsed = JSON.parse(text);
|
|
1229
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1230
|
+
return parsed;
|
|
1231
|
+
}
|
|
1232
|
+
return { value: parsed };
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
return { raw: text };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (raw === undefined || raw === null)
|
|
1239
|
+
return {};
|
|
1240
|
+
return { value: raw };
|
|
1241
|
+
}
|
|
1242
|
+
function convertOpenAIToolsToAnthropic(rawTools) {
|
|
1243
|
+
if (!Array.isArray(rawTools))
|
|
1244
|
+
return [];
|
|
1245
|
+
const tools = [];
|
|
1246
|
+
for (const item of rawTools) {
|
|
1247
|
+
if (!item || typeof item !== 'object')
|
|
1248
|
+
continue;
|
|
1249
|
+
const tool = item;
|
|
1250
|
+
if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
|
|
1251
|
+
const func = tool.function;
|
|
1252
|
+
if (typeof func.name !== 'string' || !func.name.trim())
|
|
1253
|
+
continue;
|
|
1254
|
+
const inputSchema = (func.parameters && typeof func.parameters === 'object')
|
|
1255
|
+
? func.parameters
|
|
1256
|
+
: { type: 'object', properties: {}, additionalProperties: true };
|
|
1257
|
+
tools.push({
|
|
1258
|
+
name: func.name.trim(),
|
|
1259
|
+
...(typeof func.description === 'string' && func.description.trim()
|
|
1260
|
+
? { description: func.description.trim() }
|
|
1261
|
+
: {}),
|
|
1262
|
+
input_schema: inputSchema,
|
|
1263
|
+
});
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (typeof tool.name === 'string' && tool.name.trim()) {
|
|
1267
|
+
const inputSchema = (tool.input_schema && typeof tool.input_schema === 'object')
|
|
1268
|
+
? tool.input_schema
|
|
1269
|
+
: { type: 'object', properties: {}, additionalProperties: true };
|
|
1270
|
+
tools.push({
|
|
1271
|
+
name: tool.name.trim(),
|
|
1272
|
+
...(typeof tool.description === 'string' && tool.description.trim()
|
|
1273
|
+
? { description: tool.description.trim() }
|
|
1274
|
+
: {}),
|
|
1275
|
+
input_schema: inputSchema,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return tools;
|
|
1280
|
+
}
|
|
1281
|
+
function mapOpenAIToolChoiceToAnthropic(rawToolChoice) {
|
|
1282
|
+
if (typeof rawToolChoice === 'string') {
|
|
1283
|
+
if (rawToolChoice === 'auto')
|
|
1284
|
+
return { type: 'auto' };
|
|
1285
|
+
if (rawToolChoice === 'required')
|
|
1286
|
+
return { type: 'any' };
|
|
1287
|
+
return undefined;
|
|
1288
|
+
}
|
|
1289
|
+
if (!rawToolChoice || typeof rawToolChoice !== 'object')
|
|
1290
|
+
return undefined;
|
|
1291
|
+
const choice = rawToolChoice;
|
|
1292
|
+
if (choice.type === 'function' && choice.function && typeof choice.function === 'object') {
|
|
1293
|
+
const func = choice.function;
|
|
1294
|
+
if (typeof func.name === 'string' && func.name.trim()) {
|
|
1295
|
+
return { type: 'tool', name: func.name.trim() };
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (choice.type === 'tool' && typeof choice.name === 'string' && choice.name.trim()) {
|
|
1299
|
+
return { type: 'tool', name: choice.name.trim() };
|
|
1300
|
+
}
|
|
1301
|
+
if (choice.type === 'auto' || choice.type === 'any') {
|
|
1302
|
+
return { type: choice.type };
|
|
1303
|
+
}
|
|
1304
|
+
return undefined;
|
|
1305
|
+
}
|
|
1306
|
+
function convertOpenAIRequestToAnthropic(baseRequest, servedModel) {
|
|
1307
|
+
const anthropicRequest = {
|
|
1308
|
+
model: servedModel,
|
|
1309
|
+
stream: true,
|
|
1310
|
+
};
|
|
1311
|
+
const maxTokens = Number(baseRequest.max_tokens);
|
|
1312
|
+
if (Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
1313
|
+
anthropicRequest.max_tokens = Math.floor(maxTokens);
|
|
1314
|
+
}
|
|
1315
|
+
const temperature = Number(baseRequest.temperature);
|
|
1316
|
+
if (Number.isFinite(temperature)) {
|
|
1317
|
+
anthropicRequest.temperature = temperature;
|
|
1318
|
+
}
|
|
1319
|
+
const topP = Number(baseRequest.top_p);
|
|
1320
|
+
if (Number.isFinite(topP)) {
|
|
1321
|
+
anthropicRequest.top_p = topP;
|
|
1322
|
+
}
|
|
1323
|
+
const systemParts = [];
|
|
1324
|
+
const messages = [];
|
|
1325
|
+
const rawMessages = Array.isArray(baseRequest.messages) ? baseRequest.messages : [];
|
|
1326
|
+
for (let idx = 0; idx < rawMessages.length; idx++) {
|
|
1327
|
+
const raw = rawMessages[idx];
|
|
1328
|
+
if (!raw || typeof raw !== 'object')
|
|
1329
|
+
continue;
|
|
1330
|
+
const msg = raw;
|
|
1331
|
+
const role = typeof msg.role === 'string' ? msg.role : 'user';
|
|
1332
|
+
if (role === 'system') {
|
|
1333
|
+
const systemText = extractTextFromOpenAIContent(msg.content).trim();
|
|
1334
|
+
if (systemText)
|
|
1335
|
+
systemParts.push(systemText);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (role === 'tool') {
|
|
1339
|
+
const toolResultText = extractTextFromOpenAIContent(msg.content).trim()
|
|
1340
|
+
|| (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content ?? ''));
|
|
1341
|
+
const toolUseId = typeof msg.tool_call_id === 'string' && msg.tool_call_id.trim()
|
|
1342
|
+
? msg.tool_call_id.trim()
|
|
1343
|
+
: `tool_${idx}`;
|
|
1344
|
+
messages.push({
|
|
1345
|
+
role: 'user',
|
|
1346
|
+
content: [
|
|
1347
|
+
{
|
|
1348
|
+
type: 'tool_result',
|
|
1349
|
+
tool_use_id: toolUseId,
|
|
1350
|
+
content: toolResultText,
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
});
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (role === 'assistant') {
|
|
1357
|
+
const blocks = [];
|
|
1358
|
+
const text = extractTextFromOpenAIContent(msg.content).trim();
|
|
1359
|
+
if (text) {
|
|
1360
|
+
blocks.push({ type: 'text', text });
|
|
1361
|
+
}
|
|
1362
|
+
if (Array.isArray(msg.tool_calls)) {
|
|
1363
|
+
for (let toolIndex = 0; toolIndex < msg.tool_calls.length; toolIndex++) {
|
|
1364
|
+
const rawToolCall = msg.tool_calls[toolIndex];
|
|
1365
|
+
if (!rawToolCall || typeof rawToolCall !== 'object')
|
|
1366
|
+
continue;
|
|
1367
|
+
const toolCall = rawToolCall;
|
|
1368
|
+
const fn = toolCall.function && typeof toolCall.function === 'object'
|
|
1369
|
+
? toolCall.function
|
|
1370
|
+
: {};
|
|
1371
|
+
const name = typeof fn.name === 'string' && fn.name.trim() ? fn.name.trim() : 'tool';
|
|
1372
|
+
const input = parseToolArguments(fn.arguments);
|
|
1373
|
+
const toolId = typeof toolCall.id === 'string' && toolCall.id.trim()
|
|
1374
|
+
? toolCall.id.trim()
|
|
1375
|
+
: `tool_${idx}_${toolIndex}`;
|
|
1376
|
+
blocks.push({
|
|
1377
|
+
type: 'tool_use',
|
|
1378
|
+
id: toolId,
|
|
1379
|
+
name,
|
|
1380
|
+
input,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
else if (msg.function_call && typeof msg.function_call === 'object') {
|
|
1385
|
+
const fn = msg.function_call;
|
|
1386
|
+
const name = typeof fn.name === 'string' && fn.name.trim() ? fn.name.trim() : 'tool';
|
|
1387
|
+
const input = parseToolArguments(fn.arguments);
|
|
1388
|
+
blocks.push({
|
|
1389
|
+
type: 'tool_use',
|
|
1390
|
+
id: `tool_${idx}`,
|
|
1391
|
+
name,
|
|
1392
|
+
input,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
if (blocks.length > 0) {
|
|
1396
|
+
messages.push({ role: 'assistant', content: blocks });
|
|
1397
|
+
}
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
const userText = extractTextFromOpenAIContent(msg.content).trim();
|
|
1401
|
+
if (userText) {
|
|
1402
|
+
messages.push({ role: 'user', content: userText });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (messages.length === 0) {
|
|
1406
|
+
const fallbackInput = extractTextFromOpenAIContent(baseRequest.input).trim();
|
|
1407
|
+
const fallbackPrompt = typeof baseRequest.prompt === 'string' ? baseRequest.prompt.trim() : '';
|
|
1408
|
+
const fallback = fallbackInput || fallbackPrompt;
|
|
1409
|
+
if (fallback) {
|
|
1410
|
+
messages.push({ role: 'user', content: fallback });
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
anthropicRequest.messages = messages.length > 0 ? messages : [{ role: 'user', content: '...' }];
|
|
1414
|
+
if (systemParts.length > 0) {
|
|
1415
|
+
anthropicRequest.system = systemParts.join('\n\n');
|
|
1416
|
+
}
|
|
1417
|
+
const tools = convertOpenAIToolsToAnthropic(baseRequest.tools);
|
|
1418
|
+
if (tools.length > 0) {
|
|
1419
|
+
anthropicRequest.tools = tools;
|
|
1420
|
+
}
|
|
1421
|
+
const toolChoice = mapOpenAIToolChoiceToAnthropic(baseRequest.tool_choice);
|
|
1422
|
+
if (toolChoice) {
|
|
1423
|
+
anthropicRequest.tool_choice = toolChoice;
|
|
1424
|
+
}
|
|
1425
|
+
return anthropicRequest;
|
|
1426
|
+
}
|
|
1427
|
+
function buildRequestPayloadForSeller(baseRequest, servedModel, candidateModel) {
|
|
1428
|
+
if (baseRequest._openai_compat === true && isAnthropicLikeModelName(candidateModel || servedModel)) {
|
|
1429
|
+
return convertOpenAIRequestToAnthropic(baseRequest, servedModel);
|
|
1430
|
+
}
|
|
1431
|
+
const requestPayload = {
|
|
1432
|
+
...baseRequest,
|
|
1433
|
+
model: servedModel,
|
|
1434
|
+
stream: true,
|
|
1435
|
+
};
|
|
1436
|
+
delete requestPayload._openai_compat;
|
|
1437
|
+
return requestPayload;
|
|
1438
|
+
}
|
|
812
1439
|
async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candidate, isClientClosed, attemptBudgetMs, firstChunkBudgetMs, hooks) {
|
|
813
1440
|
if (isClientClosed()) {
|
|
814
1441
|
return { ok: false, streamStarted: false, error: 'client_disconnected', fatal: true };
|
|
@@ -831,8 +1458,7 @@ async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candi
|
|
|
831
1458
|
}
|
|
832
1459
|
console.log(`[Ticket] 初始 #${initialTicket.nonce} 累计: ${formatUnits(BigInt(initialTicket.amount), 6)} USDC`);
|
|
833
1460
|
const requestPayload = {
|
|
834
|
-
...baseRequest,
|
|
835
|
-
model: servedModel,
|
|
1461
|
+
...buildRequestPayloadForSeller(baseRequest, servedModel, candidate.model),
|
|
836
1462
|
_clawmarket: initialTicket,
|
|
837
1463
|
};
|
|
838
1464
|
const wsUrl = `${RELAY_WS_URL}/relay/stream?seller_id=${encodeURIComponent(candidate.sellerId)}`;
|
|
@@ -844,6 +1470,8 @@ async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candi
|
|
|
844
1470
|
let upstreamModel = servedModel;
|
|
845
1471
|
let hasAccepted = false;
|
|
846
1472
|
let hasVisibleChunk = false;
|
|
1473
|
+
let hasStructuredAnthropicChunk = false;
|
|
1474
|
+
let hasStructuredOpenAIChunk = false;
|
|
847
1475
|
let connectTimer = null;
|
|
848
1476
|
let acceptTimer = null;
|
|
849
1477
|
let firstChunkTimer = null;
|
|
@@ -960,10 +1588,51 @@ async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candi
|
|
|
960
1588
|
// ignore invalid payload
|
|
961
1589
|
}
|
|
962
1590
|
}
|
|
1591
|
+
// 上游已确认开始,立即打开 SSE,避免客户端长时间“无响应”。
|
|
1592
|
+
ensureStreamStarted();
|
|
963
1593
|
armFirstChunkTimer();
|
|
964
1594
|
return;
|
|
965
1595
|
}
|
|
966
1596
|
if (messageType === 'stream_chunk') {
|
|
1597
|
+
const structuredChunks = parseRelayStructuredChunks(msg.payload);
|
|
1598
|
+
if (structuredChunks.length > 0) {
|
|
1599
|
+
let handledStructured = false;
|
|
1600
|
+
for (const structuredChunk of structuredChunks) {
|
|
1601
|
+
if (structuredChunk.kind === 'anthropic') {
|
|
1602
|
+
if (!hooks.onAnthropicEvent)
|
|
1603
|
+
continue;
|
|
1604
|
+
handledStructured = true;
|
|
1605
|
+
hasStructuredAnthropicChunk = true;
|
|
1606
|
+
ensureStreamStarted();
|
|
1607
|
+
firstChunkTimer = clearTimer(firstChunkTimer);
|
|
1608
|
+
hooks.onAnthropicEvent(structuredChunk.event, structuredChunk.data);
|
|
1609
|
+
const textDelta = extractAnthropicTextDelta(structuredChunk.event, structuredChunk.data);
|
|
1610
|
+
if (/\S/.test(textDelta)) {
|
|
1611
|
+
hasVisibleChunk = true;
|
|
1612
|
+
}
|
|
1613
|
+
armIdleTimer();
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
handledStructured = true;
|
|
1617
|
+
hasStructuredOpenAIChunk = true;
|
|
1618
|
+
ensureStreamStarted();
|
|
1619
|
+
firstChunkTimer = clearTimer(firstChunkTimer);
|
|
1620
|
+
if (hooks.onOpenAIEvent) {
|
|
1621
|
+
hooks.onOpenAIEvent(structuredChunk.data);
|
|
1622
|
+
}
|
|
1623
|
+
const textDelta = extractOpenAITextDelta(structuredChunk.data);
|
|
1624
|
+
if (/\S/.test(textDelta)) {
|
|
1625
|
+
hasVisibleChunk = true;
|
|
1626
|
+
if (!hooks.onOpenAIEvent) {
|
|
1627
|
+
hooks.onDelta(textDelta);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
armIdleTimer();
|
|
1631
|
+
}
|
|
1632
|
+
if (handledStructured) {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
967
1636
|
const chunkText = typeof msg.payload === 'string' ? msg.payload : String(msg.payload ?? '');
|
|
968
1637
|
const visible = /\S/.test(chunkText);
|
|
969
1638
|
if (!visible) {
|
|
@@ -994,11 +1663,6 @@ async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candi
|
|
|
994
1663
|
if (messageType === 'stream_end') {
|
|
995
1664
|
firstChunkTimer = clearTimer(firstChunkTimer);
|
|
996
1665
|
idleTimer = clearTimer(idleTimer);
|
|
997
|
-
if (!hasVisibleChunk) {
|
|
998
|
-
failBeforeStream('empty_response_before_content');
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
ensureStreamStarted();
|
|
1002
1666
|
let inputTokens = 0;
|
|
1003
1667
|
let outputTokens = 0;
|
|
1004
1668
|
if (typeof msg.payload === 'string') {
|
|
@@ -1011,6 +1675,17 @@ async function streamFromSellerAttempt(req, res, baseRequest, servedModel, candi
|
|
|
1011
1675
|
// ignore invalid usage payload
|
|
1012
1676
|
}
|
|
1013
1677
|
}
|
|
1678
|
+
// 上游偶发“无可见内容 + output_tokens=0”,这会让客户端看起来像卡死。
|
|
1679
|
+
// 这里改为路由层重试(最多 1 次),避免静默空回复。
|
|
1680
|
+
if (!hasVisibleChunk && outputTokens === 0 && !hasStructuredAnthropicChunk && !hasStructuredOpenAIChunk) {
|
|
1681
|
+
console.log('[路由] 上游空响应(无可见内容),触发重试');
|
|
1682
|
+
finish({ ok: false, streamStarted: false, error: 'empty_response_before_content' });
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (!hasVisibleChunk) {
|
|
1686
|
+
console.log('[路由] 上游无可见 chunk,但含 usage,按成功结束处理');
|
|
1687
|
+
}
|
|
1688
|
+
ensureStreamStarted();
|
|
1014
1689
|
const actualCostUsd = (inputTokens / 1_000_000) * candidate.inputPer1m + (outputTokens / 1_000_000) * candidate.outputPer1m;
|
|
1015
1690
|
const actualCostUsdc = BigInt(Math.ceil(actualCostUsd * 1_000_000));
|
|
1016
1691
|
if (actualCostUsdc > COST_PER_REQUEST) {
|
|
@@ -1154,7 +1829,7 @@ async function routeStreamRequest(req, res, request, requestedModel, hooks) {
|
|
|
1154
1829
|
return;
|
|
1155
1830
|
}
|
|
1156
1831
|
const errReason = result.error || 'unknown_error';
|
|
1157
|
-
if (!result.streamStarted && retry === 0 && isBuyerMismatchError(errReason)) {
|
|
1832
|
+
if (!result.streamStarted && retry === 0 && (isBuyerMismatchError(errReason) || isChannelStateSyncError(errReason))) {
|
|
1158
1833
|
invalidateSellerChannel(candidate.walletAddress, errReason);
|
|
1159
1834
|
continue;
|
|
1160
1835
|
}
|
|
@@ -1185,31 +1860,395 @@ async function routeStreamRequest(req, res, request, requestedModel, hooks) {
|
|
|
1185
1860
|
async function handleAnthropicStreamRequest(req, res, body) {
|
|
1186
1861
|
const request = JSON.parse(body);
|
|
1187
1862
|
const requestModel = typeof request.model === 'string' ? request.model.trim() : '';
|
|
1188
|
-
const requestedModel = requestModel || 'claude-opus-4-5';
|
|
1863
|
+
const requestedModel = normalizeRequestedModelId(requestModel || 'claude-opus-4-5');
|
|
1864
|
+
const preferredToolName = resolvePreferredToolNameFromRequest(request);
|
|
1865
|
+
const toolNames = extractToolNamesFromRequest(request);
|
|
1866
|
+
const browserActionEnum = extractBrowserActionEnumFromRequest(request);
|
|
1867
|
+
const toolInputFallbacks = deriveToolInputFallbacksFromRequest(request);
|
|
1868
|
+
debugBridge('anthropic_stream_req', {
|
|
1869
|
+
model: requestedModel,
|
|
1870
|
+
preferredToolName: preferredToolName || null,
|
|
1871
|
+
toolCount: toolNames.length,
|
|
1872
|
+
toolNames: toolNames.slice(0, 12),
|
|
1873
|
+
browserActionEnum: browserActionEnum.slice(0, 20),
|
|
1874
|
+
fallbackExecCommand: toolInputFallbacks.execCommand || null,
|
|
1875
|
+
fallbackWebFetchUrl: toolInputFallbacks.webFetchUrl || null,
|
|
1876
|
+
fallbackBrowserProfile: toolInputFallbacks.browserProfile || null,
|
|
1877
|
+
});
|
|
1189
1878
|
console.log(`[请求] Model: ${requestedModel}, Stream: true`);
|
|
1190
1879
|
const messageId = `msg_${Math.random().toString(36).slice(2, 12)}`;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1880
|
+
let servedModel = requestedModel;
|
|
1881
|
+
let textStreamStarted = false;
|
|
1882
|
+
let sawStructuredAnthropicEvent = false;
|
|
1883
|
+
let sawMessageStop = false;
|
|
1884
|
+
let probeMode = 'probe';
|
|
1885
|
+
let probeBuffer = '';
|
|
1886
|
+
let toolCandidateBuffer = '';
|
|
1887
|
+
let passthroughBuffer = '';
|
|
1888
|
+
let textTranscript = '';
|
|
1889
|
+
let nextToolIndex = 0;
|
|
1890
|
+
const toolCalls = new Map();
|
|
1891
|
+
const ensureTextStreamStarted = () => {
|
|
1892
|
+
if (textStreamStarted || res.writableEnded)
|
|
1893
|
+
return;
|
|
1894
|
+
textStreamStarted = true;
|
|
1895
|
+
ensureAnthropicTextStreamStarted(res, messageId, servedModel);
|
|
1896
|
+
};
|
|
1897
|
+
const appendParsedToolCall = (tool) => {
|
|
1898
|
+
const normalizedBaseName = normalizeToolNameForRequest(tool.name, preferredToolName, toolNames) || tool.name || 'tool';
|
|
1899
|
+
const normalizedName = coerceToolNameByArguments(normalizedBaseName, tool.arguments, toolNames);
|
|
1900
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(tool.arguments, normalizedName, toolInputFallbacks);
|
|
1901
|
+
const toolIndex = nextToolIndex++;
|
|
1902
|
+
applyOpenAIToolCallDelta(toolCalls, {
|
|
1903
|
+
index: toolIndex,
|
|
1904
|
+
id: `toolu_${Math.random().toString(36).slice(2, 12)}`,
|
|
1905
|
+
name: normalizedName,
|
|
1906
|
+
arguments: JSON.stringify(normalizedInput),
|
|
1907
|
+
});
|
|
1908
|
+
};
|
|
1909
|
+
const emitTextDelta = (text) => {
|
|
1910
|
+
if (!text)
|
|
1911
|
+
return;
|
|
1912
|
+
ensureTextStreamStarted();
|
|
1913
|
+
writeAnthropicDelta(res, text);
|
|
1914
|
+
};
|
|
1915
|
+
const flushPassthroughBuffer = (final) => {
|
|
1916
|
+
while (passthroughBuffer.length > 0) {
|
|
1917
|
+
const minimaxStartMatch = findMinimaxToolStart(passthroughBuffer);
|
|
1918
|
+
const minimaxStart = minimaxStartMatch ? minimaxStartMatch.index : -1;
|
|
1919
|
+
const bracketStart = passthroughBuffer.indexOf(BRACKET_TOOL_START);
|
|
1920
|
+
const hasMinimax = minimaxStart >= 0;
|
|
1921
|
+
const hasBracket = bracketStart >= 0;
|
|
1922
|
+
const start = !hasMinimax
|
|
1923
|
+
? bracketStart
|
|
1924
|
+
: (!hasBracket || minimaxStart <= bracketStart ? minimaxStart : bracketStart);
|
|
1925
|
+
if (start < 0) {
|
|
1926
|
+
if (!final) {
|
|
1927
|
+
const keepLen = Math.min(Math.max(0, Math.max(MINIMAX_TOOL_START.length, BRACKET_TOOL_START.length) - 1), passthroughBuffer.length);
|
|
1928
|
+
const emitLen = passthroughBuffer.length - keepLen;
|
|
1929
|
+
if (emitLen > 0) {
|
|
1930
|
+
emitTextDelta(passthroughBuffer.slice(0, emitLen));
|
|
1931
|
+
}
|
|
1932
|
+
passthroughBuffer = passthroughBuffer.slice(emitLen);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
emitTextDelta(passthroughBuffer);
|
|
1936
|
+
passthroughBuffer = '';
|
|
1204
1937
|
return;
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1938
|
+
}
|
|
1939
|
+
if (start > 0) {
|
|
1940
|
+
emitTextDelta(passthroughBuffer.slice(0, start));
|
|
1941
|
+
passthroughBuffer = passthroughBuffer.slice(start);
|
|
1942
|
+
}
|
|
1943
|
+
const usesMinimaxBlock = minimaxStartMatch !== null && minimaxStartMatch.index === 0;
|
|
1944
|
+
const minimaxEndMatch = usesMinimaxBlock ? findMinimaxToolEnd(passthroughBuffer) : null;
|
|
1945
|
+
const bracketEnd = usesMinimaxBlock ? -1 : passthroughBuffer.indexOf(BRACKET_TOOL_END);
|
|
1946
|
+
if ((usesMinimaxBlock && !minimaxEndMatch) || (!usesMinimaxBlock && bracketEnd < 0)) {
|
|
1947
|
+
if (final) {
|
|
1948
|
+
emitTextDelta(passthroughBuffer);
|
|
1949
|
+
passthroughBuffer = '';
|
|
1950
|
+
}
|
|
1209
1951
|
return;
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1952
|
+
}
|
|
1953
|
+
const blockEndIndex = usesMinimaxBlock
|
|
1954
|
+
? (minimaxEndMatch.index + minimaxEndMatch.token.length)
|
|
1955
|
+
: (bracketEnd + BRACKET_TOOL_END.length);
|
|
1956
|
+
const block = passthroughBuffer.slice(0, blockEndIndex);
|
|
1957
|
+
passthroughBuffer = passthroughBuffer.slice(blockEndIndex);
|
|
1958
|
+
const parsedCalls = usesMinimaxBlock
|
|
1959
|
+
? parseMinimaxToolBlock(block, preferredToolName)
|
|
1960
|
+
: parseBracketToolBlock(block, preferredToolName);
|
|
1961
|
+
if (parsedCalls.length === 0) {
|
|
1962
|
+
emitTextDelta(block);
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
for (const parsedCall of parsedCalls) {
|
|
1966
|
+
appendParsedToolCall(parsedCall);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
const flushProbeBufferAsText = () => {
|
|
1971
|
+
if (probeBuffer.length === 0)
|
|
1972
|
+
return;
|
|
1973
|
+
passthroughBuffer += probeBuffer;
|
|
1974
|
+
flushPassthroughBuffer(false);
|
|
1975
|
+
probeBuffer = '';
|
|
1976
|
+
};
|
|
1977
|
+
const handleTextDelta = (text) => {
|
|
1978
|
+
if (!text || res.writableEnded || sawStructuredAnthropicEvent)
|
|
1979
|
+
return;
|
|
1980
|
+
textTranscript += text;
|
|
1981
|
+
if (probeMode === 'passthrough') {
|
|
1982
|
+
passthroughBuffer += text;
|
|
1983
|
+
flushPassthroughBuffer(false);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (probeMode === 'tool_buffer') {
|
|
1987
|
+
toolCandidateBuffer += text;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
probeBuffer += text;
|
|
1991
|
+
const trimmed = probeBuffer.trimStart();
|
|
1992
|
+
if (!trimmed)
|
|
1993
|
+
return;
|
|
1994
|
+
const looksLikeToolPayload = trimmed.startsWith(MINIMAX_TOOL_START)
|
|
1995
|
+
|| trimmed.startsWith('<minimax:tool_call')
|
|
1996
|
+
|| trimmed.startsWith(BRACKET_TOOL_START)
|
|
1997
|
+
|| trimmed.startsWith('{')
|
|
1998
|
+
|| trimmed.startsWith('```');
|
|
1999
|
+
if (looksLikeToolPayload) {
|
|
2000
|
+
probeMode = 'tool_buffer';
|
|
2001
|
+
toolCandidateBuffer = probeBuffer;
|
|
2002
|
+
probeBuffer = '';
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
probeMode = 'passthrough';
|
|
2006
|
+
flushProbeBufferAsText();
|
|
2007
|
+
};
|
|
2008
|
+
const emitAnthropicToolUseFromCalls = (outputTokens) => {
|
|
2009
|
+
const normalizedToolCalls = toolCallStateToOpenAIList(toolCalls);
|
|
2010
|
+
if (normalizedToolCalls.length === 0)
|
|
2011
|
+
return false;
|
|
2012
|
+
ensureTextStreamStarted();
|
|
2013
|
+
writeAnthropicEvent(res, 'content_block_stop', {
|
|
2014
|
+
type: 'content_block_stop',
|
|
2015
|
+
index: 0,
|
|
2016
|
+
});
|
|
2017
|
+
let toolBlockIndex = 1;
|
|
2018
|
+
for (const call of normalizedToolCalls) {
|
|
2019
|
+
const toolObj = call;
|
|
2020
|
+
const toolId = typeof toolObj.id === 'string' && toolObj.id.trim()
|
|
2021
|
+
? toolObj.id.trim()
|
|
2022
|
+
: `toolu_${Math.random().toString(36).slice(2, 12)}`;
|
|
2023
|
+
const fn = toolObj.function && typeof toolObj.function === 'object'
|
|
2024
|
+
? toolObj.function
|
|
2025
|
+
: {};
|
|
2026
|
+
const toolNameRaw = typeof fn.name === 'string' && fn.name.trim() ? fn.name.trim() : 'tool';
|
|
2027
|
+
const rawArgs = fn.arguments;
|
|
2028
|
+
let argumentsText = '';
|
|
2029
|
+
if (typeof rawArgs === 'string') {
|
|
2030
|
+
argumentsText = rawArgs;
|
|
2031
|
+
}
|
|
2032
|
+
else if (rawArgs !== undefined) {
|
|
2033
|
+
try {
|
|
2034
|
+
argumentsText = JSON.stringify(rawArgs);
|
|
2035
|
+
}
|
|
2036
|
+
catch {
|
|
2037
|
+
argumentsText = String(rawArgs);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const normalizedBaseName = normalizeToolNameForRequest(toolNameRaw, preferredToolName, toolNames) || toolNameRaw;
|
|
2041
|
+
const toolName = coerceToolNameByArguments(normalizedBaseName, argumentsText, toolNames);
|
|
2042
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(argumentsText, toolName, toolInputFallbacks);
|
|
2043
|
+
let serializedInput = '{}';
|
|
2044
|
+
try {
|
|
2045
|
+
serializedInput = JSON.stringify(normalizedInput);
|
|
2046
|
+
}
|
|
2047
|
+
catch {
|
|
2048
|
+
serializedInput = '{}';
|
|
2049
|
+
}
|
|
2050
|
+
writeAnthropicEvent(res, 'content_block_start', {
|
|
2051
|
+
type: 'content_block_start',
|
|
2052
|
+
index: toolBlockIndex,
|
|
2053
|
+
content_block: {
|
|
2054
|
+
type: 'tool_use',
|
|
2055
|
+
id: toolId,
|
|
2056
|
+
name: toolName,
|
|
2057
|
+
input: {},
|
|
2058
|
+
},
|
|
2059
|
+
});
|
|
2060
|
+
writeAnthropicEvent(res, 'content_block_delta', {
|
|
2061
|
+
type: 'content_block_delta',
|
|
2062
|
+
index: toolBlockIndex,
|
|
2063
|
+
delta: {
|
|
2064
|
+
type: 'input_json_delta',
|
|
2065
|
+
partial_json: serializedInput,
|
|
2066
|
+
},
|
|
2067
|
+
});
|
|
2068
|
+
debugBridge('anthropic_emit_tool_use', {
|
|
2069
|
+
toolName,
|
|
2070
|
+
toolId,
|
|
2071
|
+
argumentsText,
|
|
2072
|
+
normalizedInput,
|
|
2073
|
+
serializedInput,
|
|
2074
|
+
});
|
|
2075
|
+
writeAnthropicEvent(res, 'content_block_stop', {
|
|
2076
|
+
type: 'content_block_stop',
|
|
2077
|
+
index: toolBlockIndex,
|
|
2078
|
+
});
|
|
2079
|
+
toolBlockIndex++;
|
|
2080
|
+
}
|
|
2081
|
+
writeAnthropicEvent(res, 'message_delta', {
|
|
2082
|
+
type: 'message_delta',
|
|
2083
|
+
delta: { stop_reason: 'tool_use', stop_sequence: null },
|
|
2084
|
+
usage: { output_tokens: outputTokens },
|
|
2085
|
+
});
|
|
2086
|
+
writeAnthropicEvent(res, 'message_stop', { type: 'message_stop' });
|
|
2087
|
+
stopSSEHeartbeat(res);
|
|
2088
|
+
res.end();
|
|
2089
|
+
return true;
|
|
2090
|
+
};
|
|
2091
|
+
await routeStreamRequest(req, res, request, requestedModel, {
|
|
2092
|
+
onStart: (nextServedModel, sellerId) => {
|
|
2093
|
+
if (res.writableEnded)
|
|
2094
|
+
return;
|
|
2095
|
+
servedModel = nextServedModel;
|
|
2096
|
+
startAnthropicSSE(res, requestedModel, servedModel, sellerId);
|
|
2097
|
+
},
|
|
2098
|
+
onDelta: (text) => {
|
|
2099
|
+
handleTextDelta(text);
|
|
2100
|
+
},
|
|
2101
|
+
onAnthropicEvent: (eventName, eventData) => {
|
|
2102
|
+
if (res.writableEnded)
|
|
2103
|
+
return;
|
|
2104
|
+
let normalizedEventData = eventData;
|
|
2105
|
+
if (eventName === 'content_block_start') {
|
|
2106
|
+
const block = eventData.content_block;
|
|
2107
|
+
if (block && typeof block === 'object') {
|
|
2108
|
+
const blockObj = block;
|
|
2109
|
+
if (blockObj.type === 'tool_use') {
|
|
2110
|
+
const toolName = typeof blockObj.name === 'string' ? blockObj.name : undefined;
|
|
2111
|
+
const normalizedToolName = normalizeToolNameForRequest(toolName || '', preferredToolName, toolNames)
|
|
2112
|
+
|| toolName
|
|
2113
|
+
|| preferredToolName
|
|
2114
|
+
|| 'tool';
|
|
2115
|
+
let argumentsText = '';
|
|
2116
|
+
if (blockObj.input !== undefined) {
|
|
2117
|
+
try {
|
|
2118
|
+
argumentsText = JSON.stringify(blockObj.input);
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
argumentsText = String(blockObj.input);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const coercedToolName = coerceToolNameByArguments(normalizedToolName, argumentsText, toolNames);
|
|
2125
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(argumentsText, coercedToolName, toolInputFallbacks);
|
|
2126
|
+
debugBridge('anthropic_struct_tool_use', {
|
|
2127
|
+
toolName: toolName || null,
|
|
2128
|
+
normalizedToolName,
|
|
2129
|
+
coercedToolName,
|
|
2130
|
+
rawInput: blockObj.input ?? null,
|
|
2131
|
+
normalizedInput,
|
|
2132
|
+
});
|
|
2133
|
+
let serializedInput = '{}';
|
|
2134
|
+
try {
|
|
2135
|
+
serializedInput = JSON.stringify(normalizedInput);
|
|
2136
|
+
}
|
|
2137
|
+
catch {
|
|
2138
|
+
serializedInput = '{}';
|
|
2139
|
+
}
|
|
2140
|
+
normalizedEventData = {
|
|
2141
|
+
...eventData,
|
|
2142
|
+
content_block: {
|
|
2143
|
+
...blockObj,
|
|
2144
|
+
name: coercedToolName,
|
|
2145
|
+
input: {},
|
|
2146
|
+
},
|
|
2147
|
+
};
|
|
2148
|
+
sawStructuredAnthropicEvent = true;
|
|
2149
|
+
writeAnthropicEvent(res, eventName, normalizedEventData);
|
|
2150
|
+
writeAnthropicEvent(res, 'content_block_delta', {
|
|
2151
|
+
type: 'content_block_delta',
|
|
2152
|
+
index: eventData.index,
|
|
2153
|
+
delta: {
|
|
2154
|
+
type: 'input_json_delta',
|
|
2155
|
+
partial_json: serializedInput,
|
|
2156
|
+
},
|
|
2157
|
+
});
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
sawStructuredAnthropicEvent = true;
|
|
2163
|
+
writeAnthropicEvent(res, eventName, normalizedEventData);
|
|
2164
|
+
if (eventName === 'message_stop') {
|
|
2165
|
+
sawMessageStop = true;
|
|
2166
|
+
}
|
|
2167
|
+
},
|
|
2168
|
+
onOpenAIEvent: (eventData) => {
|
|
2169
|
+
if (res.writableEnded || sawStructuredAnthropicEvent)
|
|
2170
|
+
return;
|
|
2171
|
+
const textDelta = extractOpenAITextDelta(eventData);
|
|
2172
|
+
if (textDelta.length > 0) {
|
|
2173
|
+
handleTextDelta(textDelta);
|
|
2174
|
+
}
|
|
2175
|
+
const toolDeltas = extractOpenAIToolCallDeltas(eventData);
|
|
2176
|
+
for (const delta of toolDeltas) {
|
|
2177
|
+
applyOpenAIToolCallDelta(toolCalls, delta);
|
|
2178
|
+
}
|
|
2179
|
+
},
|
|
2180
|
+
onEnd: (outputTokens) => {
|
|
2181
|
+
if (res.writableEnded)
|
|
2182
|
+
return;
|
|
2183
|
+
if (sawStructuredAnthropicEvent) {
|
|
2184
|
+
stopSSEHeartbeat(res);
|
|
2185
|
+
if (!sawMessageStop) {
|
|
2186
|
+
writeAnthropicEvent(res, 'message_stop', { type: 'message_stop' });
|
|
2187
|
+
}
|
|
2188
|
+
res.end();
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
if (probeMode === 'probe') {
|
|
2192
|
+
probeMode = 'passthrough';
|
|
2193
|
+
flushProbeBufferAsText();
|
|
2194
|
+
}
|
|
2195
|
+
else if (probeMode === 'tool_buffer') {
|
|
2196
|
+
const buffered = toolCandidateBuffer;
|
|
2197
|
+
toolCandidateBuffer = '';
|
|
2198
|
+
probeMode = 'passthrough';
|
|
2199
|
+
const parsed = extractToolCallsFromText(buffered, preferredToolName);
|
|
2200
|
+
if (parsed.calls.length > 0) {
|
|
2201
|
+
for (const call of parsed.calls) {
|
|
2202
|
+
appendParsedToolCall(call);
|
|
2203
|
+
}
|
|
2204
|
+
if (parsed.cleanText.trim().length > 0) {
|
|
2205
|
+
passthroughBuffer += parsed.cleanText;
|
|
2206
|
+
flushPassthroughBuffer(false);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
else if (buffered.length > 0) {
|
|
2210
|
+
passthroughBuffer += buffered;
|
|
2211
|
+
flushPassthroughBuffer(false);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
flushPassthroughBuffer(true);
|
|
2215
|
+
if (toolCalls.size === 0 && textTranscript.trim().length > 0) {
|
|
2216
|
+
const parsedFallback = extractToolCallsFromText(textTranscript, preferredToolName);
|
|
2217
|
+
for (const call of parsedFallback.calls) {
|
|
2218
|
+
appendParsedToolCall(call);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (ENABLE_FORCED_BROWSER_OPEN && toolCalls.size === 0 && shouldForceBrowserOpenToolCall(textTranscript, toolNames, toolInputFallbacks.webFetchUrl || '')) {
|
|
2222
|
+
const browserToolName = findMatchingAvailableToolName('browser', toolNames) || 'browser';
|
|
2223
|
+
const forcedInput = parseToolArgumentsAsAnthropicInput(JSON.stringify({
|
|
2224
|
+
action: 'open',
|
|
2225
|
+
url: toolInputFallbacks.webFetchUrl,
|
|
2226
|
+
...(toolInputFallbacks.browserProfile ? { profile: toolInputFallbacks.browserProfile } : {}),
|
|
2227
|
+
}), browserToolName, toolInputFallbacks);
|
|
2228
|
+
const toolIndex = nextToolIndex++;
|
|
2229
|
+
applyOpenAIToolCallDelta(toolCalls, {
|
|
2230
|
+
index: toolIndex,
|
|
2231
|
+
id: `toolu_${Math.random().toString(36).slice(2, 12)}`,
|
|
2232
|
+
name: browserToolName,
|
|
2233
|
+
arguments: JSON.stringify(forcedInput),
|
|
2234
|
+
});
|
|
2235
|
+
debugBridge('anthropic_forced_browser_tool_use', {
|
|
2236
|
+
toolName: browserToolName,
|
|
2237
|
+
forcedInput,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
if (emitAnthropicToolUseFromCalls(outputTokens)) {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
ensureTextStreamStarted();
|
|
2244
|
+
endAnthropicSSE(res, outputTokens);
|
|
2245
|
+
},
|
|
2246
|
+
onError: (message) => {
|
|
2247
|
+
if (res.writableEnded)
|
|
2248
|
+
return;
|
|
2249
|
+
failAnthropicSSE(res, message);
|
|
2250
|
+
},
|
|
2251
|
+
});
|
|
1213
2252
|
}
|
|
1214
2253
|
// ============ OpenAI 接口 ============
|
|
1215
2254
|
async function handleOpenAIStreamRequest(req, res, rawRequest) {
|
|
@@ -1218,55 +2257,1845 @@ async function handleOpenAIStreamRequest(req, res, rawRequest) {
|
|
|
1218
2257
|
failOpenAISSE(res, 'messages 不能为空');
|
|
1219
2258
|
return;
|
|
1220
2259
|
}
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
2260
|
+
const preferredToolName = resolvePreferredToolNameFromRequest(normalized.requestPayload);
|
|
2261
|
+
const availableToolNames = extractToolNamesFromRequest(normalized.requestPayload);
|
|
2262
|
+
const toolInputFallbacks = deriveToolInputFallbacksFromRequest(normalized.requestPayload);
|
|
2263
|
+
const routeModel = await resolveOpenAIRouteModel(normalized.requestedModel);
|
|
2264
|
+
const translated = routeModel !== normalized.requestedModel;
|
|
2265
|
+
const routeRequest = {
|
|
2266
|
+
...normalized.requestPayload,
|
|
2267
|
+
model: routeModel,
|
|
2268
|
+
};
|
|
2269
|
+
if (translated) {
|
|
2270
|
+
console.log(`[请求] Model: ${normalized.requestedModel} -> ${routeModel}, Stream: true (openai)`);
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
console.log(`[请求] Model: ${normalized.requestedModel}, Stream: true (openai)`);
|
|
2274
|
+
}
|
|
2275
|
+
const completionId = `chatcmpl_${Math.random().toString(36).slice(2, 12)}`;
|
|
2276
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
2277
|
+
let servedModel = routeModel;
|
|
2278
|
+
let sawToolCall = false;
|
|
2279
|
+
let nextToolIndex = 0;
|
|
2280
|
+
const toolCalls = new Map();
|
|
2281
|
+
const anthropicToolBlockMap = new Map();
|
|
2282
|
+
let minimaxTextBuffer = '';
|
|
2283
|
+
const allocateToolIndex = (anthropicBlockIndex) => {
|
|
2284
|
+
const existing = anthropicToolBlockMap.get(anthropicBlockIndex);
|
|
2285
|
+
if (existing !== undefined)
|
|
2286
|
+
return existing;
|
|
2287
|
+
const idx = nextToolIndex++;
|
|
2288
|
+
anthropicToolBlockMap.set(anthropicBlockIndex, idx);
|
|
2289
|
+
return idx;
|
|
2290
|
+
};
|
|
2291
|
+
const emitParsedToolCall = (tool) => {
|
|
2292
|
+
const normalizedBaseName = normalizeToolNameForRequest(tool.name, preferredToolName, availableToolNames) || tool.name || 'tool';
|
|
2293
|
+
const normalizedName = coerceToolNameByArguments(normalizedBaseName, tool.arguments, availableToolNames);
|
|
2294
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(tool.arguments, normalizedName, toolInputFallbacks);
|
|
2295
|
+
const toolIndex = nextToolIndex++;
|
|
2296
|
+
const delta = {
|
|
2297
|
+
index: toolIndex,
|
|
2298
|
+
id: `call_${completionId}_${toolIndex}`,
|
|
2299
|
+
name: normalizedName,
|
|
2300
|
+
arguments: JSON.stringify(normalizedInput),
|
|
2301
|
+
};
|
|
2302
|
+
applyOpenAIToolCallDelta(toolCalls, delta);
|
|
2303
|
+
writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, [delta]);
|
|
2304
|
+
sawToolCall = true;
|
|
2305
|
+
};
|
|
2306
|
+
const flushMinimaxTextBuffer = (final) => {
|
|
2307
|
+
while (minimaxTextBuffer.length > 0) {
|
|
2308
|
+
const startMatch = findMinimaxToolStart(minimaxTextBuffer);
|
|
2309
|
+
const start = startMatch ? startMatch.index : -1;
|
|
2310
|
+
if (start < 0) {
|
|
2311
|
+
writeOpenAIDelta(res, completionId, createdAt, servedModel, minimaxTextBuffer);
|
|
2312
|
+
minimaxTextBuffer = '';
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (start > 0) {
|
|
2316
|
+
const prefix = minimaxTextBuffer.slice(0, start);
|
|
2317
|
+
writeOpenAIDelta(res, completionId, createdAt, servedModel, prefix);
|
|
2318
|
+
minimaxTextBuffer = minimaxTextBuffer.slice(start);
|
|
2319
|
+
}
|
|
2320
|
+
const endMatch = findMinimaxToolEnd(minimaxTextBuffer);
|
|
2321
|
+
if (!endMatch) {
|
|
2322
|
+
if (final) {
|
|
2323
|
+
writeOpenAIDelta(res, completionId, createdAt, servedModel, minimaxTextBuffer);
|
|
2324
|
+
minimaxTextBuffer = '';
|
|
2325
|
+
}
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
const block = minimaxTextBuffer.slice(0, endMatch.index + endMatch.token.length);
|
|
2329
|
+
minimaxTextBuffer = minimaxTextBuffer.slice(endMatch.index + endMatch.token.length);
|
|
2330
|
+
const parsedCalls = parseMinimaxToolBlock(block, preferredToolName);
|
|
2331
|
+
if (parsedCalls.length === 0) {
|
|
2332
|
+
writeOpenAIDelta(res, completionId, createdAt, servedModel, block);
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
parsedCalls.forEach((tool) => emitParsedToolCall(tool));
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
const handleTextDelta = (text) => {
|
|
2339
|
+
minimaxTextBuffer += text;
|
|
2340
|
+
flushMinimaxTextBuffer(false);
|
|
2341
|
+
};
|
|
2342
|
+
await routeStreamRequest(req, res, routeRequest, routeModel, {
|
|
2343
|
+
onStart: (nextServedModel, sellerId) => {
|
|
2344
|
+
if (res.writableEnded)
|
|
2345
|
+
return;
|
|
2346
|
+
servedModel = nextServedModel;
|
|
2347
|
+
startOpenAISSE(res, completionId, createdAt, normalized.requestedModel, servedModel, sellerId);
|
|
2348
|
+
},
|
|
2349
|
+
onDelta: (text) => {
|
|
2350
|
+
if (res.writableEnded)
|
|
2351
|
+
return;
|
|
2352
|
+
handleTextDelta(text);
|
|
2353
|
+
},
|
|
2354
|
+
onAnthropicEvent: (eventName, eventData) => {
|
|
2355
|
+
if (res.writableEnded)
|
|
2356
|
+
return;
|
|
2357
|
+
if (eventName === 'content_block_start') {
|
|
2358
|
+
const block = eventData.content_block;
|
|
2359
|
+
const blockIndex = Number.isFinite(Number(eventData.index)) ? Number(eventData.index) : -1;
|
|
2360
|
+
if (block && typeof block === 'object') {
|
|
2361
|
+
const anyBlock = block;
|
|
2362
|
+
if (anyBlock.type === 'tool_use') {
|
|
2363
|
+
const toolIndex = blockIndex >= 0 ? allocateToolIndex(blockIndex) : nextToolIndex++;
|
|
2364
|
+
const toolId = typeof anyBlock.id === 'string' && anyBlock.id.trim()
|
|
2365
|
+
? anyBlock.id.trim()
|
|
2366
|
+
: `call_${completionId}_${toolIndex}`;
|
|
2367
|
+
const rawToolName = typeof anyBlock.name === 'string' && anyBlock.name.trim()
|
|
2368
|
+
? anyBlock.name.trim()
|
|
2369
|
+
: 'tool';
|
|
2370
|
+
const toolName = normalizeToolNameForRequest(rawToolName, preferredToolName, availableToolNames) || rawToolName;
|
|
2371
|
+
let initialArgs = '';
|
|
2372
|
+
if (anyBlock.input !== undefined) {
|
|
2373
|
+
try {
|
|
2374
|
+
initialArgs = JSON.stringify(anyBlock.input);
|
|
2375
|
+
}
|
|
2376
|
+
catch {
|
|
2377
|
+
initialArgs = String(anyBlock.input);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
const coercedToolName = coerceToolNameByArguments(toolName, initialArgs, availableToolNames);
|
|
2381
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(initialArgs, coercedToolName, toolInputFallbacks);
|
|
2382
|
+
const delta = {
|
|
2383
|
+
index: toolIndex,
|
|
2384
|
+
id: toolId,
|
|
2385
|
+
name: coercedToolName,
|
|
2386
|
+
arguments: JSON.stringify(normalizedInput),
|
|
2387
|
+
};
|
|
2388
|
+
applyOpenAIToolCallDelta(toolCalls, delta);
|
|
2389
|
+
writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, [delta]);
|
|
2390
|
+
sawToolCall = true;
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
if (anyBlock.type === 'text' && typeof anyBlock.text === 'string' && anyBlock.text.length > 0) {
|
|
2394
|
+
handleTextDelta(anyBlock.text);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (eventName === 'content_block_delta') {
|
|
2400
|
+
const delta = eventData.delta;
|
|
2401
|
+
if (!delta || typeof delta !== 'object')
|
|
2402
|
+
return;
|
|
2403
|
+
const anyDelta = delta;
|
|
2404
|
+
if ((anyDelta.type === 'text_delta' || anyDelta.type === undefined) && typeof anyDelta.text === 'string' && anyDelta.text.length > 0) {
|
|
2405
|
+
handleTextDelta(anyDelta.text);
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (anyDelta.type === 'input_json_delta' && typeof anyDelta.partial_json === 'string') {
|
|
2409
|
+
const blockIndex = Number.isFinite(Number(eventData.index)) ? Number(eventData.index) : -1;
|
|
2410
|
+
const toolIndex = blockIndex >= 0 ? allocateToolIndex(blockIndex) : nextToolIndex++;
|
|
2411
|
+
const existing = toolCalls.get(toolIndex);
|
|
2412
|
+
if (!existing) {
|
|
2413
|
+
const startDelta = {
|
|
2414
|
+
index: toolIndex,
|
|
2415
|
+
id: `call_${completionId}_${toolIndex}`,
|
|
2416
|
+
name: preferredToolName || 'tool',
|
|
2417
|
+
arguments: '',
|
|
2418
|
+
};
|
|
2419
|
+
applyOpenAIToolCallDelta(toolCalls, startDelta);
|
|
2420
|
+
writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, [startDelta]);
|
|
2421
|
+
}
|
|
2422
|
+
const argsDelta = {
|
|
2423
|
+
index: toolIndex,
|
|
2424
|
+
arguments: anyDelta.partial_json,
|
|
2425
|
+
};
|
|
2426
|
+
applyOpenAIToolCallDelta(toolCalls, argsDelta);
|
|
2427
|
+
writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, [argsDelta]);
|
|
2428
|
+
sawToolCall = true;
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (eventName === 'message_delta') {
|
|
2433
|
+
const delta = eventData.delta;
|
|
2434
|
+
if (delta && typeof delta === 'object') {
|
|
2435
|
+
const stopReason = delta.stop_reason;
|
|
2436
|
+
if (stopReason === 'tool_use') {
|
|
2437
|
+
sawToolCall = true;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
},
|
|
2442
|
+
onOpenAIEvent: (eventData) => {
|
|
2443
|
+
if (res.writableEnded)
|
|
2444
|
+
return;
|
|
2445
|
+
const textDelta = extractOpenAITextDelta(eventData);
|
|
2446
|
+
if (textDelta.length > 0) {
|
|
2447
|
+
handleTextDelta(textDelta);
|
|
2448
|
+
}
|
|
2449
|
+
const toolDeltas = extractOpenAIToolCallDeltas(eventData);
|
|
2450
|
+
if (toolDeltas.length > 0) {
|
|
2451
|
+
const normalizedDeltas = toolDeltas.map((delta) => {
|
|
2452
|
+
if (!delta.name)
|
|
2453
|
+
return delta;
|
|
2454
|
+
const argsText = typeof delta.arguments === 'string' ? delta.arguments : '';
|
|
2455
|
+
const normalizedBaseName = normalizeToolNameForRequest(delta.name, preferredToolName, availableToolNames) || delta.name;
|
|
2456
|
+
const coercedName = coerceToolNameByArguments(normalizedBaseName, argsText, availableToolNames);
|
|
2457
|
+
return {
|
|
2458
|
+
...delta,
|
|
2459
|
+
name: coercedName,
|
|
2460
|
+
};
|
|
2461
|
+
});
|
|
2462
|
+
normalizedDeltas.forEach((delta) => applyOpenAIToolCallDelta(toolCalls, delta));
|
|
2463
|
+
writeOpenAIToolCallDelta(res, completionId, createdAt, servedModel, normalizedDeltas);
|
|
2464
|
+
sawToolCall = true;
|
|
2465
|
+
}
|
|
2466
|
+
if (extractOpenAIFinishReason(eventData) === 'tool_calls') {
|
|
2467
|
+
sawToolCall = true;
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
onEnd: (outputTokens) => {
|
|
2471
|
+
if (res.writableEnded)
|
|
2472
|
+
return;
|
|
2473
|
+
flushMinimaxTextBuffer(true);
|
|
2474
|
+
const finishReason = sawToolCall ? 'tool_calls' : 'stop';
|
|
2475
|
+
endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens, finishReason);
|
|
2476
|
+
},
|
|
2477
|
+
onError: (message) => {
|
|
2478
|
+
if (res.writableEnded)
|
|
2479
|
+
return;
|
|
2480
|
+
failOpenAISSE(res, message);
|
|
2481
|
+
},
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
function sendOpenAIJsonError(res, status, message) {
|
|
2485
|
+
if (res.writableEnded)
|
|
2486
|
+
return;
|
|
2487
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2488
|
+
res.end(JSON.stringify({
|
|
2489
|
+
error: {
|
|
2490
|
+
type: 'api_error',
|
|
2491
|
+
message,
|
|
2492
|
+
},
|
|
2493
|
+
}));
|
|
2494
|
+
}
|
|
2495
|
+
function extractOpenAIToolCallDeltas(eventData) {
|
|
2496
|
+
const choices = eventData.choices;
|
|
2497
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
2498
|
+
return [];
|
|
2499
|
+
const first = choices[0];
|
|
2500
|
+
if (!first || typeof first !== 'object')
|
|
2501
|
+
return [];
|
|
2502
|
+
const delta = first.delta;
|
|
2503
|
+
if (!delta || typeof delta !== 'object')
|
|
2504
|
+
return [];
|
|
2505
|
+
const rawCalls = delta.tool_calls;
|
|
2506
|
+
if (!Array.isArray(rawCalls))
|
|
2507
|
+
return [];
|
|
2508
|
+
const toolDeltas = [];
|
|
2509
|
+
rawCalls.forEach((call, idx) => {
|
|
2510
|
+
const normalized = sanitizeOpenAIToolCallDelta(call, idx);
|
|
2511
|
+
if (normalized)
|
|
2512
|
+
toolDeltas.push(normalized);
|
|
2513
|
+
});
|
|
2514
|
+
return toolDeltas;
|
|
2515
|
+
}
|
|
2516
|
+
function extractOpenAIFinishReason(eventData) {
|
|
2517
|
+
const choices = eventData.choices;
|
|
2518
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
2519
|
+
return null;
|
|
2520
|
+
const first = choices[0];
|
|
2521
|
+
if (!first || typeof first !== 'object')
|
|
2522
|
+
return null;
|
|
2523
|
+
const reason = first.finish_reason;
|
|
2524
|
+
return typeof reason === 'string' ? reason : null;
|
|
2525
|
+
}
|
|
2526
|
+
const MINIMAX_TOOL_START = '<minimax:tool_call>';
|
|
2527
|
+
const MINIMAX_TOOL_END = '</minimax:tool_call>';
|
|
2528
|
+
const MINIMAX_TOOL_START_PATTERN = /<minimax:tool_call\b[^>]*>/i;
|
|
2529
|
+
const MINIMAX_TOOL_END_PATTERN = /<\/minimax:tool_call>/i;
|
|
2530
|
+
const BRACKET_TOOL_START = '[TOOL_CALL]';
|
|
2531
|
+
const BRACKET_TOOL_END = '[/TOOL_CALL]';
|
|
2532
|
+
const FUNCTION_CALL_START = '<FunctionCall>';
|
|
2533
|
+
const FUNCTION_CALL_END = '</FunctionCall>';
|
|
2534
|
+
const EXEC_TOOL_ALIASES = new Set([
|
|
2535
|
+
'exec',
|
|
2536
|
+
'bash',
|
|
2537
|
+
'sh',
|
|
2538
|
+
'zsh',
|
|
2539
|
+
'shell',
|
|
2540
|
+
'terminal',
|
|
2541
|
+
'cmd',
|
|
2542
|
+
'powershell',
|
|
2543
|
+
'command',
|
|
2544
|
+
'run_command',
|
|
2545
|
+
'run-command',
|
|
2546
|
+
'execute_command',
|
|
2547
|
+
'execute-command',
|
|
2548
|
+
'cli-mcp-server_run_command',
|
|
2549
|
+
'cli-mcp-server_execute_command',
|
|
2550
|
+
]);
|
|
2551
|
+
const WEB_FETCH_TOOL_ALIASES = new Set([
|
|
2552
|
+
'web_fetch',
|
|
2553
|
+
'web-fetch',
|
|
2554
|
+
'webfetch',
|
|
2555
|
+
'fetch_url',
|
|
2556
|
+
'fetch-url',
|
|
2557
|
+
'url_fetch',
|
|
2558
|
+
'url-fetch',
|
|
2559
|
+
]);
|
|
2560
|
+
function findMinimaxToolStart(text) {
|
|
2561
|
+
const match = MINIMAX_TOOL_START_PATTERN.exec(text);
|
|
2562
|
+
MINIMAX_TOOL_START_PATTERN.lastIndex = 0;
|
|
2563
|
+
if (!match || match.index === undefined)
|
|
2564
|
+
return null;
|
|
2565
|
+
return { index: match.index, token: match[0] || MINIMAX_TOOL_START };
|
|
2566
|
+
}
|
|
2567
|
+
function findMinimaxToolEnd(text) {
|
|
2568
|
+
const match = MINIMAX_TOOL_END_PATTERN.exec(text);
|
|
2569
|
+
MINIMAX_TOOL_END_PATTERN.lastIndex = 0;
|
|
2570
|
+
if (!match || match.index === undefined)
|
|
2571
|
+
return null;
|
|
2572
|
+
return { index: match.index, token: match[0] || MINIMAX_TOOL_END };
|
|
2573
|
+
}
|
|
2574
|
+
function normalizeBrokenMinimaxToolMarkup(block) {
|
|
2575
|
+
let normalized = block;
|
|
2576
|
+
// Some sellers emit `invoke name="..."` without an opening `<`.
|
|
2577
|
+
normalized = normalized.replace(/(^|\n|\r)\s*invoke(\s+name=["'][^"']+["'][^>]*>)/gi, '$1<invoke$2');
|
|
2578
|
+
if (/<invoke\b/i.test(normalized) && !/<\/invoke>/i.test(normalized)) {
|
|
2579
|
+
const endMatch = findMinimaxToolEnd(normalized);
|
|
2580
|
+
if (endMatch) {
|
|
2581
|
+
normalized = `${normalized.slice(0, endMatch.index)}</invoke>${normalized.slice(endMatch.index)}`;
|
|
2582
|
+
}
|
|
2583
|
+
else {
|
|
2584
|
+
normalized = `${normalized}</invoke>`;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
return normalized;
|
|
2588
|
+
}
|
|
2589
|
+
function isExecLikeToolName(name) {
|
|
2590
|
+
const lowered = name.trim().toLowerCase();
|
|
2591
|
+
if (!lowered)
|
|
2592
|
+
return false;
|
|
2593
|
+
if (EXEC_TOOL_ALIASES.has(lowered))
|
|
2594
|
+
return true;
|
|
2595
|
+
if (lowered.startsWith('cli-mcp-server_'))
|
|
2596
|
+
return true;
|
|
2597
|
+
if (/([_-]|^)run[_-]?command$/i.test(lowered))
|
|
2598
|
+
return true;
|
|
2599
|
+
if (/([_-]|^)execute[_-]?command$/i.test(lowered))
|
|
2600
|
+
return true;
|
|
2601
|
+
if ((lowered.includes('shell') || lowered.includes('terminal')) && lowered.includes('command'))
|
|
2602
|
+
return true;
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
function isWebFetchLikeToolName(name) {
|
|
2606
|
+
const lowered = name.trim().toLowerCase();
|
|
2607
|
+
if (!lowered)
|
|
2608
|
+
return false;
|
|
2609
|
+
if (WEB_FETCH_TOOL_ALIASES.has(lowered))
|
|
2610
|
+
return true;
|
|
2611
|
+
if ((lowered.includes('web') || lowered.includes('url')) && lowered.includes('fetch'))
|
|
2612
|
+
return true;
|
|
2613
|
+
return false;
|
|
2614
|
+
}
|
|
2615
|
+
function decodeXmlEntities(text) {
|
|
2616
|
+
return text
|
|
2617
|
+
.replace(/</g, '<')
|
|
2618
|
+
.replace(/>/g, '>')
|
|
2619
|
+
.replace(/&/g, '&')
|
|
2620
|
+
.replace(/"/g, '"')
|
|
2621
|
+
.replace(/'/g, "'");
|
|
2622
|
+
}
|
|
2623
|
+
function normalizeToolName(name, fallbackToolName) {
|
|
2624
|
+
const trimmed = name.trim();
|
|
2625
|
+
const fallback = typeof fallbackToolName === 'string' ? fallbackToolName.trim() : '';
|
|
2626
|
+
if (!trimmed)
|
|
2627
|
+
return fallback;
|
|
2628
|
+
if (isExecLikeToolName(trimmed)) {
|
|
2629
|
+
return fallback && fallback.toLowerCase() === 'exec' ? fallback : 'exec';
|
|
2630
|
+
}
|
|
2631
|
+
if (isWebFetchLikeToolName(trimmed)) {
|
|
2632
|
+
return fallback && fallback.toLowerCase() === 'web_fetch' ? fallback : 'web_fetch';
|
|
2633
|
+
}
|
|
2634
|
+
if (!fallback)
|
|
2635
|
+
return trimmed;
|
|
2636
|
+
if (trimmed.toLowerCase() === fallback.toLowerCase())
|
|
2637
|
+
return fallback;
|
|
2638
|
+
return fallback;
|
|
2639
|
+
}
|
|
2640
|
+
function parseMinimaxToolBlock(block, fallbackToolName) {
|
|
2641
|
+
const invokes = [];
|
|
2642
|
+
const normalizedBlock = normalizeBrokenMinimaxToolMarkup(block);
|
|
2643
|
+
const invokeRegex = /<invoke\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/invoke>/gi;
|
|
2644
|
+
let invokeMatch = null;
|
|
2645
|
+
while ((invokeMatch = invokeRegex.exec(normalizedBlock)) !== null) {
|
|
2646
|
+
const toolName = normalizeToolName(decodeXmlEntities(invokeMatch[1] || ''), fallbackToolName);
|
|
2647
|
+
if (!toolName)
|
|
2648
|
+
continue;
|
|
2649
|
+
const invokeBody = invokeMatch[2] || '';
|
|
2650
|
+
const params = {};
|
|
2651
|
+
const paramRegex = /<parameter\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/parameter>/gi;
|
|
2652
|
+
let paramMatch = null;
|
|
2653
|
+
while ((paramMatch = paramRegex.exec(invokeBody)) !== null) {
|
|
2654
|
+
const key = decodeXmlEntities(paramMatch[1] || '').trim();
|
|
2655
|
+
if (!key)
|
|
2656
|
+
continue;
|
|
2657
|
+
const value = decodeXmlEntities(paramMatch[2] || '').trim();
|
|
2658
|
+
params[key] = value;
|
|
2659
|
+
}
|
|
2660
|
+
invokes.push({
|
|
2661
|
+
name: toolName,
|
|
2662
|
+
arguments: JSON.stringify(params),
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
return invokes;
|
|
2666
|
+
}
|
|
2667
|
+
function parseBracketToolBody(body, fallbackToolName) {
|
|
2668
|
+
let candidate = body.trim();
|
|
2669
|
+
if (!candidate)
|
|
2670
|
+
return [];
|
|
2671
|
+
const fenced = candidate.match(/^```(?:[a-z0-9_-]+)?\s*([\s\S]*?)\s*```$/i);
|
|
2672
|
+
if (fenced && fenced[1]) {
|
|
2673
|
+
candidate = fenced[1].trim();
|
|
2674
|
+
}
|
|
2675
|
+
const jsonCalls = parseJsonToolCallsFromText(candidate, fallbackToolName);
|
|
2676
|
+
if (jsonCalls.length > 0)
|
|
2677
|
+
return jsonCalls;
|
|
2678
|
+
const toolNameMatch = candidate.match(/\btool\s*=>\s*["']([^"']+)["']/i)
|
|
2679
|
+
|| candidate.match(/\bname\s*=>\s*["']([^"']+)["']/i)
|
|
2680
|
+
|| candidate.match(/"tool"\s*:\s*"([^"]+)"/i)
|
|
2681
|
+
|| candidate.match(/"name"\s*:\s*"([^"]+)"/i);
|
|
2682
|
+
const rawToolName = toolNameMatch?.[1] ?? '';
|
|
2683
|
+
const toolName = normalizeToolName(rawToolName, fallbackToolName);
|
|
2684
|
+
if (!toolName)
|
|
2685
|
+
return [];
|
|
2686
|
+
const commandMatch = candidate.match(/--command\s+"([^"]+)"/i)
|
|
2687
|
+
|| candidate.match(/--command\s+'([^']+)'/i)
|
|
2688
|
+
|| candidate.match(/"command"\s*:\s*"([^"]+)"/i)
|
|
2689
|
+
|| candidate.match(/'command'\s*:\s*'([^']+)'/i)
|
|
2690
|
+
|| candidate.match(/\bcommand\s*=>\s*"([^"]+)"/i)
|
|
2691
|
+
|| candidate.match(/\bcommand\s*=>\s*'([^']+)'/i);
|
|
2692
|
+
if (commandMatch?.[1]) {
|
|
2693
|
+
return [{
|
|
2694
|
+
name: toolName,
|
|
2695
|
+
arguments: JSON.stringify({ command: commandMatch[1] }),
|
|
2696
|
+
}];
|
|
2697
|
+
}
|
|
2698
|
+
const argsJson = candidate.match(/\bargs\s*=>\s*(\{[\s\S]*\})/i)
|
|
2699
|
+
|| candidate.match(/"args"\s*:\s*(\{[\s\S]*\})/i);
|
|
2700
|
+
if (argsJson?.[1]) {
|
|
2701
|
+
return [{
|
|
2702
|
+
name: toolName,
|
|
2703
|
+
arguments: argsJson[1],
|
|
2704
|
+
}];
|
|
2705
|
+
}
|
|
2706
|
+
if (toolName.toLowerCase() === 'exec') {
|
|
2707
|
+
return [];
|
|
2708
|
+
}
|
|
2709
|
+
return [{
|
|
2710
|
+
name: toolName,
|
|
2711
|
+
arguments: '{}',
|
|
2712
|
+
}];
|
|
2713
|
+
}
|
|
2714
|
+
function parseBracketToolBlock(block, fallbackToolName) {
|
|
2715
|
+
const invokes = [];
|
|
2716
|
+
const blockRegex = /\[TOOL_CALL\]([\s\S]*?)\[\/TOOL_CALL\]/gi;
|
|
2717
|
+
let blockMatch = null;
|
|
2718
|
+
while ((blockMatch = blockRegex.exec(block)) !== null) {
|
|
2719
|
+
const parsed = parseBracketToolBody(blockMatch[1] || '', fallbackToolName);
|
|
2720
|
+
if (parsed.length > 0) {
|
|
2721
|
+
invokes.push(...parsed);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
return invokes;
|
|
2725
|
+
}
|
|
2726
|
+
function parseFunctionCallBody(body, fallbackToolName) {
|
|
2727
|
+
let candidate = body.trim();
|
|
2728
|
+
if (!candidate)
|
|
2729
|
+
return [];
|
|
2730
|
+
const fenced = candidate.match(/^```(?:[a-z0-9_-]+)?\s*([\s\S]*?)\s*```$/i);
|
|
2731
|
+
if (fenced?.[1]) {
|
|
2732
|
+
candidate = fenced[1].trim();
|
|
2733
|
+
}
|
|
2734
|
+
const toolNameMatch = candidate.match(/['"]?tool['"]?\s*[:=]\s*['"]([^'"]+)['"]/i)
|
|
2735
|
+
|| candidate.match(/['"]?name['"]?\s*[:=]\s*['"]([^'"]+)['"]/i)
|
|
2736
|
+
|| candidate.match(/"tool"\s*:\s*"([^"]+)"/i)
|
|
2737
|
+
|| candidate.match(/"name"\s*:\s*"([^"]+)"/i);
|
|
2738
|
+
const rawToolName = toolNameMatch?.[1] ?? '';
|
|
2739
|
+
const toolName = normalizeToolName(rawToolName, fallbackToolName);
|
|
2740
|
+
if (!toolName)
|
|
2741
|
+
return [];
|
|
2742
|
+
const parseCommand = (text) => {
|
|
2743
|
+
const commandMatch = text.match(/--command\s+"([^"]+)"/i)
|
|
2744
|
+
|| text.match(/--command\s+'([^']+)'/i)
|
|
2745
|
+
|| text.match(/['"]command['"]?\s*[:=]\s*['"]([^'"]+)['"]/i);
|
|
2746
|
+
if (!commandMatch?.[1])
|
|
2747
|
+
return null;
|
|
2748
|
+
const command = sanitizeExtractedExecCommand(commandMatch[1]
|
|
2749
|
+
.replace(/\\n/g, '\n')
|
|
2750
|
+
.replace(/\\"/g, '"')
|
|
2751
|
+
.replace(/\\'/g, "'"));
|
|
2752
|
+
return command || null;
|
|
2753
|
+
};
|
|
2754
|
+
const directCommand = parseCommand(candidate);
|
|
2755
|
+
if (directCommand) {
|
|
2756
|
+
return [{
|
|
2757
|
+
name: toolName,
|
|
2758
|
+
arguments: JSON.stringify({ command: directCommand }),
|
|
2759
|
+
}];
|
|
2760
|
+
}
|
|
2761
|
+
const argsStringMatch = candidate.match(/['"]?args['"]?\s*[:=]\s*'([\s\S]*?)'/i)
|
|
2762
|
+
|| candidate.match(/['"]?args['"]?\s*[:=]\s*"([\s\S]*?)"/i);
|
|
2763
|
+
if (argsStringMatch?.[1]) {
|
|
2764
|
+
const argsBody = argsStringMatch[1];
|
|
2765
|
+
const command = parseCommand(argsBody);
|
|
2766
|
+
if (command) {
|
|
2767
|
+
return [{
|
|
2768
|
+
name: toolName,
|
|
2769
|
+
arguments: JSON.stringify({ command }),
|
|
2770
|
+
}];
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
if (toolName === 'exec')
|
|
2774
|
+
return [];
|
|
2775
|
+
return [{
|
|
2776
|
+
name: toolName,
|
|
2777
|
+
arguments: '{}',
|
|
2778
|
+
}];
|
|
2779
|
+
}
|
|
2780
|
+
function extractFunctionCallToolCallsFromText(text, fallbackToolName) {
|
|
2781
|
+
let remaining = text;
|
|
2782
|
+
let cleanText = '';
|
|
2783
|
+
const calls = [];
|
|
2784
|
+
while (remaining.length > 0) {
|
|
2785
|
+
const start = remaining.indexOf(FUNCTION_CALL_START);
|
|
2786
|
+
if (start < 0) {
|
|
2787
|
+
cleanText += remaining;
|
|
2788
|
+
break;
|
|
2789
|
+
}
|
|
2790
|
+
if (start > 0) {
|
|
2791
|
+
cleanText += remaining.slice(0, start);
|
|
2792
|
+
remaining = remaining.slice(start);
|
|
2793
|
+
}
|
|
2794
|
+
const end = remaining.indexOf(FUNCTION_CALL_END);
|
|
2795
|
+
if (end < 0) {
|
|
2796
|
+
cleanText += remaining;
|
|
2797
|
+
break;
|
|
2798
|
+
}
|
|
2799
|
+
const block = remaining.slice(0, end + FUNCTION_CALL_END.length);
|
|
2800
|
+
remaining = remaining.slice(end + FUNCTION_CALL_END.length);
|
|
2801
|
+
const body = block
|
|
2802
|
+
.slice(FUNCTION_CALL_START.length, block.length - FUNCTION_CALL_END.length)
|
|
2803
|
+
.trim();
|
|
2804
|
+
const parsedInvokes = parseFunctionCallBody(body, fallbackToolName);
|
|
2805
|
+
if (parsedInvokes.length === 0) {
|
|
2806
|
+
cleanText += block;
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
calls.push(...parsedInvokes);
|
|
2810
|
+
}
|
|
2811
|
+
return { cleanText, calls };
|
|
2812
|
+
}
|
|
2813
|
+
function extractMinimaxToolCallsFromText(text, fallbackToolName) {
|
|
2814
|
+
let remaining = text;
|
|
2815
|
+
let cleanText = '';
|
|
2816
|
+
const calls = [];
|
|
2817
|
+
while (remaining.length > 0) {
|
|
2818
|
+
const startMatch = findMinimaxToolStart(remaining);
|
|
2819
|
+
const start = startMatch ? startMatch.index : -1;
|
|
2820
|
+
if (start < 0) {
|
|
2821
|
+
cleanText += remaining;
|
|
2822
|
+
break;
|
|
2823
|
+
}
|
|
2824
|
+
if (start > 0) {
|
|
2825
|
+
cleanText += remaining.slice(0, start);
|
|
2826
|
+
remaining = remaining.slice(start);
|
|
2827
|
+
}
|
|
2828
|
+
const endMatch = findMinimaxToolEnd(remaining);
|
|
2829
|
+
if (!endMatch) {
|
|
2830
|
+
cleanText += remaining;
|
|
2831
|
+
break;
|
|
2832
|
+
}
|
|
2833
|
+
const block = remaining.slice(0, endMatch.index + endMatch.token.length);
|
|
2834
|
+
remaining = remaining.slice(endMatch.index + endMatch.token.length);
|
|
2835
|
+
const parsedInvokes = parseMinimaxToolBlock(block, fallbackToolName);
|
|
2836
|
+
if (parsedInvokes.length === 0) {
|
|
2837
|
+
cleanText += block;
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
calls.push(...parsedInvokes);
|
|
2841
|
+
}
|
|
2842
|
+
return { cleanText, calls };
|
|
2843
|
+
}
|
|
2844
|
+
function extractBracketToolCallsFromText(text, fallbackToolName) {
|
|
2845
|
+
let remaining = text;
|
|
2846
|
+
let cleanText = '';
|
|
2847
|
+
const calls = [];
|
|
2848
|
+
while (remaining.length > 0) {
|
|
2849
|
+
const start = remaining.indexOf(BRACKET_TOOL_START);
|
|
2850
|
+
if (start < 0) {
|
|
2851
|
+
cleanText += remaining;
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
if (start > 0) {
|
|
2855
|
+
cleanText += remaining.slice(0, start);
|
|
2856
|
+
remaining = remaining.slice(start);
|
|
2857
|
+
}
|
|
2858
|
+
const end = remaining.indexOf(BRACKET_TOOL_END);
|
|
2859
|
+
if (end < 0) {
|
|
2860
|
+
cleanText += remaining;
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
const block = remaining.slice(0, end + BRACKET_TOOL_END.length);
|
|
2864
|
+
remaining = remaining.slice(end + BRACKET_TOOL_END.length);
|
|
2865
|
+
const parsedInvokes = parseBracketToolBlock(block, fallbackToolName);
|
|
2866
|
+
if (parsedInvokes.length === 0) {
|
|
2867
|
+
cleanText += block;
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
calls.push(...parsedInvokes);
|
|
2871
|
+
}
|
|
2872
|
+
return { cleanText, calls };
|
|
2873
|
+
}
|
|
2874
|
+
function normalizeParsedInvoke(rawName, rawArguments, fallbackToolName) {
|
|
2875
|
+
if (typeof rawName !== 'string' || !rawName.trim())
|
|
2876
|
+
return null;
|
|
2877
|
+
const toolName = normalizeToolName(rawName, fallbackToolName);
|
|
2878
|
+
if (!toolName)
|
|
2879
|
+
return null;
|
|
2880
|
+
let argumentsText = '{}';
|
|
2881
|
+
if (typeof rawArguments === 'string') {
|
|
2882
|
+
argumentsText = rawArguments;
|
|
2883
|
+
}
|
|
2884
|
+
else if (rawArguments !== undefined) {
|
|
2885
|
+
try {
|
|
2886
|
+
argumentsText = JSON.stringify(rawArguments);
|
|
2887
|
+
}
|
|
2888
|
+
catch {
|
|
2889
|
+
argumentsText = String(rawArguments);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return {
|
|
2893
|
+
name: toolName,
|
|
2894
|
+
arguments: argumentsText,
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
function extractToolCallsFromJsonValue(value, fallbackToolName) {
|
|
2898
|
+
if (Array.isArray(value)) {
|
|
2899
|
+
return value.flatMap((item) => extractToolCallsFromJsonValue(item, fallbackToolName));
|
|
2900
|
+
}
|
|
2901
|
+
if (!value || typeof value !== 'object')
|
|
2902
|
+
return [];
|
|
2903
|
+
const obj = value;
|
|
2904
|
+
const calls = [];
|
|
2905
|
+
const push = (name, args) => {
|
|
2906
|
+
const parsed = normalizeParsedInvoke(name, args, fallbackToolName);
|
|
2907
|
+
if (parsed)
|
|
2908
|
+
calls.push(parsed);
|
|
2909
|
+
};
|
|
2910
|
+
if (Array.isArray(obj.tool_calls)) {
|
|
2911
|
+
for (const rawCall of obj.tool_calls) {
|
|
2912
|
+
if (!rawCall || typeof rawCall !== 'object')
|
|
2913
|
+
continue;
|
|
2914
|
+
const call = rawCall;
|
|
2915
|
+
const fn = call.function && typeof call.function === 'object'
|
|
2916
|
+
? call.function
|
|
2917
|
+
: null;
|
|
2918
|
+
push(fn?.name ?? call.name, fn?.arguments ?? call.arguments ?? call.input);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
if (Array.isArray(obj.calls)) {
|
|
2922
|
+
for (const rawCall of obj.calls) {
|
|
2923
|
+
if (!rawCall || typeof rawCall !== 'object')
|
|
2924
|
+
continue;
|
|
2925
|
+
const call = rawCall;
|
|
2926
|
+
const fn = call.function && typeof call.function === 'object'
|
|
2927
|
+
? call.function
|
|
2928
|
+
: null;
|
|
2929
|
+
push(fn?.name ?? call.name, fn?.arguments ?? call.arguments ?? call.input);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
if (obj.invoke && typeof obj.invoke === 'object') {
|
|
2933
|
+
const invoke = obj.invoke;
|
|
2934
|
+
push(invoke.name, invoke.parameters ?? invoke.arguments ?? invoke.input);
|
|
2935
|
+
}
|
|
2936
|
+
const directFunction = obj.function && typeof obj.function === 'object'
|
|
2937
|
+
? obj.function
|
|
2938
|
+
: null;
|
|
2939
|
+
push(obj.name ?? obj.tool_name ?? directFunction?.name, obj.arguments ?? obj.input ?? obj.parameters ?? obj.params ?? directFunction?.arguments);
|
|
2940
|
+
const dedup = new Set();
|
|
2941
|
+
const normalizedCalls = [];
|
|
2942
|
+
for (const call of calls) {
|
|
2943
|
+
const key = `${call.name}\u0000${call.arguments}`;
|
|
2944
|
+
if (dedup.has(key))
|
|
2945
|
+
continue;
|
|
2946
|
+
dedup.add(key);
|
|
2947
|
+
normalizedCalls.push(call);
|
|
2948
|
+
}
|
|
2949
|
+
return normalizedCalls;
|
|
2950
|
+
}
|
|
2951
|
+
function parseJsonToolCallsFromText(text, fallbackToolName) {
|
|
2952
|
+
const inferToolNameFromJsonArguments = (value) => {
|
|
2953
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
2954
|
+
return null;
|
|
2955
|
+
const obj = value;
|
|
2956
|
+
const hasExecShape = (typeof obj.command === 'string'
|
|
2957
|
+
|| typeof obj.cmd === 'string');
|
|
2958
|
+
if (hasExecShape) {
|
|
2959
|
+
return normalizeToolName('exec', fallbackToolName);
|
|
2960
|
+
}
|
|
2961
|
+
const hasBrowserShape = (typeof obj.action === 'string'
|
|
2962
|
+
|| typeof obj.url === 'string'
|
|
2963
|
+
|| typeof obj.targetUrl === 'string'
|
|
2964
|
+
|| typeof obj.targetId === 'string'
|
|
2965
|
+
|| typeof obj.query === 'string');
|
|
2966
|
+
if (hasBrowserShape) {
|
|
2967
|
+
return normalizeToolName('browser', fallbackToolName);
|
|
2968
|
+
}
|
|
2969
|
+
const hasReadShape = (typeof obj.path === 'string'
|
|
2970
|
+
|| typeof obj.file_path === 'string');
|
|
2971
|
+
if (hasReadShape) {
|
|
2972
|
+
return normalizeToolName('read', fallbackToolName);
|
|
2973
|
+
}
|
|
2974
|
+
const hasMessageShape = (typeof obj.to === 'string'
|
|
2975
|
+
&& (typeof obj.body === 'string' || typeof obj.subject === 'string'));
|
|
2976
|
+
if (hasMessageShape) {
|
|
2977
|
+
return normalizeToolName('message', fallbackToolName);
|
|
2978
|
+
}
|
|
2979
|
+
if (typeof fallbackToolName === 'string' && fallbackToolName.trim()) {
|
|
2980
|
+
return normalizeToolName(fallbackToolName, null);
|
|
2981
|
+
}
|
|
2982
|
+
return null;
|
|
2983
|
+
};
|
|
2984
|
+
const trimmed = text.trim();
|
|
2985
|
+
if (!trimmed)
|
|
2986
|
+
return [];
|
|
2987
|
+
const candidates = [trimmed];
|
|
2988
|
+
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
2989
|
+
if (fenced && fenced[1]) {
|
|
2990
|
+
candidates.push(fenced[1].trim());
|
|
2991
|
+
}
|
|
2992
|
+
for (const candidateRaw of candidates) {
|
|
2993
|
+
const candidate = candidateRaw.trim();
|
|
2994
|
+
if (!candidate)
|
|
2995
|
+
continue;
|
|
2996
|
+
if (!(candidate.startsWith('{') || candidate.startsWith('[')))
|
|
2997
|
+
continue;
|
|
2998
|
+
try {
|
|
2999
|
+
const parsed = JSON.parse(candidate);
|
|
3000
|
+
const calls = extractToolCallsFromJsonValue(parsed, fallbackToolName);
|
|
3001
|
+
if (calls.length > 0)
|
|
3002
|
+
return calls;
|
|
3003
|
+
const inferredToolName = inferToolNameFromJsonArguments(parsed);
|
|
3004
|
+
if (inferredToolName) {
|
|
3005
|
+
return [{
|
|
3006
|
+
name: inferredToolName,
|
|
3007
|
+
arguments: JSON.stringify(parsed),
|
|
3008
|
+
}];
|
|
3009
|
+
}
|
|
3010
|
+
if (fallbackToolName
|
|
3011
|
+
&& parsed
|
|
3012
|
+
&& typeof parsed === 'object'
|
|
3013
|
+
&& !Array.isArray(parsed)) {
|
|
3014
|
+
return [{
|
|
3015
|
+
name: fallbackToolName,
|
|
3016
|
+
arguments: JSON.stringify(parsed),
|
|
3017
|
+
}];
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
catch {
|
|
3021
|
+
continue;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return [];
|
|
3025
|
+
}
|
|
3026
|
+
function extractJsonToolCallsFromText(text, fallbackToolName) {
|
|
3027
|
+
const calls = parseJsonToolCallsFromText(text, fallbackToolName);
|
|
3028
|
+
if (calls.length === 0) {
|
|
3029
|
+
return { cleanText: text, calls: [] };
|
|
3030
|
+
}
|
|
3031
|
+
return { cleanText: '', calls };
|
|
3032
|
+
}
|
|
3033
|
+
function isLikelyShellCommand(text) {
|
|
3034
|
+
const trimmed = text.trim();
|
|
3035
|
+
if (!trimmed)
|
|
3036
|
+
return false;
|
|
3037
|
+
return /^[A-Za-z0-9_./:-]+(?:\s+[A-Za-z0-9_./:=@%+,-]+){0,20}$/.test(trimmed);
|
|
3038
|
+
}
|
|
3039
|
+
function sanitizeExtractedExecCommand(raw) {
|
|
3040
|
+
let cmd = raw.trim().replace(/^`+|`+$/g, '').trim();
|
|
3041
|
+
if (!cmd)
|
|
3042
|
+
return '';
|
|
3043
|
+
const punctuationCut = cmd.match(/^([^。;,]+)[。;,]/);
|
|
3044
|
+
if (punctuationCut?.[1] && isLikelyShellCommand(punctuationCut[1])) {
|
|
3045
|
+
cmd = punctuationCut[1].trim();
|
|
3046
|
+
}
|
|
3047
|
+
const cjkIndex = cmd.search(/[\u4e00-\u9fff]/);
|
|
3048
|
+
if (cjkIndex > 0) {
|
|
3049
|
+
const prefix = cmd.slice(0, cjkIndex).replace(/[,。;\s]+$/g, '').trim();
|
|
3050
|
+
if (isLikelyShellCommand(prefix)) {
|
|
3051
|
+
cmd = prefix;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
cmd = cmd.replace(/[,。;.!?]+$/g, '').trim();
|
|
3055
|
+
return cmd;
|
|
3056
|
+
}
|
|
3057
|
+
function extractExecCommandFromNarrativeText(text) {
|
|
3058
|
+
const fencedRegex = /```(?:bash|sh|zsh|shell|cmd|powershell)?\s*([\s\S]*?)```/gi;
|
|
3059
|
+
let fencedMatch = null;
|
|
3060
|
+
while ((fencedMatch = fencedRegex.exec(text)) !== null) {
|
|
3061
|
+
const body = fencedMatch[1] || '';
|
|
3062
|
+
const lines = body.split('\n');
|
|
3063
|
+
for (const rawLine of lines) {
|
|
3064
|
+
const line = rawLine.trim().replace(/^\$\s*/, '');
|
|
3065
|
+
if (!line)
|
|
3066
|
+
continue;
|
|
3067
|
+
if (line.startsWith('#') || line.startsWith('//'))
|
|
3068
|
+
continue;
|
|
3069
|
+
if (!/^[A-Za-z0-9_./:-]+(?:\s+.+)?$/.test(line))
|
|
3070
|
+
continue;
|
|
3071
|
+
if (/^(python|javascript|typescript|json|yaml|xml)\b/i.test(line))
|
|
3072
|
+
continue;
|
|
3073
|
+
const cleaned = sanitizeExtractedExecCommand(line);
|
|
3074
|
+
if (cleaned)
|
|
3075
|
+
return cleaned;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
const inlineMatch = text.match(/(?:command|命令)\s*[::]\s*`?([^\n`]+)`?/i);
|
|
3079
|
+
if (inlineMatch?.[1]) {
|
|
3080
|
+
const cmd = sanitizeExtractedExecCommand(inlineMatch[1]);
|
|
3081
|
+
if (cmd)
|
|
3082
|
+
return cmd;
|
|
3083
|
+
}
|
|
3084
|
+
return null;
|
|
3085
|
+
}
|
|
3086
|
+
function extractNarrativeExecToolCallsFromText(text, fallbackToolName) {
|
|
3087
|
+
const fallback = (fallbackToolName || '').trim();
|
|
3088
|
+
if (fallback.toLowerCase() !== 'exec')
|
|
3089
|
+
return [];
|
|
3090
|
+
const command = extractExecCommandFromNarrativeText(text);
|
|
3091
|
+
if (!command)
|
|
3092
|
+
return [];
|
|
3093
|
+
return [{
|
|
3094
|
+
name: fallback,
|
|
3095
|
+
arguments: JSON.stringify({ command }),
|
|
3096
|
+
}];
|
|
3097
|
+
}
|
|
3098
|
+
function extractTextFromMessageContent(content) {
|
|
3099
|
+
if (typeof content === 'string')
|
|
3100
|
+
return content;
|
|
3101
|
+
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
3102
|
+
const obj = content;
|
|
3103
|
+
if (typeof obj.text === 'string')
|
|
3104
|
+
return obj.text;
|
|
3105
|
+
if (obj.text && typeof obj.text === 'object') {
|
|
3106
|
+
const nested = obj.text;
|
|
3107
|
+
if (typeof nested.value === 'string')
|
|
3108
|
+
return nested.value;
|
|
3109
|
+
}
|
|
3110
|
+
return '';
|
|
3111
|
+
}
|
|
3112
|
+
if (!Array.isArray(content))
|
|
3113
|
+
return '';
|
|
3114
|
+
let text = '';
|
|
3115
|
+
for (const block of content) {
|
|
3116
|
+
if (!block || typeof block !== 'object')
|
|
3117
|
+
continue;
|
|
3118
|
+
const obj = block;
|
|
3119
|
+
if (typeof obj.text === 'string') {
|
|
3120
|
+
text += `${obj.text}\n`;
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
if (obj.text && typeof obj.text === 'object') {
|
|
3124
|
+
const nested = obj.text;
|
|
3125
|
+
if (typeof nested.value === 'string') {
|
|
3126
|
+
text += `${nested.value}\n`;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
return text.trim();
|
|
3131
|
+
}
|
|
3132
|
+
function collectRequestText(request) {
|
|
3133
|
+
const messages = request.messages;
|
|
3134
|
+
if (!Array.isArray(messages))
|
|
3135
|
+
return '';
|
|
3136
|
+
const parts = [];
|
|
3137
|
+
for (const item of messages) {
|
|
3138
|
+
if (!item || typeof item !== 'object')
|
|
3139
|
+
continue;
|
|
3140
|
+
const msg = item;
|
|
3141
|
+
const role = typeof msg.role === 'string' ? msg.role : '';
|
|
3142
|
+
if (role !== 'user' && role !== 'system')
|
|
3143
|
+
continue;
|
|
3144
|
+
const content = extractTextFromMessageContent(msg.content);
|
|
3145
|
+
if (content)
|
|
3146
|
+
parts.push(content);
|
|
3147
|
+
}
|
|
3148
|
+
return parts.join('\n').trim();
|
|
3149
|
+
}
|
|
3150
|
+
function extractFirstUrl(text) {
|
|
3151
|
+
const match = text.match(/https?:\/\/[^\s"'`<>,。;,]+/i);
|
|
3152
|
+
return match?.[0] || null;
|
|
3153
|
+
}
|
|
3154
|
+
function extractBrowserProfileFromPromptText(text) {
|
|
3155
|
+
const match = text.match(/profile\s*=\s*([a-z0-9_.-]+)/i);
|
|
3156
|
+
if (!match?.[1])
|
|
3157
|
+
return null;
|
|
3158
|
+
const profile = match[1].trim();
|
|
3159
|
+
return profile || null;
|
|
3160
|
+
}
|
|
3161
|
+
function extractExecCommandFromPromptText(text) {
|
|
3162
|
+
if (!text.trim())
|
|
3163
|
+
return null;
|
|
3164
|
+
const fromNarrative = extractExecCommandFromNarrativeText(text);
|
|
3165
|
+
if (fromNarrative)
|
|
3166
|
+
return fromNarrative;
|
|
3167
|
+
const commandField = text.match(/(?:command|命令)\s*[::=]\s*([^\n]+)/i);
|
|
3168
|
+
if (commandField?.[1]) {
|
|
3169
|
+
const raw = sanitizeExtractedExecCommand(commandField[1]);
|
|
3170
|
+
if (raw)
|
|
3171
|
+
return raw;
|
|
3172
|
+
}
|
|
3173
|
+
const execVerb = text.match(/(?:执行|运行)\s+([a-z0-9_./-]+(?:\s+[a-z0-9_./:=@%-]+){0,8})/i);
|
|
3174
|
+
if (execVerb?.[1]) {
|
|
3175
|
+
const raw = sanitizeExtractedExecCommand(execVerb[1]);
|
|
3176
|
+
if (raw)
|
|
3177
|
+
return raw;
|
|
3178
|
+
}
|
|
3179
|
+
if (/\bdate\b/i.test(text))
|
|
3180
|
+
return 'date';
|
|
3181
|
+
return null;
|
|
3182
|
+
}
|
|
3183
|
+
function deriveToolInputFallbacksFromRequest(request) {
|
|
3184
|
+
const browserActionEnum = extractBrowserActionEnumFromRequest(request);
|
|
3185
|
+
const browserRequestKindEnum = extractBrowserRequestKindEnumFromRequest(request);
|
|
3186
|
+
const text = collectRequestText(request);
|
|
3187
|
+
if (!text) {
|
|
3188
|
+
if (browserActionEnum.length > 0 || browserRequestKindEnum.length > 0) {
|
|
3189
|
+
return {
|
|
3190
|
+
...(browserActionEnum.length > 0 ? { browserActionEnum } : {}),
|
|
3191
|
+
...(browserRequestKindEnum.length > 0 ? { browserRequestKindEnum } : {}),
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
return {};
|
|
3195
|
+
}
|
|
3196
|
+
const execCommand = extractExecCommandFromPromptText(text) || undefined;
|
|
3197
|
+
const webFetchUrl = extractFirstUrl(text) || undefined;
|
|
3198
|
+
const browserProfile = extractBrowserProfileFromPromptText(text) || undefined;
|
|
3199
|
+
return {
|
|
3200
|
+
execCommand,
|
|
3201
|
+
webFetchUrl,
|
|
3202
|
+
browserProfile,
|
|
3203
|
+
browserActionEnum: browserActionEnum.length > 0 ? browserActionEnum : undefined,
|
|
3204
|
+
browserRequestKindEnum: browserRequestKindEnum.length > 0 ? browserRequestKindEnum : undefined,
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
function extractToolCallsFromText(text, fallbackToolName) {
|
|
3208
|
+
const minimax = extractMinimaxToolCallsFromText(text, fallbackToolName);
|
|
3209
|
+
const bracket = extractBracketToolCallsFromText(minimax.cleanText, fallbackToolName);
|
|
3210
|
+
const functionCall = extractFunctionCallToolCallsFromText(bracket.cleanText, fallbackToolName);
|
|
3211
|
+
const json = extractJsonToolCallsFromText(functionCall.cleanText, fallbackToolName);
|
|
3212
|
+
const combinedCalls = [...minimax.calls, ...bracket.calls, ...functionCall.calls, ...json.calls];
|
|
3213
|
+
if (combinedCalls.length === 0) {
|
|
3214
|
+
const narrativeExec = extractNarrativeExecToolCallsFromText(json.cleanText, fallbackToolName);
|
|
3215
|
+
if (narrativeExec.length > 0) {
|
|
3216
|
+
return {
|
|
3217
|
+
cleanText: '',
|
|
3218
|
+
calls: narrativeExec,
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return {
|
|
3223
|
+
cleanText: json.cleanText,
|
|
3224
|
+
calls: combinedCalls,
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
function resolvePreferredToolNameFromRequest(request) {
|
|
3228
|
+
const toolChoice = request.tool_choice;
|
|
3229
|
+
if (toolChoice && typeof toolChoice === 'object') {
|
|
3230
|
+
const choice = toolChoice;
|
|
3231
|
+
if (typeof choice.name === 'string' && choice.name.trim()) {
|
|
3232
|
+
return choice.name.trim();
|
|
3233
|
+
}
|
|
3234
|
+
if (choice.type === 'tool' && typeof choice.name === 'string' && choice.name.trim()) {
|
|
3235
|
+
return choice.name.trim();
|
|
3236
|
+
}
|
|
3237
|
+
if (choice.function && typeof choice.function === 'object') {
|
|
3238
|
+
const fn = choice.function;
|
|
3239
|
+
if (typeof fn.name === 'string' && fn.name.trim()) {
|
|
3240
|
+
return fn.name.trim();
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
const tools = request.tools;
|
|
3245
|
+
if (Array.isArray(tools) && tools.length === 1) {
|
|
3246
|
+
const firstTool = tools[0];
|
|
3247
|
+
if (firstTool && typeof firstTool === 'object') {
|
|
3248
|
+
const toolObj = firstTool;
|
|
3249
|
+
if (typeof toolObj.name === 'string' && toolObj.name.trim()) {
|
|
3250
|
+
return toolObj.name.trim();
|
|
3251
|
+
}
|
|
3252
|
+
if (toolObj.function && typeof toolObj.function === 'object') {
|
|
3253
|
+
const fn = toolObj.function;
|
|
3254
|
+
if (typeof fn.name === 'string' && fn.name.trim()) {
|
|
3255
|
+
return fn.name.trim();
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
3262
|
+
function extractToolNamesFromRequest(request) {
|
|
3263
|
+
const tools = request.tools;
|
|
3264
|
+
if (!Array.isArray(tools))
|
|
3265
|
+
return [];
|
|
3266
|
+
const names = [];
|
|
3267
|
+
for (const rawTool of tools) {
|
|
3268
|
+
if (!rawTool || typeof rawTool !== 'object')
|
|
3269
|
+
continue;
|
|
3270
|
+
const tool = rawTool;
|
|
3271
|
+
if (typeof tool.name === 'string' && tool.name.trim()) {
|
|
3272
|
+
names.push(tool.name.trim());
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
if (tool.function && typeof tool.function === 'object') {
|
|
3276
|
+
const fn = tool.function;
|
|
3277
|
+
if (typeof fn.name === 'string' && fn.name.trim()) {
|
|
3278
|
+
names.push(fn.name.trim());
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return [...new Set(names)];
|
|
3283
|
+
}
|
|
3284
|
+
function extractBrowserActionEnumFromRequest(request) {
|
|
3285
|
+
const tools = request.tools;
|
|
3286
|
+
if (!Array.isArray(tools))
|
|
3287
|
+
return [];
|
|
3288
|
+
for (const rawTool of tools) {
|
|
3289
|
+
if (!rawTool || typeof rawTool !== 'object')
|
|
3290
|
+
continue;
|
|
3291
|
+
const tool = rawTool;
|
|
3292
|
+
const rawName = typeof tool.name === 'string'
|
|
3293
|
+
? tool.name
|
|
3294
|
+
: (tool.function && typeof tool.function === 'object'
|
|
3295
|
+
? String(tool.function.name || '')
|
|
3296
|
+
: '');
|
|
3297
|
+
if (rawName.trim().toLowerCase() !== 'browser')
|
|
3298
|
+
continue;
|
|
3299
|
+
const schema = (tool.input_schema && typeof tool.input_schema === 'object'
|
|
3300
|
+
? tool.input_schema
|
|
3301
|
+
: (tool.function && typeof tool.function === 'object'
|
|
3302
|
+
? tool.function.parameters
|
|
3303
|
+
: null));
|
|
3304
|
+
if (!schema || typeof schema !== 'object')
|
|
3305
|
+
return [];
|
|
3306
|
+
const properties = schema.properties;
|
|
3307
|
+
if (!properties || typeof properties !== 'object')
|
|
3308
|
+
return [];
|
|
3309
|
+
const actionProp = properties.action;
|
|
3310
|
+
if (!actionProp || typeof actionProp !== 'object')
|
|
3311
|
+
return [];
|
|
3312
|
+
const actionObj = actionProp;
|
|
3313
|
+
const enums = actionObj.enum;
|
|
3314
|
+
if (!Array.isArray(enums))
|
|
3315
|
+
return [];
|
|
3316
|
+
return enums
|
|
3317
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
3318
|
+
.filter((item) => item.length > 0);
|
|
1229
3319
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
3320
|
+
return [];
|
|
3321
|
+
}
|
|
3322
|
+
function extractBrowserRequestKindEnumFromRequest(request) {
|
|
3323
|
+
const tools = request.tools;
|
|
3324
|
+
if (!Array.isArray(tools))
|
|
3325
|
+
return [];
|
|
3326
|
+
for (const rawTool of tools) {
|
|
3327
|
+
if (!rawTool || typeof rawTool !== 'object')
|
|
3328
|
+
continue;
|
|
3329
|
+
const tool = rawTool;
|
|
3330
|
+
const rawName = typeof tool.name === 'string'
|
|
3331
|
+
? tool.name
|
|
3332
|
+
: (tool.function && typeof tool.function === 'object'
|
|
3333
|
+
? String(tool.function.name || '')
|
|
3334
|
+
: '');
|
|
3335
|
+
if (rawName.trim().toLowerCase() !== 'browser')
|
|
3336
|
+
continue;
|
|
3337
|
+
const schema = (tool.input_schema && typeof tool.input_schema === 'object'
|
|
3338
|
+
? tool.input_schema
|
|
3339
|
+
: (tool.function && typeof tool.function === 'object'
|
|
3340
|
+
? tool.function.parameters
|
|
3341
|
+
: null));
|
|
3342
|
+
if (!schema || typeof schema !== 'object')
|
|
3343
|
+
return [];
|
|
3344
|
+
const properties = schema.properties;
|
|
3345
|
+
if (!properties || typeof properties !== 'object')
|
|
3346
|
+
return [];
|
|
3347
|
+
const requestProp = properties.request;
|
|
3348
|
+
if (!requestProp || typeof requestProp !== 'object')
|
|
3349
|
+
return [];
|
|
3350
|
+
const requestObj = requestProp;
|
|
3351
|
+
const requestProperties = requestObj.properties;
|
|
3352
|
+
if (!requestProperties || typeof requestProperties !== 'object')
|
|
3353
|
+
return [];
|
|
3354
|
+
const kindProp = requestProperties.kind;
|
|
3355
|
+
if (!kindProp || typeof kindProp !== 'object')
|
|
3356
|
+
return [];
|
|
3357
|
+
const kindObj = kindProp;
|
|
3358
|
+
const enums = kindObj.enum;
|
|
3359
|
+
if (!Array.isArray(enums))
|
|
3360
|
+
return [];
|
|
3361
|
+
return enums
|
|
3362
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
3363
|
+
.filter((item) => item.length > 0);
|
|
1232
3364
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
onError: (message) => {
|
|
1254
|
-
if (res.writableEnded)
|
|
1255
|
-
return;
|
|
1256
|
-
failOpenAISSE(res, message);
|
|
1257
|
-
},
|
|
3365
|
+
return [];
|
|
3366
|
+
}
|
|
3367
|
+
function findMatchingAvailableToolName(candidate, availableToolNames) {
|
|
3368
|
+
const normalizedAvailable = availableToolNames
|
|
3369
|
+
.map((name) => name.trim())
|
|
3370
|
+
.filter((name) => name.length > 0);
|
|
3371
|
+
if (normalizedAvailable.length === 0)
|
|
3372
|
+
return null;
|
|
3373
|
+
const loweredCandidate = candidate.trim().toLowerCase();
|
|
3374
|
+
if (!loweredCandidate)
|
|
3375
|
+
return null;
|
|
3376
|
+
const exact = normalizedAvailable.find((name) => name.toLowerCase() === loweredCandidate);
|
|
3377
|
+
if (exact)
|
|
3378
|
+
return exact;
|
|
3379
|
+
const separatorMatch = normalizedAvailable.find((name) => {
|
|
3380
|
+
const lowered = name.toLowerCase();
|
|
3381
|
+
return loweredCandidate.startsWith(`${lowered}_`)
|
|
3382
|
+
|| loweredCandidate.startsWith(`${lowered}-`)
|
|
3383
|
+
|| loweredCandidate.endsWith(`_${lowered}`)
|
|
3384
|
+
|| loweredCandidate.endsWith(`-${lowered}`);
|
|
1258
3385
|
});
|
|
3386
|
+
if (separatorMatch)
|
|
3387
|
+
return separatorMatch;
|
|
3388
|
+
if (/(?:browse|browser|navigate|open_url|open-url)/i.test(loweredCandidate)) {
|
|
3389
|
+
const browserTool = normalizedAvailable.find((name) => name.toLowerCase() === 'browser');
|
|
3390
|
+
if (browserTool)
|
|
3391
|
+
return browserTool;
|
|
3392
|
+
}
|
|
3393
|
+
if (/search/i.test(loweredCandidate)) {
|
|
3394
|
+
const searchTool = normalizedAvailable.find((name) => name.toLowerCase() === 'search');
|
|
3395
|
+
if (searchTool)
|
|
3396
|
+
return searchTool;
|
|
3397
|
+
}
|
|
3398
|
+
if (/(?:exec|command|shell|terminal|bash|sh)/i.test(loweredCandidate)) {
|
|
3399
|
+
const execTool = normalizedAvailable.find((name) => name.toLowerCase() === 'exec');
|
|
3400
|
+
if (execTool)
|
|
3401
|
+
return execTool;
|
|
3402
|
+
}
|
|
3403
|
+
return null;
|
|
1259
3404
|
}
|
|
1260
|
-
function
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
3405
|
+
function normalizeToolNameForRequest(rawName, fallbackToolName, availableToolNames) {
|
|
3406
|
+
const normalized = normalizeToolName(rawName, fallbackToolName);
|
|
3407
|
+
const matched = findMatchingAvailableToolName(normalized || rawName, availableToolNames);
|
|
3408
|
+
if (matched)
|
|
3409
|
+
return matched;
|
|
3410
|
+
return normalized;
|
|
3411
|
+
}
|
|
3412
|
+
function parseArgumentsTextToObject(argumentsText) {
|
|
3413
|
+
const trimmed = argumentsText.trim();
|
|
3414
|
+
if (!trimmed)
|
|
3415
|
+
return null;
|
|
3416
|
+
try {
|
|
3417
|
+
const parsed = JSON.parse(trimmed);
|
|
3418
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
3419
|
+
return parsed;
|
|
3420
|
+
}
|
|
3421
|
+
return null;
|
|
3422
|
+
}
|
|
3423
|
+
catch {
|
|
3424
|
+
return null;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
function coerceToolNameByArguments(toolName, argumentsText, availableToolNames) {
|
|
3428
|
+
if (findMatchingAvailableToolName(toolName, availableToolNames)) {
|
|
3429
|
+
return toolName;
|
|
3430
|
+
}
|
|
3431
|
+
const argsObj = parseArgumentsTextToObject(argumentsText) || {};
|
|
3432
|
+
const hasBrowserShape = (typeof argsObj.url === 'string'
|
|
3433
|
+
|| typeof argsObj.targetUrl === 'string'
|
|
3434
|
+
|| typeof argsObj.targetId === 'string'
|
|
3435
|
+
|| typeof argsObj.action === 'string');
|
|
3436
|
+
if (hasBrowserShape) {
|
|
3437
|
+
const browserTool = findMatchingAvailableToolName('browser', availableToolNames);
|
|
3438
|
+
if (browserTool)
|
|
3439
|
+
return browserTool;
|
|
3440
|
+
}
|
|
3441
|
+
const hasExecShape = (typeof argsObj.command === 'string'
|
|
3442
|
+
|| typeof argsObj.cmd === 'string'
|
|
3443
|
+
|| typeof argsObj.path === 'string'
|
|
3444
|
+
|| typeof argsObj.file_path === 'string');
|
|
3445
|
+
if (hasExecShape) {
|
|
3446
|
+
const execTool = findMatchingAvailableToolName('exec', availableToolNames);
|
|
3447
|
+
if (execTool)
|
|
3448
|
+
return execTool;
|
|
3449
|
+
}
|
|
3450
|
+
if (typeof argsObj.path === 'string'
|
|
3451
|
+
|| typeof argsObj.file_path === 'string'
|
|
3452
|
+
|| typeof argsObj.targetId === 'string') {
|
|
3453
|
+
const readTool = findMatchingAvailableToolName('read', availableToolNames);
|
|
3454
|
+
if (readTool)
|
|
3455
|
+
return readTool;
|
|
3456
|
+
}
|
|
3457
|
+
if (typeof argsObj.to === 'string'
|
|
3458
|
+
&& (typeof argsObj.body === 'string' || typeof argsObj.subject === 'string')) {
|
|
3459
|
+
const messageTool = findMatchingAvailableToolName('message', availableToolNames);
|
|
3460
|
+
if (messageTool)
|
|
3461
|
+
return messageTool;
|
|
3462
|
+
}
|
|
3463
|
+
if (availableToolNames.length === 1) {
|
|
3464
|
+
return availableToolNames[0] || toolName;
|
|
3465
|
+
}
|
|
3466
|
+
return toolName;
|
|
3467
|
+
}
|
|
3468
|
+
function shouldForceBrowserOpenToolCall(text, availableToolNames, fallbackUrl) {
|
|
3469
|
+
if (!fallbackUrl)
|
|
3470
|
+
return false;
|
|
3471
|
+
if (!findMatchingAvailableToolName('browser', availableToolNames))
|
|
3472
|
+
return false;
|
|
3473
|
+
if (!text.trim())
|
|
3474
|
+
return false;
|
|
3475
|
+
const refusalPattern = /(?:无法|不能|没法|不具备|cannot|can't|unable|not able|no ability)/i;
|
|
3476
|
+
const browserPattern = /(?:浏览器|browser|网页|website|x\.com|twitter)/i;
|
|
3477
|
+
return refusalPattern.test(text) && browserPattern.test(text);
|
|
3478
|
+
}
|
|
3479
|
+
function parseToolArgumentsAsAnthropicInput(argumentsText, toolName, fallbacks) {
|
|
3480
|
+
const normalizedToolName = normalizeToolName(toolName || '', null);
|
|
3481
|
+
const isExecTool = normalizedToolName === 'exec';
|
|
3482
|
+
const isWebFetchTool = normalizedToolName === 'web_fetch';
|
|
3483
|
+
const isBrowserTool = normalizedToolName === 'browser';
|
|
3484
|
+
const trimmed = argumentsText.trim();
|
|
3485
|
+
const fallbackExecCommand = typeof fallbacks?.execCommand === 'string'
|
|
3486
|
+
? sanitizeExtractedExecCommand(fallbacks.execCommand)
|
|
3487
|
+
: '';
|
|
3488
|
+
const fallbackWebFetchUrl = typeof fallbacks?.webFetchUrl === 'string'
|
|
3489
|
+
? fallbacks.webFetchUrl.trim()
|
|
3490
|
+
: '';
|
|
3491
|
+
const fallbackBrowserProfile = typeof fallbacks?.browserProfile === 'string'
|
|
3492
|
+
? fallbacks.browserProfile.trim()
|
|
3493
|
+
: '';
|
|
3494
|
+
const fallbackBrowserActionEnum = Array.isArray(fallbacks?.browserActionEnum)
|
|
3495
|
+
? fallbacks.browserActionEnum
|
|
3496
|
+
.map((item) => (typeof item === 'string' ? item.trim().toLowerCase() : ''))
|
|
3497
|
+
.filter((item) => item.length > 0)
|
|
3498
|
+
: [];
|
|
3499
|
+
const fallbackBrowserRequestKindEnum = Array.isArray(fallbacks?.browserRequestKindEnum)
|
|
3500
|
+
? fallbacks.browserRequestKindEnum
|
|
3501
|
+
.map((item) => (typeof item === 'string' ? item.trim().toLowerCase() : ''))
|
|
3502
|
+
.filter((item) => item.length > 0)
|
|
3503
|
+
: [];
|
|
3504
|
+
const normalizeExecCommand = (raw) => {
|
|
3505
|
+
let cmd = sanitizeExtractedExecCommand(raw);
|
|
3506
|
+
if (!cmd)
|
|
3507
|
+
return cmd;
|
|
3508
|
+
if (/^openclaw(\s|$)/i.test(cmd)) {
|
|
3509
|
+
cmd = cmd.replace(/^openclaw\b/i, '/usr/local/bin/node /usr/local/bin/openclaw');
|
|
3510
|
+
}
|
|
3511
|
+
else if (/^\/usr\/local\/bin\/openclaw(\s|$)/i.test(cmd)) {
|
|
3512
|
+
cmd = cmd.replace(/^\/usr\/local\/bin\/openclaw\b/i, '/usr/local/bin/node /usr/local/bin/openclaw');
|
|
3513
|
+
}
|
|
3514
|
+
if (/^\/usr\/local\/bin\/node\s+\/usr\/local\/bin\/openclaw\s+cron\s+run\b/i.test(cmd) && !/\s--timeout\b/i.test(cmd)) {
|
|
3515
|
+
cmd = `${cmd} --timeout 300000`;
|
|
3516
|
+
}
|
|
3517
|
+
return cmd;
|
|
3518
|
+
};
|
|
3519
|
+
const normalizeExecInput = (value) => {
|
|
3520
|
+
if (typeof value === 'string') {
|
|
3521
|
+
const cmd = normalizeExecCommand(value);
|
|
3522
|
+
if (cmd)
|
|
3523
|
+
return { command: cmd };
|
|
3524
|
+
if (fallbackExecCommand)
|
|
3525
|
+
return { command: normalizeExecCommand(fallbackExecCommand) };
|
|
3526
|
+
return {};
|
|
3527
|
+
}
|
|
3528
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
3529
|
+
const obj = value;
|
|
3530
|
+
if (typeof obj.command === 'string' && obj.command.trim()) {
|
|
3531
|
+
const normalized = { ...obj };
|
|
3532
|
+
normalized.command = normalizeExecCommand(obj.command);
|
|
3533
|
+
return normalized;
|
|
3534
|
+
}
|
|
3535
|
+
if (typeof obj.cmd === 'string' && obj.cmd.trim()) {
|
|
3536
|
+
const normalized = { ...obj };
|
|
3537
|
+
normalized.command = normalizeExecCommand(obj.cmd);
|
|
3538
|
+
return normalized;
|
|
3539
|
+
}
|
|
3540
|
+
if (typeof obj.value === 'string' && obj.value.trim()) {
|
|
3541
|
+
return { command: normalizeExecCommand(obj.value) };
|
|
3542
|
+
}
|
|
3543
|
+
if (typeof obj.path === 'string' && obj.path.trim()) {
|
|
3544
|
+
const escaped = obj.path.replace(/"/g, '\\"');
|
|
3545
|
+
return { command: normalizeExecCommand(`cat "${escaped}"`) };
|
|
3546
|
+
}
|
|
3547
|
+
if (typeof obj.file_path === 'string' && obj.file_path.trim()) {
|
|
3548
|
+
const escaped = obj.file_path.replace(/"/g, '\\"');
|
|
3549
|
+
return { command: normalizeExecCommand(`cat "${escaped}"`) };
|
|
3550
|
+
}
|
|
3551
|
+
if (fallbackExecCommand) {
|
|
3552
|
+
return {
|
|
3553
|
+
...obj,
|
|
3554
|
+
command: normalizeExecCommand(fallbackExecCommand),
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
return obj;
|
|
3558
|
+
}
|
|
3559
|
+
if (value === null || value === undefined) {
|
|
3560
|
+
if (fallbackExecCommand)
|
|
3561
|
+
return { command: normalizeExecCommand(fallbackExecCommand) };
|
|
3562
|
+
return {};
|
|
3563
|
+
}
|
|
3564
|
+
const stringValue = normalizeExecCommand(String(value));
|
|
3565
|
+
if (stringValue)
|
|
3566
|
+
return { command: stringValue };
|
|
3567
|
+
if (fallbackExecCommand)
|
|
3568
|
+
return { command: normalizeExecCommand(fallbackExecCommand) };
|
|
3569
|
+
return {};
|
|
3570
|
+
};
|
|
3571
|
+
const normalizeWebFetchInput = (value) => {
|
|
3572
|
+
if (typeof value === 'string') {
|
|
3573
|
+
const url = value.trim();
|
|
3574
|
+
if (url)
|
|
3575
|
+
return { url };
|
|
3576
|
+
if (fallbackWebFetchUrl)
|
|
3577
|
+
return { url: fallbackWebFetchUrl };
|
|
3578
|
+
return {};
|
|
3579
|
+
}
|
|
3580
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
3581
|
+
const obj = value;
|
|
3582
|
+
if (typeof obj.url === 'string' && obj.url.trim()) {
|
|
3583
|
+
return { ...obj, url: obj.url.trim() };
|
|
3584
|
+
}
|
|
3585
|
+
if (fallbackWebFetchUrl) {
|
|
3586
|
+
return { ...obj, url: fallbackWebFetchUrl };
|
|
3587
|
+
}
|
|
3588
|
+
return obj;
|
|
3589
|
+
}
|
|
3590
|
+
if (fallbackWebFetchUrl)
|
|
3591
|
+
return { url: fallbackWebFetchUrl };
|
|
3592
|
+
return {};
|
|
3593
|
+
};
|
|
3594
|
+
const normalizeBrowserInput = (value) => {
|
|
3595
|
+
if (typeof value === 'string') {
|
|
3596
|
+
const url = value.trim();
|
|
3597
|
+
if (!url)
|
|
3598
|
+
return {};
|
|
3599
|
+
return { action: 'open', url, targetUrl: url };
|
|
3600
|
+
}
|
|
3601
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
3602
|
+
const obj = value;
|
|
3603
|
+
const normalized = { ...obj };
|
|
3604
|
+
if (typeof normalized.url === 'string') {
|
|
3605
|
+
normalized.url = normalized.url.trim();
|
|
3606
|
+
}
|
|
3607
|
+
if (typeof normalized.targetUrl === 'string') {
|
|
3608
|
+
normalized.targetUrl = normalized.targetUrl.trim();
|
|
3609
|
+
}
|
|
3610
|
+
const toFiniteNumber = (raw) => {
|
|
3611
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
3612
|
+
return raw;
|
|
3613
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
3614
|
+
const parsed = Number(raw.trim());
|
|
3615
|
+
if (Number.isFinite(parsed))
|
|
3616
|
+
return parsed;
|
|
3617
|
+
}
|
|
3618
|
+
return undefined;
|
|
3619
|
+
};
|
|
3620
|
+
const parseWaitMs = () => {
|
|
3621
|
+
const directRaw = normalized.timeMs ?? normalized.waitMs ?? normalized.wait ?? normalized.time;
|
|
3622
|
+
const directValue = toFiniteNumber(directRaw);
|
|
3623
|
+
if (directValue !== undefined && directValue > 0) {
|
|
3624
|
+
return Math.round(directValue);
|
|
3625
|
+
}
|
|
3626
|
+
const durationRaw = normalized.duration ?? normalized.delay ?? normalized.sleep;
|
|
3627
|
+
const durationValue = toFiniteNumber(durationRaw);
|
|
3628
|
+
if (durationValue !== undefined && durationValue > 0) {
|
|
3629
|
+
// Most model outputs express "duration" in seconds.
|
|
3630
|
+
if (durationValue <= 60)
|
|
3631
|
+
return Math.round(durationValue * 1000);
|
|
3632
|
+
return Math.round(durationValue);
|
|
3633
|
+
}
|
|
3634
|
+
return undefined;
|
|
3635
|
+
};
|
|
3636
|
+
const parseTimeoutMs = () => {
|
|
3637
|
+
const timeoutRaw = normalized.timeoutMs ?? normalized.timeout;
|
|
3638
|
+
const timeoutValue = toFiniteNumber(timeoutRaw);
|
|
3639
|
+
if (timeoutValue === undefined || timeoutValue <= 0)
|
|
3640
|
+
return undefined;
|
|
3641
|
+
if (timeoutValue <= 60)
|
|
3642
|
+
return Math.round(timeoutValue * 1000);
|
|
3643
|
+
return Math.round(timeoutValue);
|
|
3644
|
+
};
|
|
3645
|
+
const parseLoadState = () => {
|
|
3646
|
+
const rawLoadState = (normalized.loadState
|
|
3647
|
+
?? normalized.state
|
|
3648
|
+
?? normalized.load);
|
|
3649
|
+
if (typeof rawLoadState !== 'string')
|
|
3650
|
+
return undefined;
|
|
3651
|
+
const lowered = rawLoadState.trim().toLowerCase();
|
|
3652
|
+
if (!lowered)
|
|
3653
|
+
return undefined;
|
|
3654
|
+
if (lowered === 'load' || lowered === 'domcontentloaded' || lowered === 'networkidle') {
|
|
3655
|
+
return lowered;
|
|
3656
|
+
}
|
|
3657
|
+
return undefined;
|
|
3658
|
+
};
|
|
3659
|
+
const parseRequestObject = () => {
|
|
3660
|
+
if (normalized.request && typeof normalized.request === 'object' && !Array.isArray(normalized.request)) {
|
|
3661
|
+
return { ...normalized.request };
|
|
3662
|
+
}
|
|
3663
|
+
if (typeof normalized.request === 'string') {
|
|
3664
|
+
const requestText = normalized.request.trim();
|
|
3665
|
+
if (requestText.startsWith('{') || requestText.startsWith('[')) {
|
|
3666
|
+
try {
|
|
3667
|
+
const parsedRequest = JSON.parse(requestText);
|
|
3668
|
+
if (parsedRequest && typeof parsedRequest === 'object' && !Array.isArray(parsedRequest)) {
|
|
3669
|
+
return { ...parsedRequest };
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
catch {
|
|
3673
|
+
// Keep fallback empty object when request text is not valid JSON.
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
return {};
|
|
3678
|
+
};
|
|
3679
|
+
let action = '';
|
|
3680
|
+
if (typeof normalized.action === 'string' && normalized.action.trim()) {
|
|
3681
|
+
action = normalized.action.trim().toLowerCase();
|
|
3682
|
+
}
|
|
3683
|
+
else if (typeof normalized.url === 'string' && normalized.url) {
|
|
3684
|
+
action = 'open';
|
|
3685
|
+
}
|
|
3686
|
+
else if (typeof normalized.query === 'string' && normalized.query.trim()) {
|
|
3687
|
+
action = 'search';
|
|
3688
|
+
}
|
|
3689
|
+
const allowedActions = new Set((fallbackBrowserActionEnum.length > 0
|
|
3690
|
+
? fallbackBrowserActionEnum
|
|
3691
|
+
: [
|
|
3692
|
+
'open',
|
|
3693
|
+
'close',
|
|
3694
|
+
'stop',
|
|
3695
|
+
'start',
|
|
3696
|
+
'console',
|
|
3697
|
+
'status',
|
|
3698
|
+
'navigate',
|
|
3699
|
+
'profiles',
|
|
3700
|
+
'focus',
|
|
3701
|
+
'upload',
|
|
3702
|
+
'tabs',
|
|
3703
|
+
'screenshot',
|
|
3704
|
+
'snapshot',
|
|
3705
|
+
'pdf',
|
|
3706
|
+
'dialog',
|
|
3707
|
+
'act',
|
|
3708
|
+
])
|
|
3709
|
+
.map((item) => item.toLowerCase()));
|
|
3710
|
+
const allowedRequestKinds = new Set((fallbackBrowserRequestKindEnum.length > 0
|
|
3711
|
+
? fallbackBrowserRequestKindEnum
|
|
3712
|
+
: [
|
|
3713
|
+
'fill',
|
|
3714
|
+
'close',
|
|
3715
|
+
'type',
|
|
3716
|
+
'resize',
|
|
3717
|
+
'wait',
|
|
3718
|
+
'select',
|
|
3719
|
+
'click',
|
|
3720
|
+
'drag',
|
|
3721
|
+
'evaluate',
|
|
3722
|
+
'hover',
|
|
3723
|
+
'press',
|
|
3724
|
+
])
|
|
3725
|
+
.map((item) => item.toLowerCase()));
|
|
3726
|
+
const isAllowedAction = (candidate) => {
|
|
3727
|
+
if (!candidate)
|
|
3728
|
+
return false;
|
|
3729
|
+
return allowedActions.has(candidate.toLowerCase());
|
|
3730
|
+
};
|
|
3731
|
+
const isAllowedRequestKind = (candidate) => {
|
|
3732
|
+
if (!candidate)
|
|
3733
|
+
return false;
|
|
3734
|
+
return allowedRequestKinds.has(candidate.toLowerCase());
|
|
3735
|
+
};
|
|
3736
|
+
const asNonEmptyString = (raw) => {
|
|
3737
|
+
if (typeof raw !== 'string')
|
|
3738
|
+
return undefined;
|
|
3739
|
+
const value = raw.trim();
|
|
3740
|
+
return value || undefined;
|
|
3741
|
+
};
|
|
3742
|
+
const resolveRef = () => (asNonEmptyString(normalized.ref)
|
|
3743
|
+
|| asNonEmptyString(normalized.targetId)
|
|
3744
|
+
|| asNonEmptyString(normalized.element));
|
|
3745
|
+
const resolveTextValue = () => (asNonEmptyString(normalized.text)
|
|
3746
|
+
|| asNonEmptyString(normalized.value)
|
|
3747
|
+
|| asNonEmptyString(normalized.input)
|
|
3748
|
+
|| asNonEmptyString(normalized.content)
|
|
3749
|
+
|| asNonEmptyString(normalized.query));
|
|
3750
|
+
const resolveActKind = (kind) => {
|
|
3751
|
+
const lowered = kind.trim().toLowerCase();
|
|
3752
|
+
if (isAllowedRequestKind(lowered))
|
|
3753
|
+
return lowered;
|
|
3754
|
+
const kindAlias = {
|
|
3755
|
+
input: 'type',
|
|
3756
|
+
tap: 'click',
|
|
3757
|
+
dblclick: 'click',
|
|
3758
|
+
doubleclick: 'click',
|
|
3759
|
+
double_click: 'click',
|
|
3760
|
+
keypress: 'press',
|
|
3761
|
+
execute: 'evaluate',
|
|
3762
|
+
execute_js: 'evaluate',
|
|
3763
|
+
'execute-js': 'evaluate',
|
|
3764
|
+
eval: 'evaluate',
|
|
3765
|
+
scroll: 'press',
|
|
3766
|
+
scrolldown: 'press',
|
|
3767
|
+
scrollup: 'press',
|
|
3768
|
+
scrollintoview: 'hover',
|
|
3769
|
+
'scroll-into-view': 'hover',
|
|
3770
|
+
wait_for_load_state: 'wait',
|
|
3771
|
+
waitforloadstate: 'wait',
|
|
3772
|
+
wait_for_selector: 'wait',
|
|
3773
|
+
waitforselector: 'wait',
|
|
3774
|
+
};
|
|
3775
|
+
const aliased = kindAlias[lowered];
|
|
3776
|
+
if (aliased && isAllowedRequestKind(aliased))
|
|
3777
|
+
return aliased;
|
|
3778
|
+
if (isAllowedRequestKind('wait'))
|
|
3779
|
+
return 'wait';
|
|
3780
|
+
const [firstAllowed] = [...allowedRequestKinds];
|
|
3781
|
+
return firstAllowed || 'wait';
|
|
3782
|
+
};
|
|
3783
|
+
const setAction = (nextAction) => {
|
|
3784
|
+
action = nextAction.toLowerCase();
|
|
3785
|
+
normalized.action = action;
|
|
3786
|
+
};
|
|
3787
|
+
const setActRequest = (kind, extras = {}) => {
|
|
3788
|
+
const request = parseRequestObject();
|
|
3789
|
+
request.kind = resolveActKind(kind);
|
|
3790
|
+
for (const [key, rawValue] of Object.entries(extras)) {
|
|
3791
|
+
if (rawValue === undefined)
|
|
3792
|
+
continue;
|
|
3793
|
+
request[key] = rawValue;
|
|
3794
|
+
}
|
|
3795
|
+
normalized.request = request;
|
|
3796
|
+
setAction('act');
|
|
3797
|
+
};
|
|
3798
|
+
const actionAlias = {
|
|
3799
|
+
launch: 'open',
|
|
3800
|
+
start: 'open',
|
|
3801
|
+
goto: 'open',
|
|
3802
|
+
'open-url': 'open',
|
|
3803
|
+
open_url: 'open',
|
|
3804
|
+
openurl: 'open',
|
|
3805
|
+
'wait-for-load-state': 'wait',
|
|
3806
|
+
wait_for_load_state: 'wait',
|
|
3807
|
+
waitforloadstate: 'wait',
|
|
3808
|
+
'wait-for-selector': 'wait',
|
|
3809
|
+
wait_for_selector: 'wait',
|
|
3810
|
+
waitforselector: 'wait',
|
|
3811
|
+
sleep: 'wait',
|
|
3812
|
+
delay: 'wait',
|
|
3813
|
+
pause: 'wait',
|
|
3814
|
+
scrolldown: 'scroll',
|
|
3815
|
+
scroll_down: 'scroll',
|
|
3816
|
+
scrollup: 'scroll',
|
|
3817
|
+
scroll_up: 'scroll',
|
|
3818
|
+
eval: 'evaluate',
|
|
3819
|
+
execute: 'evaluate',
|
|
3820
|
+
execute_js: 'evaluate',
|
|
3821
|
+
'execute-js': 'evaluate',
|
|
3822
|
+
extract: 'snapshot',
|
|
3823
|
+
scrape: 'snapshot',
|
|
3824
|
+
read: 'snapshot',
|
|
3825
|
+
html: 'snapshot',
|
|
3826
|
+
dom: 'snapshot',
|
|
3827
|
+
content: 'snapshot',
|
|
3828
|
+
input: 'type',
|
|
3829
|
+
tap: 'click',
|
|
3830
|
+
};
|
|
3831
|
+
const aliasedAction = action ? actionAlias[action] : undefined;
|
|
3832
|
+
if (typeof aliasedAction === 'string' && aliasedAction) {
|
|
3833
|
+
setAction(aliasedAction);
|
|
3834
|
+
}
|
|
3835
|
+
if (!action) {
|
|
3836
|
+
if (typeof normalized.url === 'string' && normalized.url && isAllowedAction('open')) {
|
|
3837
|
+
setAction('open');
|
|
3838
|
+
}
|
|
3839
|
+
else if (typeof normalized.query === 'string' && normalized.query.trim() && isAllowedAction('search')) {
|
|
3840
|
+
setAction('search');
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
if (action && !isAllowedAction(action)) {
|
|
3844
|
+
if (isAllowedAction('act')) {
|
|
3845
|
+
if (action === 'wait') {
|
|
3846
|
+
const waitExtras = {};
|
|
3847
|
+
const waitMs = parseWaitMs();
|
|
3848
|
+
const timeoutMs = parseTimeoutMs();
|
|
3849
|
+
const loadState = parseLoadState();
|
|
3850
|
+
if (waitMs !== undefined)
|
|
3851
|
+
waitExtras.timeMs = waitMs;
|
|
3852
|
+
if (timeoutMs !== undefined)
|
|
3853
|
+
waitExtras.timeoutMs = timeoutMs;
|
|
3854
|
+
if (typeof normalized.selector === 'string' && normalized.selector.trim()) {
|
|
3855
|
+
waitExtras.selector = normalized.selector.trim();
|
|
3856
|
+
}
|
|
3857
|
+
if (typeof normalized.text === 'string' && normalized.text.trim()) {
|
|
3858
|
+
waitExtras.text = normalized.text.trim();
|
|
3859
|
+
}
|
|
3860
|
+
if (typeof normalized.textGone === 'string' && normalized.textGone.trim()) {
|
|
3861
|
+
waitExtras.textGone = normalized.textGone.trim();
|
|
3862
|
+
}
|
|
3863
|
+
if (loadState)
|
|
3864
|
+
waitExtras.loadState = loadState;
|
|
3865
|
+
if (Object.keys(waitExtras).length === 0) {
|
|
3866
|
+
waitExtras.timeMs = 1000;
|
|
3867
|
+
}
|
|
3868
|
+
setActRequest('wait', waitExtras);
|
|
3869
|
+
}
|
|
3870
|
+
else if (action === 'click') {
|
|
3871
|
+
setActRequest('click', {
|
|
3872
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3873
|
+
...(asNonEmptyString(normalized.selector) ? { selector: asNonEmptyString(normalized.selector) } : {}),
|
|
3874
|
+
...(typeof normalized.doubleClick === 'boolean' ? { doubleClick: normalized.doubleClick } : {}),
|
|
3875
|
+
...(asNonEmptyString(normalized.button) ? { button: asNonEmptyString(normalized.button) } : {}),
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
else if (action === 'type' || action === 'fill') {
|
|
3879
|
+
setActRequest(action === 'fill' ? 'fill' : 'type', {
|
|
3880
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3881
|
+
...(asNonEmptyString(normalized.selector) ? { selector: asNonEmptyString(normalized.selector) } : {}),
|
|
3882
|
+
...(resolveTextValue() ? { text: resolveTextValue() } : {}),
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
else if (action === 'press') {
|
|
3886
|
+
setActRequest('press', {
|
|
3887
|
+
...(asNonEmptyString(normalized.key) ? { key: asNonEmptyString(normalized.key) } : { key: 'Enter' }),
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
else if (action === 'hover') {
|
|
3891
|
+
setActRequest('hover', {
|
|
3892
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3893
|
+
...(asNonEmptyString(normalized.selector) ? { selector: asNonEmptyString(normalized.selector) } : {}),
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
else if (action === 'select') {
|
|
3897
|
+
const values = Array.isArray(normalized.values)
|
|
3898
|
+
? normalized.values
|
|
3899
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
3900
|
+
.filter((item) => item.length > 0)
|
|
3901
|
+
: [];
|
|
3902
|
+
const value = asNonEmptyString(normalized.value);
|
|
3903
|
+
setActRequest('select', {
|
|
3904
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3905
|
+
...(asNonEmptyString(normalized.selector) ? { selector: asNonEmptyString(normalized.selector) } : {}),
|
|
3906
|
+
...(values.length > 0 ? { values } : (value ? { values: [value] } : {})),
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
else if (action === 'resize') {
|
|
3910
|
+
const width = toFiniteNumber(normalized.width);
|
|
3911
|
+
const height = toFiniteNumber(normalized.height);
|
|
3912
|
+
setActRequest('resize', {
|
|
3913
|
+
...(width !== undefined ? { width: Math.round(width) } : {}),
|
|
3914
|
+
...(height !== undefined ? { height: Math.round(height) } : {}),
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
else if (action === 'drag') {
|
|
3918
|
+
setActRequest('drag', {
|
|
3919
|
+
...(asNonEmptyString(normalized.startRef) ? { startRef: asNonEmptyString(normalized.startRef) } : {}),
|
|
3920
|
+
...(asNonEmptyString(normalized.endRef) ? { endRef: asNonEmptyString(normalized.endRef) } : {}),
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
else if (action === 'navigate' || action === 'search' || action === 'open') {
|
|
3924
|
+
if (isAllowedAction('open') && typeof normalized.url === 'string' && normalized.url) {
|
|
3925
|
+
setAction('open');
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
else if ((action === 'scroll' || action === 'evaluate')) {
|
|
3929
|
+
const direction = typeof normalized.direction === 'string' ? normalized.direction.trim().toLowerCase() : '';
|
|
3930
|
+
const script = typeof normalized.script === 'string'
|
|
3931
|
+
? normalized.script.trim()
|
|
3932
|
+
: (typeof normalized.fn === 'string' ? normalized.fn.trim() : '');
|
|
3933
|
+
if (action === 'evaluate' && script && !/scroll/i.test(script)) {
|
|
3934
|
+
const timeoutMs = parseTimeoutMs();
|
|
3935
|
+
setActRequest('evaluate', {
|
|
3936
|
+
fn: script,
|
|
3937
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3938
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3941
|
+
else {
|
|
3942
|
+
setActRequest('press', { key: direction === 'up' ? 'PageUp' : 'PageDown' });
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
else if (action === 'scrollintoview') {
|
|
3946
|
+
setActRequest('hover', {
|
|
3947
|
+
...(resolveRef() ? { ref: resolveRef() } : {}),
|
|
3948
|
+
...(asNonEmptyString(normalized.selector) ? { selector: asNonEmptyString(normalized.selector) } : {}),
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
else if (action === 'snapshot' && isAllowedAction('screenshot')) {
|
|
3952
|
+
setAction('screenshot');
|
|
3953
|
+
}
|
|
3954
|
+
else if (action === 'close') {
|
|
3955
|
+
setActRequest('close');
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
else if (action === 'navigate' || action === 'search') {
|
|
3959
|
+
if (isAllowedAction('open') && typeof normalized.url === 'string' && normalized.url) {
|
|
3960
|
+
setAction('open');
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
else if (action === 'snapshot' && !isAllowedAction('snapshot') && isAllowedAction('screenshot')) {
|
|
3964
|
+
setAction('screenshot');
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
if (action && !isAllowedAction(action)) {
|
|
3968
|
+
const navLikeAction = new Set([
|
|
3969
|
+
'open',
|
|
3970
|
+
'navigate',
|
|
3971
|
+
'search',
|
|
3972
|
+
'goto',
|
|
3973
|
+
'launch',
|
|
3974
|
+
'start',
|
|
3975
|
+
]);
|
|
3976
|
+
if (navLikeAction.has(action)
|
|
3977
|
+
&& isAllowedAction('open')
|
|
3978
|
+
&& typeof normalized.url === 'string'
|
|
3979
|
+
&& normalized.url) {
|
|
3980
|
+
setAction('open');
|
|
3981
|
+
}
|
|
3982
|
+
else if (isAllowedAction('act')) {
|
|
3983
|
+
const waitMs = parseWaitMs();
|
|
3984
|
+
setActRequest('wait', { timeMs: waitMs ?? 1000 });
|
|
3985
|
+
}
|
|
3986
|
+
else if (isAllowedAction('snapshot')) {
|
|
3987
|
+
setAction('snapshot');
|
|
3988
|
+
}
|
|
3989
|
+
else if (isAllowedAction('screenshot')) {
|
|
3990
|
+
setAction('screenshot');
|
|
3991
|
+
}
|
|
3992
|
+
else {
|
|
3993
|
+
const [firstAllowed] = [...allowedActions];
|
|
3994
|
+
if (firstAllowed)
|
|
3995
|
+
setAction(firstAllowed);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
if (!normalized.url && fallbackWebFetchUrl) {
|
|
3999
|
+
normalized.url = fallbackWebFetchUrl;
|
|
4000
|
+
if (!action && isAllowedAction('open')) {
|
|
4001
|
+
setAction('open');
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
if (!normalized.targetUrl && typeof normalized.url === 'string' && normalized.url) {
|
|
4005
|
+
normalized.targetUrl = normalized.url;
|
|
4006
|
+
}
|
|
4007
|
+
if (!normalized.profile && fallbackBrowserProfile) {
|
|
4008
|
+
normalized.profile = fallbackBrowserProfile;
|
|
4009
|
+
}
|
|
4010
|
+
return normalized;
|
|
4011
|
+
}
|
|
4012
|
+
if (fallbackWebFetchUrl) {
|
|
4013
|
+
return {
|
|
4014
|
+
action: 'open',
|
|
4015
|
+
url: fallbackWebFetchUrl,
|
|
4016
|
+
targetUrl: fallbackWebFetchUrl,
|
|
4017
|
+
...(fallbackBrowserProfile ? { profile: fallbackBrowserProfile } : {}),
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
return {};
|
|
4021
|
+
};
|
|
4022
|
+
if (!trimmed) {
|
|
4023
|
+
if (isExecTool && fallbackExecCommand) {
|
|
4024
|
+
return { command: normalizeExecCommand(fallbackExecCommand) };
|
|
4025
|
+
}
|
|
4026
|
+
if (isWebFetchTool && fallbackWebFetchUrl) {
|
|
4027
|
+
return { url: fallbackWebFetchUrl };
|
|
4028
|
+
}
|
|
4029
|
+
if (isBrowserTool && fallbackWebFetchUrl) {
|
|
4030
|
+
return {
|
|
4031
|
+
action: 'open',
|
|
4032
|
+
url: fallbackWebFetchUrl,
|
|
4033
|
+
targetUrl: fallbackWebFetchUrl,
|
|
4034
|
+
...(fallbackBrowserProfile ? { profile: fallbackBrowserProfile } : {}),
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
return {};
|
|
4038
|
+
}
|
|
4039
|
+
try {
|
|
4040
|
+
const parsed = JSON.parse(trimmed);
|
|
4041
|
+
if (isExecTool) {
|
|
4042
|
+
return normalizeExecInput(parsed);
|
|
4043
|
+
}
|
|
4044
|
+
if (isWebFetchTool) {
|
|
4045
|
+
return normalizeWebFetchInput(parsed);
|
|
4046
|
+
}
|
|
4047
|
+
if (isBrowserTool) {
|
|
4048
|
+
return normalizeBrowserInput(parsed);
|
|
4049
|
+
}
|
|
4050
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
4051
|
+
return parsed;
|
|
4052
|
+
}
|
|
4053
|
+
return { value: parsed };
|
|
4054
|
+
}
|
|
4055
|
+
catch {
|
|
4056
|
+
if (isExecTool) {
|
|
4057
|
+
const commandMatch = trimmed.match(/"command"\s*:\s*"([^"]+)"/i);
|
|
4058
|
+
if (commandMatch && commandMatch[1]) {
|
|
4059
|
+
return { command: normalizeExecCommand(commandMatch[1]) };
|
|
4060
|
+
}
|
|
4061
|
+
const cmdMatch = trimmed.match(/"cmd"\s*:\s*"([^"]+)"/i);
|
|
4062
|
+
if (cmdMatch && cmdMatch[1]) {
|
|
4063
|
+
return { command: normalizeExecCommand(cmdMatch[1]) };
|
|
4064
|
+
}
|
|
4065
|
+
const normalized = normalizeExecCommand(trimmed);
|
|
4066
|
+
if (normalized)
|
|
4067
|
+
return { command: normalized };
|
|
4068
|
+
if (fallbackExecCommand)
|
|
4069
|
+
return { command: normalizeExecCommand(fallbackExecCommand) };
|
|
4070
|
+
return {};
|
|
4071
|
+
}
|
|
4072
|
+
if (isWebFetchTool) {
|
|
4073
|
+
const urlMatch = trimmed.match(/"url"\s*:\s*"([^"]+)"/i);
|
|
4074
|
+
if (urlMatch?.[1])
|
|
4075
|
+
return { url: urlMatch[1].trim() };
|
|
4076
|
+
const inline = extractFirstUrl(trimmed);
|
|
4077
|
+
if (inline)
|
|
4078
|
+
return { url: inline };
|
|
4079
|
+
if (fallbackWebFetchUrl)
|
|
4080
|
+
return { url: fallbackWebFetchUrl };
|
|
4081
|
+
return {};
|
|
4082
|
+
}
|
|
4083
|
+
if (isBrowserTool) {
|
|
4084
|
+
const inline = extractFirstUrl(trimmed);
|
|
4085
|
+
if (inline)
|
|
4086
|
+
return { action: 'open', url: inline, targetUrl: inline };
|
|
4087
|
+
if (fallbackWebFetchUrl) {
|
|
4088
|
+
return {
|
|
4089
|
+
action: 'open',
|
|
4090
|
+
url: fallbackWebFetchUrl,
|
|
4091
|
+
targetUrl: fallbackWebFetchUrl,
|
|
4092
|
+
...(fallbackBrowserProfile ? { profile: fallbackBrowserProfile } : {}),
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
return {};
|
|
4096
|
+
}
|
|
4097
|
+
return { value: trimmed };
|
|
4098
|
+
}
|
|
1270
4099
|
}
|
|
1271
4100
|
async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
1272
4101
|
const normalized = normalizeOpenAIRequest(rawRequest);
|
|
@@ -1274,6 +4103,9 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1274
4103
|
sendOpenAIJsonError(res, 400, 'messages 不能为空');
|
|
1275
4104
|
return;
|
|
1276
4105
|
}
|
|
4106
|
+
const preferredToolName = resolvePreferredToolNameFromRequest(normalized.requestPayload);
|
|
4107
|
+
const availableToolNames = extractToolNamesFromRequest(normalized.requestPayload);
|
|
4108
|
+
const toolInputFallbacks = deriveToolInputFallbacksFromRequest(normalized.requestPayload);
|
|
1277
4109
|
if (isCursorOpenAIChallenge(rawRequest)) {
|
|
1278
4110
|
const completionId = `chatcmpl_${Math.random().toString(36).slice(2, 12)}`;
|
|
1279
4111
|
const createdAt = Math.floor(Date.now() / 1000);
|
|
@@ -1323,6 +4155,18 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1323
4155
|
let sellerId = '';
|
|
1324
4156
|
let outputText = '';
|
|
1325
4157
|
let finished = false;
|
|
4158
|
+
let sawToolCall = false;
|
|
4159
|
+
let nextToolIndex = 0;
|
|
4160
|
+
const toolCalls = new Map();
|
|
4161
|
+
const anthropicToolBlockMap = new Map();
|
|
4162
|
+
const allocateToolIndex = (anthropicBlockIndex) => {
|
|
4163
|
+
const existing = anthropicToolBlockMap.get(anthropicBlockIndex);
|
|
4164
|
+
if (existing !== undefined)
|
|
4165
|
+
return existing;
|
|
4166
|
+
const idx = nextToolIndex++;
|
|
4167
|
+
anthropicToolBlockMap.set(anthropicBlockIndex, idx);
|
|
4168
|
+
return idx;
|
|
4169
|
+
};
|
|
1326
4170
|
const finishWithError = (message) => {
|
|
1327
4171
|
if (finished)
|
|
1328
4172
|
return;
|
|
@@ -1343,6 +4187,62 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1343
4187
|
if (sellerId) {
|
|
1344
4188
|
headers['x-clawmarket-seller'] = sellerId;
|
|
1345
4189
|
}
|
|
4190
|
+
const parsedFromText = extractToolCallsFromText(outputText, preferredToolName);
|
|
4191
|
+
outputText = parsedFromText.cleanText;
|
|
4192
|
+
for (const tool of parsedFromText.calls) {
|
|
4193
|
+
const normalizedBaseName = normalizeToolNameForRequest(tool.name, preferredToolName, availableToolNames) || tool.name || 'tool';
|
|
4194
|
+
const normalizedToolName = coerceToolNameByArguments(normalizedBaseName, tool.arguments, availableToolNames);
|
|
4195
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(tool.arguments, normalizedToolName, toolInputFallbacks);
|
|
4196
|
+
const toolIndex = nextToolIndex++;
|
|
4197
|
+
applyOpenAIToolCallDelta(toolCalls, {
|
|
4198
|
+
index: toolIndex,
|
|
4199
|
+
id: `call_${completionId}_${toolIndex}`,
|
|
4200
|
+
name: normalizedToolName,
|
|
4201
|
+
arguments: JSON.stringify(normalizedInput),
|
|
4202
|
+
});
|
|
4203
|
+
sawToolCall = true;
|
|
4204
|
+
}
|
|
4205
|
+
const normalizedToolCalls = toolCallStateToOpenAIList(toolCalls).map((call) => {
|
|
4206
|
+
if (!call || typeof call !== 'object')
|
|
4207
|
+
return call;
|
|
4208
|
+
const callObj = call;
|
|
4209
|
+
const fn = callObj.function && typeof callObj.function === 'object'
|
|
4210
|
+
? callObj.function
|
|
4211
|
+
: {};
|
|
4212
|
+
const rawToolName = typeof fn.name === 'string' ? fn.name : '';
|
|
4213
|
+
let argumentsText = '';
|
|
4214
|
+
if (typeof fn.arguments === 'string') {
|
|
4215
|
+
argumentsText = fn.arguments;
|
|
4216
|
+
}
|
|
4217
|
+
else if (fn.arguments !== undefined) {
|
|
4218
|
+
try {
|
|
4219
|
+
argumentsText = JSON.stringify(fn.arguments);
|
|
4220
|
+
}
|
|
4221
|
+
catch {
|
|
4222
|
+
argumentsText = String(fn.arguments);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
const normalizedBaseName = normalizeToolNameForRequest(rawToolName, preferredToolName, availableToolNames) || rawToolName || 'tool';
|
|
4226
|
+
const normalizedToolName = coerceToolNameByArguments(normalizedBaseName, argumentsText, availableToolNames);
|
|
4227
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(argumentsText, normalizedToolName, toolInputFallbacks);
|
|
4228
|
+
return {
|
|
4229
|
+
...callObj,
|
|
4230
|
+
function: {
|
|
4231
|
+
...fn,
|
|
4232
|
+
name: normalizedToolName,
|
|
4233
|
+
arguments: JSON.stringify(normalizedInput),
|
|
4234
|
+
},
|
|
4235
|
+
};
|
|
4236
|
+
});
|
|
4237
|
+
const hasToolCalls = normalizedToolCalls.length > 0;
|
|
4238
|
+
const finishReason = hasToolCalls || sawToolCall ? 'tool_calls' : 'stop';
|
|
4239
|
+
const assistantMessage = {
|
|
4240
|
+
role: 'assistant',
|
|
4241
|
+
content: outputText || (finishReason === 'tool_calls' ? null : ''),
|
|
4242
|
+
};
|
|
4243
|
+
if (hasToolCalls) {
|
|
4244
|
+
assistantMessage.tool_calls = normalizedToolCalls;
|
|
4245
|
+
}
|
|
1346
4246
|
res.writeHead(200, headers);
|
|
1347
4247
|
res.end(JSON.stringify({
|
|
1348
4248
|
id: completionId,
|
|
@@ -1352,11 +4252,8 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1352
4252
|
choices: [
|
|
1353
4253
|
{
|
|
1354
4254
|
index: 0,
|
|
1355
|
-
message:
|
|
1356
|
-
|
|
1357
|
-
content: outputText,
|
|
1358
|
-
},
|
|
1359
|
-
finish_reason: 'stop',
|
|
4255
|
+
message: assistantMessage,
|
|
4256
|
+
finish_reason: finishReason,
|
|
1360
4257
|
},
|
|
1361
4258
|
],
|
|
1362
4259
|
usage: {
|
|
@@ -1374,6 +4271,111 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1374
4271
|
onDelta: (text) => {
|
|
1375
4272
|
outputText += text;
|
|
1376
4273
|
},
|
|
4274
|
+
onAnthropicEvent: (eventName, eventData) => {
|
|
4275
|
+
if (eventName === 'content_block_start') {
|
|
4276
|
+
const block = eventData.content_block;
|
|
4277
|
+
const blockIndex = Number.isFinite(Number(eventData.index)) ? Number(eventData.index) : -1;
|
|
4278
|
+
if (block && typeof block === 'object') {
|
|
4279
|
+
const anyBlock = block;
|
|
4280
|
+
if (anyBlock.type === 'tool_use') {
|
|
4281
|
+
const toolIndex = blockIndex >= 0 ? allocateToolIndex(blockIndex) : nextToolIndex++;
|
|
4282
|
+
const delta = {
|
|
4283
|
+
index: toolIndex,
|
|
4284
|
+
id: typeof anyBlock.id === 'string' && anyBlock.id.trim()
|
|
4285
|
+
? anyBlock.id.trim()
|
|
4286
|
+
: `call_${completionId}_${toolIndex}`,
|
|
4287
|
+
name: normalizeToolNameForRequest(typeof anyBlock.name === 'string' && anyBlock.name.trim()
|
|
4288
|
+
? anyBlock.name.trim()
|
|
4289
|
+
: 'tool', preferredToolName, availableToolNames) || 'tool',
|
|
4290
|
+
};
|
|
4291
|
+
let argumentsText = '';
|
|
4292
|
+
if (anyBlock.input !== undefined) {
|
|
4293
|
+
try {
|
|
4294
|
+
argumentsText = JSON.stringify(anyBlock.input);
|
|
4295
|
+
}
|
|
4296
|
+
catch {
|
|
4297
|
+
argumentsText = String(anyBlock.input);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
const coercedName = coerceToolNameByArguments(delta.name || 'tool', argumentsText, availableToolNames);
|
|
4301
|
+
const normalizedInput = parseToolArgumentsAsAnthropicInput(argumentsText, coercedName, toolInputFallbacks);
|
|
4302
|
+
delta.name = coercedName;
|
|
4303
|
+
delta.arguments = JSON.stringify(normalizedInput);
|
|
4304
|
+
applyOpenAIToolCallDelta(toolCalls, delta);
|
|
4305
|
+
sawToolCall = true;
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
if (anyBlock.type === 'text' && typeof anyBlock.text === 'string' && anyBlock.text.length > 0) {
|
|
4309
|
+
outputText += anyBlock.text;
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
if (eventName === 'content_block_delta') {
|
|
4315
|
+
const delta = eventData.delta;
|
|
4316
|
+
if (!delta || typeof delta !== 'object')
|
|
4317
|
+
return;
|
|
4318
|
+
const anyDelta = delta;
|
|
4319
|
+
if ((anyDelta.type === 'text_delta' || anyDelta.type === undefined) && typeof anyDelta.text === 'string') {
|
|
4320
|
+
outputText += anyDelta.text;
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
if (anyDelta.type === 'input_json_delta' && typeof anyDelta.partial_json === 'string') {
|
|
4324
|
+
const blockIndex = Number.isFinite(Number(eventData.index)) ? Number(eventData.index) : -1;
|
|
4325
|
+
const toolIndex = blockIndex >= 0 ? allocateToolIndex(blockIndex) : nextToolIndex++;
|
|
4326
|
+
const existing = toolCalls.get(toolIndex);
|
|
4327
|
+
if (!existing) {
|
|
4328
|
+
applyOpenAIToolCallDelta(toolCalls, {
|
|
4329
|
+
index: toolIndex,
|
|
4330
|
+
id: `call_${completionId}_${toolIndex}`,
|
|
4331
|
+
name: preferredToolName || 'tool',
|
|
4332
|
+
arguments: '',
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
applyOpenAIToolCallDelta(toolCalls, { index: toolIndex, arguments: anyDelta.partial_json });
|
|
4336
|
+
sawToolCall = true;
|
|
4337
|
+
}
|
|
4338
|
+
return;
|
|
4339
|
+
}
|
|
4340
|
+
if (eventName === 'message_delta') {
|
|
4341
|
+
const delta = eventData.delta;
|
|
4342
|
+
if (delta && typeof delta === 'object') {
|
|
4343
|
+
const stopReason = delta.stop_reason;
|
|
4344
|
+
if (stopReason === 'tool_use') {
|
|
4345
|
+
sawToolCall = true;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
},
|
|
4350
|
+
onOpenAIEvent: (eventData) => {
|
|
4351
|
+
const textDelta = extractOpenAITextDelta(eventData);
|
|
4352
|
+
if (textDelta.length > 0) {
|
|
4353
|
+
outputText += textDelta;
|
|
4354
|
+
}
|
|
4355
|
+
const toolDeltas = extractOpenAIToolCallDeltas(eventData);
|
|
4356
|
+
if (toolDeltas.length > 0) {
|
|
4357
|
+
toolDeltas.forEach((delta) => {
|
|
4358
|
+
const argsText = typeof delta.arguments === 'string' ? delta.arguments : '';
|
|
4359
|
+
const normalizedBaseName = delta.name
|
|
4360
|
+
? (normalizeToolNameForRequest(delta.name, preferredToolName, availableToolNames) || delta.name)
|
|
4361
|
+
: undefined;
|
|
4362
|
+
const coercedName = normalizedBaseName
|
|
4363
|
+
? coerceToolNameByArguments(normalizedBaseName, argsText, availableToolNames)
|
|
4364
|
+
: undefined;
|
|
4365
|
+
const normalizedDelta = {
|
|
4366
|
+
...delta,
|
|
4367
|
+
...(delta.name
|
|
4368
|
+
? { name: coercedName || delta.name }
|
|
4369
|
+
: {}),
|
|
4370
|
+
};
|
|
4371
|
+
applyOpenAIToolCallDelta(toolCalls, normalizedDelta);
|
|
4372
|
+
});
|
|
4373
|
+
sawToolCall = true;
|
|
4374
|
+
}
|
|
4375
|
+
if (extractOpenAIFinishReason(eventData) === 'tool_calls') {
|
|
4376
|
+
sawToolCall = true;
|
|
4377
|
+
}
|
|
4378
|
+
},
|
|
1377
4379
|
onEnd: (outputTokens) => {
|
|
1378
4380
|
finishWithSuccess(outputTokens);
|
|
1379
4381
|
},
|
|
@@ -1383,7 +4385,80 @@ async function handleOpenAINonStreamRequest(req, res, rawRequest) {
|
|
|
1383
4385
|
});
|
|
1384
4386
|
}
|
|
1385
4387
|
// ============ 非流式兼容(走 Gateway HTTP) ============
|
|
1386
|
-
function
|
|
4388
|
+
function bridgeAnthropicNonStreamResponse(payload, requestPayload) {
|
|
4389
|
+
const preferredToolName = resolvePreferredToolNameFromRequest(requestPayload);
|
|
4390
|
+
const availableToolNames = extractToolNamesFromRequest(requestPayload);
|
|
4391
|
+
const toolInputFallbacks = deriveToolInputFallbacksFromRequest(requestPayload);
|
|
4392
|
+
const sourceContent = Array.isArray(payload.content) ? payload.content : [];
|
|
4393
|
+
const bridgedContent = [];
|
|
4394
|
+
let sawToolUse = false;
|
|
4395
|
+
const pushToolUse = (toolNameRaw, argumentsRaw, toolIdRaw) => {
|
|
4396
|
+
const normalizedBaseName = normalizeToolNameForRequest(String(toolNameRaw || '').trim(), preferredToolName, availableToolNames);
|
|
4397
|
+
let argumentsText = '';
|
|
4398
|
+
if (typeof argumentsRaw === 'string') {
|
|
4399
|
+
argumentsText = argumentsRaw;
|
|
4400
|
+
}
|
|
4401
|
+
else if (argumentsRaw !== undefined) {
|
|
4402
|
+
try {
|
|
4403
|
+
argumentsText = JSON.stringify(argumentsRaw);
|
|
4404
|
+
}
|
|
4405
|
+
catch {
|
|
4406
|
+
argumentsText = String(argumentsRaw);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
const normalizedName = coerceToolNameByArguments(normalizedBaseName, argumentsText, availableToolNames);
|
|
4410
|
+
if (!normalizedName)
|
|
4411
|
+
return;
|
|
4412
|
+
bridgedContent.push({
|
|
4413
|
+
type: 'tool_use',
|
|
4414
|
+
id: (typeof toolIdRaw === 'string' && toolIdRaw.trim())
|
|
4415
|
+
? toolIdRaw
|
|
4416
|
+
: `toolu_${Math.random().toString(36).slice(2, 12)}`,
|
|
4417
|
+
name: normalizedName,
|
|
4418
|
+
input: parseToolArgumentsAsAnthropicInput(argumentsText, normalizedName, toolInputFallbacks),
|
|
4419
|
+
});
|
|
4420
|
+
sawToolUse = true;
|
|
4421
|
+
};
|
|
4422
|
+
for (const block of sourceContent) {
|
|
4423
|
+
if (!block || typeof block !== 'object') {
|
|
4424
|
+
continue;
|
|
4425
|
+
}
|
|
4426
|
+
const blockObj = block;
|
|
4427
|
+
if (blockObj.type === 'text' && typeof blockObj.text === 'string') {
|
|
4428
|
+
const parsed = extractToolCallsFromText(blockObj.text, preferredToolName);
|
|
4429
|
+
if (parsed.cleanText.trim().length > 0) {
|
|
4430
|
+
bridgedContent.push({
|
|
4431
|
+
...blockObj,
|
|
4432
|
+
text: parsed.cleanText,
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
for (const call of parsed.calls) {
|
|
4436
|
+
pushToolUse(call.name, call.arguments);
|
|
4437
|
+
}
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
if (blockObj.type === 'tool_use') {
|
|
4441
|
+
pushToolUse(blockObj.name, blockObj.input, blockObj.id);
|
|
4442
|
+
continue;
|
|
4443
|
+
}
|
|
4444
|
+
bridgedContent.push(blockObj);
|
|
4445
|
+
}
|
|
4446
|
+
if (!sawToolUse) {
|
|
4447
|
+
return payload;
|
|
4448
|
+
}
|
|
4449
|
+
return {
|
|
4450
|
+
...payload,
|
|
4451
|
+
content: bridgedContent,
|
|
4452
|
+
stop_reason: 'tool_use',
|
|
4453
|
+
stop_sequence: null,
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
function proxyAnthropicNonStreamRequest(res, body, requestPayload, ticket) {
|
|
4457
|
+
debugBridge('anthropic_nonstream_req', {
|
|
4458
|
+
model: typeof requestPayload.model === 'string' ? requestPayload.model : null,
|
|
4459
|
+
preferredToolName: resolvePreferredToolNameFromRequest(requestPayload) || null,
|
|
4460
|
+
fallbackExecCommand: deriveToolInputFallbacksFromRequest(requestPayload).execCommand || null,
|
|
4461
|
+
});
|
|
1387
4462
|
const url = new URL(GATEWAY_URL);
|
|
1388
4463
|
const mod = url.protocol === 'https:' ? https : http;
|
|
1389
4464
|
const headers = { 'Content-Type': 'application/json', 'x-api-key': wallet.address, 'anthropic-version': '2023-06-01' };
|
|
@@ -1394,8 +4469,34 @@ function proxyRequestHTTP(req, res, body, ticket) {
|
|
|
1394
4469
|
headers['x-clawmarket-signature'] = ticket.signature;
|
|
1395
4470
|
}
|
|
1396
4471
|
const proxyReq = mod.request({ hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: '/v1/messages', method: 'POST', headers }, (proxyRes) => {
|
|
1397
|
-
|
|
1398
|
-
proxyRes.
|
|
4472
|
+
let raw = '';
|
|
4473
|
+
proxyRes.on('data', (chunk) => {
|
|
4474
|
+
raw += chunk.toString();
|
|
4475
|
+
});
|
|
4476
|
+
proxyRes.on('end', () => {
|
|
4477
|
+
const status = proxyRes.statusCode || 200;
|
|
4478
|
+
if (status >= 400) {
|
|
4479
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
4480
|
+
res.end(raw);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
try {
|
|
4484
|
+
const parsed = JSON.parse(raw);
|
|
4485
|
+
const bridged = bridgeAnthropicNonStreamResponse(parsed, requestPayload);
|
|
4486
|
+
debugBridge('anthropic_nonstream_resp', {
|
|
4487
|
+
stopReason: bridged.stop_reason ?? null,
|
|
4488
|
+
contentTypes: Array.isArray(bridged.content)
|
|
4489
|
+
? bridged.content.map((item) => (item && typeof item === 'object' ? item.type ?? null : null))
|
|
4490
|
+
: [],
|
|
4491
|
+
});
|
|
4492
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
4493
|
+
res.end(JSON.stringify(bridged));
|
|
4494
|
+
}
|
|
4495
|
+
catch {
|
|
4496
|
+
res.writeHead(status, { 'Content-Type': proxyRes.headers['content-type'] || 'application/json' });
|
|
4497
|
+
res.end(raw);
|
|
4498
|
+
}
|
|
4499
|
+
});
|
|
1399
4500
|
});
|
|
1400
4501
|
proxyReq.setTimeout(30_000, () => {
|
|
1401
4502
|
proxyReq.destroy(new Error('gateway_timeout'));
|
|
@@ -1467,12 +4568,27 @@ async function handleRequest(req, res) {
|
|
|
1467
4568
|
req.on('end', async () => {
|
|
1468
4569
|
try {
|
|
1469
4570
|
const request = JSON.parse(body);
|
|
4571
|
+
const requestRecord = request;
|
|
4572
|
+
const toolNames = extractToolNamesFromRequest(requestRecord);
|
|
4573
|
+
debugBridge('incoming_messages', {
|
|
4574
|
+
stream: request?.stream === true,
|
|
4575
|
+
model: typeof request?.model === 'string' ? request.model : null,
|
|
4576
|
+
preferredToolName: resolvePreferredToolNameFromRequest(requestRecord) || null,
|
|
4577
|
+
toolCount: toolNames.length,
|
|
4578
|
+
toolNames: toolNames.slice(0, 12),
|
|
4579
|
+
});
|
|
1470
4580
|
// 流式请求走 WebSocket
|
|
1471
4581
|
if (request.stream) {
|
|
1472
4582
|
await handleAnthropicStreamRequest(req, res, body);
|
|
1473
4583
|
return;
|
|
1474
4584
|
}
|
|
1475
4585
|
// 非流式走旧路径
|
|
4586
|
+
const rawAnthropicModel = typeof requestRecord.model === 'string' ? requestRecord.model : '';
|
|
4587
|
+
const normalizedAnthropicModel = normalizeRequestedModelId(rawAnthropicModel);
|
|
4588
|
+
if (normalizedAnthropicModel) {
|
|
4589
|
+
requestRecord.model = normalizedAnthropicModel;
|
|
4590
|
+
}
|
|
4591
|
+
const normalizedAnthropicBody = JSON.stringify(requestRecord);
|
|
1476
4592
|
const sellerAddress = defaultSeller.toLowerCase();
|
|
1477
4593
|
let channel = null;
|
|
1478
4594
|
try {
|
|
@@ -1486,7 +4602,7 @@ async function handleRequest(req, res) {
|
|
|
1486
4602
|
ticket = await signTicket(channel, COST_PER_REQUEST);
|
|
1487
4603
|
console.log(`[Ticket] #${ticket.nonce} 累计: ${formatUnits(BigInt(ticket.amount), 6)} USDC`);
|
|
1488
4604
|
}
|
|
1489
|
-
|
|
4605
|
+
proxyAnthropicNonStreamRequest(res, normalizedAnthropicBody, requestRecord, ticket);
|
|
1490
4606
|
}
|
|
1491
4607
|
catch (err) {
|
|
1492
4608
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -1655,6 +4771,7 @@ async function main() {
|
|
|
1655
4771
|
}
|
|
1656
4772
|
console.log('[路由] 静态模型 fallback:', ENABLE_STATIC_MODEL_FALLBACK ? '开启' : '关闭');
|
|
1657
4773
|
console.log('[路由] 跨模型 fallback:', ENABLE_CROSS_MODEL_FALLBACK ? '开启' : '关闭');
|
|
4774
|
+
console.log('[路由] 强制 browser open 兜底:', ENABLE_FORCED_BROWSER_OPEN ? '开启' : '关闭');
|
|
1658
4775
|
const server = http.createServer((req, res) => {
|
|
1659
4776
|
handleRequest(req, res).catch((err) => {
|
|
1660
4777
|
if (!res.headersSent) {
|