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.
@@ -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
- let channel = channels.get(key);
343
- if (channel && (channel.expiresAt < Date.now() / 1000 || channel.spent + COST_PER_REQUEST > channel.deposit)) {
344
- channels.delete(key);
345
- channel = undefined;
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
- const channel = { channelId, seller: sellerAddress, deposit: channelData[2], spent: channelData[3], nonce: 0n, expiresAt };
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
- const recovered = await recoverChannelFromChain(sellerAddress);
418
- if (recovered)
419
- return recovered;
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 startAnthropicSSE(res, messageId, requestedModel, servedModel, sellerId) {
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 endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens) {
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: 'stop',
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 text = extractTextFromOpenAIContent(msg.content).trim();
683
- if (!text)
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
- const role = msg.role === 'assistant' ? 'assistant' : 'user';
686
- normalized.push({ role, content: text });
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
- await routeStreamRequest(req, res, request, requestedModel, {
1192
- onStart: (servedModel, sellerId) => {
1193
- if (res.writableEnded)
1194
- return;
1195
- startAnthropicSSE(res, messageId, requestedModel, servedModel, sellerId);
1196
- },
1197
- onDelta: (text) => {
1198
- if (res.writableEnded)
1199
- return;
1200
- writeAnthropicDelta(res, text);
1201
- },
1202
- onEnd: (outputTokens) => {
1203
- if (res.writableEnded)
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
- endAnthropicSSE(res, outputTokens);
1206
- },
1207
- onError: (message) => {
1208
- if (res.writableEnded)
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
- failAnthropicSSE(res, message);
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 routeModel = await resolveOpenAIRouteModel(normalized.requestedModel);
1222
- const translated = routeModel !== normalized.requestedModel;
1223
- const routeRequest = {
1224
- ...normalized.requestPayload,
1225
- model: routeModel,
1226
- };
1227
- if (translated) {
1228
- console.log(`[请求] Model: ${normalized.requestedModel} -> ${routeModel}, Stream: true (openai)`);
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(/&lt;/g, '<')
2618
+ .replace(/&gt;/g, '>')
2619
+ .replace(/&amp;/g, '&')
2620
+ .replace(/&quot;/g, '"')
2621
+ .replace(/&#39;/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
- else {
1231
- console.log(`[请求] Model: ${normalized.requestedModel}, Stream: true (openai)`);
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
- const completionId = `chatcmpl_${Math.random().toString(36).slice(2, 12)}`;
1234
- const createdAt = Math.floor(Date.now() / 1000);
1235
- let servedModel = routeModel;
1236
- await routeStreamRequest(req, res, routeRequest, routeModel, {
1237
- onStart: (nextServedModel, sellerId) => {
1238
- if (res.writableEnded)
1239
- return;
1240
- servedModel = nextServedModel;
1241
- startOpenAISSE(res, completionId, createdAt, normalized.requestedModel, servedModel, sellerId);
1242
- },
1243
- onDelta: (text) => {
1244
- if (res.writableEnded)
1245
- return;
1246
- writeOpenAIDelta(res, completionId, createdAt, servedModel, text);
1247
- },
1248
- onEnd: (outputTokens) => {
1249
- if (res.writableEnded)
1250
- return;
1251
- endOpenAISSE(res, completionId, createdAt, servedModel, outputTokens);
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 sendOpenAIJsonError(res, status, message) {
1261
- if (res.writableEnded)
1262
- return;
1263
- res.writeHead(status, { 'Content-Type': 'application/json' });
1264
- res.end(JSON.stringify({
1265
- error: {
1266
- type: 'api_error',
1267
- message,
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
- role: 'assistant',
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 proxyRequestHTTP(req, res, body, ticket) {
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
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
1398
- proxyRes.pipe(res);
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
- proxyRequestHTTP(req, res, body, ticket);
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) {