@jsonstudio/llms 0.6.1892 → 0.6.2172

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.
Files changed (159) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.js +16 -2
  2. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +7 -1
  3. package/dist/conversion/compat/actions/deepseek-web-response.js +302 -40
  4. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +5 -0
  5. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +7 -4
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +1 -0
  7. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +12 -0
  8. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +1 -1
  9. package/dist/conversion/compat/actions/tool-text-request-guidance.d.ts +9 -0
  10. package/dist/conversion/compat/actions/tool-text-request-guidance.js +177 -0
  11. package/dist/conversion/compat/antigravity-session-signature.d.ts +6 -0
  12. package/dist/conversion/compat/antigravity-session-signature.js +15 -0
  13. package/dist/conversion/compat/profiles/chat-deepseek-web.json +52 -1
  14. package/dist/conversion/compat/profiles/chat-glm.json +22 -0
  15. package/dist/conversion/compat/profiles/chat-iflow.json +4 -0
  16. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +13 -27
  17. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +10 -1
  18. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +13 -4
  19. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -53
  20. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline.js +8 -4
  22. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +191 -9
  23. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +118 -15
  24. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +65 -2
  25. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +34 -0
  26. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +75 -0
  27. package/dist/conversion/hub/process/chat-process.js +85 -18
  28. package/dist/conversion/hub/response/provider-response.js +21 -50
  29. package/dist/conversion/hub/response/response-runtime.js +71 -10
  30. package/dist/conversion/responses/responses-openai-bridge/response-payload.d.ts +3 -0
  31. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +576 -0
  32. package/dist/conversion/responses/responses-openai-bridge/types.d.ts +42 -0
  33. package/dist/conversion/responses/responses-openai-bridge/types.js +1 -0
  34. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -44
  35. package/dist/conversion/responses/responses-openai-bridge.js +193 -504
  36. package/dist/conversion/shared/anthropic-message-utils.js +82 -2
  37. package/dist/conversion/shared/bridge-message-utils.js +92 -39
  38. package/dist/conversion/shared/snapshot-hooks.js +8 -13
  39. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +2 -0
  40. package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +129 -0
  41. package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +4 -0
  42. package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +637 -0
  43. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +21 -0
  44. package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +177 -0
  45. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +5 -0
  46. package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +385 -0
  47. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +10 -0
  48. package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +602 -0
  49. package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +5 -0
  50. package/dist/conversion/shared/text-markup-normalizer/extractors.js +4 -0
  51. package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -0
  52. package/dist/conversion/shared/text-markup-normalizer/normalize.js +76 -0
  53. package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -25
  54. package/dist/conversion/shared/text-markup-normalizer.js +2 -1386
  55. package/dist/conversion/shared/tool-governor.js +136 -10
  56. package/dist/filters/utils/snapshot-writer.js +3 -3
  57. package/dist/router/virtual-router/bootstrap/auth-utils.d.ts +6 -0
  58. package/dist/router/virtual-router/bootstrap/auth-utils.js +288 -0
  59. package/dist/router/virtual-router/bootstrap/claude-code-helpers.d.ts +11 -0
  60. package/dist/router/virtual-router/bootstrap/claude-code-helpers.js +18 -0
  61. package/dist/router/virtual-router/bootstrap/config-defaults.d.ts +5 -0
  62. package/dist/router/virtual-router/bootstrap/config-defaults.js +13 -0
  63. package/dist/router/virtual-router/bootstrap/config-normalizers.d.ts +4 -0
  64. package/dist/router/virtual-router/bootstrap/config-normalizers.js +106 -0
  65. package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +7 -0
  66. package/dist/router/virtual-router/bootstrap/profile-builder.js +68 -0
  67. package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +40 -0
  68. package/dist/router/virtual-router/bootstrap/provider-normalization.js +212 -0
  69. package/dist/router/virtual-router/bootstrap/responses-helpers.d.ts +15 -0
  70. package/dist/router/virtual-router/bootstrap/responses-helpers.js +65 -0
  71. package/dist/router/virtual-router/bootstrap/routing-config.d.ts +23 -0
  72. package/dist/router/virtual-router/bootstrap/routing-config.js +293 -0
  73. package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +12 -0
  74. package/dist/router/virtual-router/bootstrap/streaming-helpers.js +128 -0
  75. package/dist/router/virtual-router/bootstrap/utils.d.ts +5 -0
  76. package/dist/router/virtual-router/bootstrap/utils.js +41 -0
  77. package/dist/router/virtual-router/bootstrap/web-search-config.d.ts +4 -0
  78. package/dist/router/virtual-router/bootstrap/web-search-config.js +131 -0
  79. package/dist/router/virtual-router/bootstrap.d.ts +0 -4
  80. package/dist/router/virtual-router/bootstrap.js +31 -1275
  81. package/dist/router/virtual-router/classifier.js +32 -14
  82. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +2 -2
  83. package/dist/router/virtual-router/engine/cooldown-manager.d.ts +34 -0
  84. package/dist/router/virtual-router/engine/cooldown-manager.js +118 -0
  85. package/dist/router/virtual-router/engine/route-analytics.d.ts +28 -0
  86. package/dist/router/virtual-router/engine/route-analytics.js +44 -0
  87. package/dist/router/virtual-router/engine/routing-pools/index.js +165 -4
  88. package/dist/router/virtual-router/engine/sticky-session-manager.d.ts +29 -0
  89. package/dist/router/virtual-router/engine/sticky-session-manager.js +55 -0
  90. package/dist/router/virtual-router/engine-logging.d.ts +42 -1
  91. package/dist/router/virtual-router/engine-logging.js +82 -15
  92. package/dist/router/virtual-router/engine-selection/multimodal-capability.d.ts +3 -0
  93. package/dist/router/virtual-router/engine-selection/multimodal-capability.js +26 -0
  94. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -2
  95. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +1 -0
  96. package/dist/router/virtual-router/engine-selection/tier-selection.js +31 -1
  97. package/dist/router/virtual-router/engine.d.ts +21 -7
  98. package/dist/router/virtual-router/engine.js +198 -194
  99. package/dist/router/virtual-router/features.js +12 -4
  100. package/dist/router/virtual-router/message-utils.d.ts +8 -0
  101. package/dist/router/virtual-router/message-utils.js +170 -45
  102. package/dist/router/virtual-router/pre-command-file-resolver.js +40 -2
  103. package/dist/router/virtual-router/routing-instructions.d.ts +8 -0
  104. package/dist/router/virtual-router/routing-instructions.js +18 -2
  105. package/dist/router/virtual-router/routing-stop-message-actions.js +34 -10
  106. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +2 -0
  107. package/dist/router/virtual-router/routing-stop-message-state-codec.js +50 -1
  108. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  109. package/dist/router/virtual-router/stop-message-state-sync.js +3 -0
  110. package/dist/router/virtual-router/token-counter.js +51 -10
  111. package/dist/router/virtual-router/tool-signals.js +4 -0
  112. package/dist/router/virtual-router/types.d.ts +15 -0
  113. package/dist/servertool/clock/session-scope.d.ts +3 -0
  114. package/dist/servertool/clock/session-scope.js +52 -0
  115. package/dist/servertool/clock/state.js +9 -0
  116. package/dist/servertool/clock/tasks.js +12 -1
  117. package/dist/servertool/clock/types.d.ts +3 -0
  118. package/dist/servertool/engine.js +177 -31
  119. package/dist/servertool/handlers/clock-auto.js +2 -8
  120. package/dist/servertool/handlers/clock.js +6 -9
  121. package/dist/servertool/handlers/recursive-detection-guard.js +53 -14
  122. package/dist/servertool/handlers/stop-message-auto/blocked-report.d.ts +16 -0
  123. package/dist/servertool/handlers/stop-message-auto/blocked-report.js +349 -0
  124. package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -0
  125. package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +503 -0
  126. package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +38 -0
  127. package/dist/servertool/handlers/stop-message-auto/routing-state.js +149 -0
  128. package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +67 -0
  129. package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +387 -0
  130. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -1
  131. package/dist/servertool/handlers/stop-message-auto.js +80 -556
  132. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +18 -0
  133. package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +398 -0
  134. package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +9 -0
  135. package/dist/servertool/handlers/stop-message-stage-policy/decision.js +127 -0
  136. package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +2 -0
  137. package/dist/servertool/handlers/stop-message-stage-policy/observation.js +179 -0
  138. package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +4 -0
  139. package/dist/servertool/handlers/stop-message-stage-policy/templates.js +96 -0
  140. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +9 -0
  141. package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +89 -0
  142. package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +59 -0
  143. package/dist/servertool/handlers/stop-message-stage-policy/types.js +1 -0
  144. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +3 -43
  145. package/dist/servertool/handlers/stop-message-stage-policy.js +2 -684
  146. package/dist/servertool/handlers/web-search.js +117 -0
  147. package/dist/servertool/server-side-tools.d.ts +0 -1
  148. package/dist/servertool/server-side-tools.js +4 -3
  149. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  150. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +1 -0
  151. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +110 -37
  152. package/dist/telemetry/stats-center.d.ts +9 -0
  153. package/dist/telemetry/stats-center.js +29 -1
  154. package/dist/tools/apply-patch/structured/coercion.js +3 -11
  155. package/dist/tools/exec-command/validator.d.ts +1 -0
  156. package/dist/tools/exec-command/validator.js +132 -0
  157. package/dist/tools/tool-registry.d.ts +1 -0
  158. package/dist/tools/tool-registry.js +1 -1
  159. package/package.json +1 -1
@@ -217,6 +217,10 @@ function isIflowWebSearchEngine(engine) {
217
217
  const key = engine.providerKey.toLowerCase();
218
218
  return key.startsWith('iflow.');
219
219
  }
220
+ function isQwenWebSearchEngine(engine) {
221
+ const key = engine.providerKey.toLowerCase();
222
+ return key.startsWith('qwen.');
223
+ }
220
224
  function normalizeResultCount(value) {
221
225
  if (typeof value === 'number' && Number.isFinite(value)) {
222
226
  const normalized = Math.trunc(value);
@@ -250,6 +254,18 @@ async function executeWebSearchBackend(args) {
250
254
  hits = backendResult.hits;
251
255
  ok = backendResult.ok;
252
256
  }
257
+ else if (isQwenWebSearchEngine(engine) && options.providerInvoker) {
258
+ const backendResult = await executeQwenWebSearchViaProvider({
259
+ options,
260
+ engine,
261
+ query,
262
+ count: args.resultCount,
263
+ requestSuffix
264
+ });
265
+ summary = backendResult.summary;
266
+ hits = backendResult.hits;
267
+ ok = backendResult.ok;
268
+ }
253
269
  else if (options.reenterPipeline) {
254
270
  const payload = buildWebSearchReenterPayload(engine, query, recency, args.resultCount);
255
271
  const followup = await reenterServerToolBackend({
@@ -586,6 +602,107 @@ async function executeIflowWebSearchViaProvider(args) {
586
602
  ok
587
603
  };
588
604
  }
605
+ async function executeQwenWebSearchViaProvider(args) {
606
+ const { options, engine, query, count, requestSuffix } = args;
607
+ if (!options.providerInvoker) {
608
+ return {
609
+ summary: '',
610
+ hits: [],
611
+ ok: false
612
+ };
613
+ }
614
+ const payload = {
615
+ data: {
616
+ model: engine.id,
617
+ uq: query,
618
+ page: 1,
619
+ rows: count
620
+ },
621
+ metadata: {
622
+ entryEndpoint: '/api/v1/indices/plugin/web_search',
623
+ qwenWebSearch: true,
624
+ routeName: 'web_search'
625
+ }
626
+ };
627
+ const backend = await options.providerInvoker({
628
+ providerKey: engine.providerKey,
629
+ providerType: undefined,
630
+ modelId: undefined,
631
+ providerProtocol: options.providerProtocol,
632
+ payload,
633
+ entryEndpoint: '/api/v1/indices/plugin/web_search',
634
+ requestId: `${options.requestId}${requestSuffix}`,
635
+ routeHint: 'web_search'
636
+ });
637
+ const providerResponse = backend.providerResponse && typeof backend.providerResponse === 'object'
638
+ ? backend.providerResponse
639
+ : null;
640
+ if (!providerResponse) {
641
+ return {
642
+ summary: '',
643
+ hits: [],
644
+ ok: false
645
+ };
646
+ }
647
+ const container = providerResponse;
648
+ const status = typeof container.status === 'number' ? container.status : undefined;
649
+ const message = typeof container.message === 'string' && container.message.trim()
650
+ ? container.message.trim()
651
+ : typeof container.msg === 'string' && container.msg.trim()
652
+ ? container.msg.trim()
653
+ : '';
654
+ if (status !== undefined && status !== 0) {
655
+ throw new Error(message || `qwen web_search failed with status=${status}`);
656
+ }
657
+ const dataNode = container.data && typeof container.data === 'object' && !Array.isArray(container.data)
658
+ ? container.data
659
+ : undefined;
660
+ const rawDocs = Array.isArray(dataNode?.docs) ? dataNode?.docs : [];
661
+ const hits = [];
662
+ for (const item of rawDocs) {
663
+ if (!item || typeof item !== 'object' || Array.isArray(item))
664
+ continue;
665
+ const record = item;
666
+ const linkCandidate = typeof record.url === 'string' && record.url.trim()
667
+ ? record.url.trim()
668
+ : typeof record.link === 'string' && record.link.trim()
669
+ ? record.link.trim()
670
+ : '';
671
+ if (!linkCandidate)
672
+ continue;
673
+ const title = typeof record.title === 'string' && record.title.trim() ? record.title.trim() : undefined;
674
+ const content = typeof record.snippet === 'string' && record.snippet.trim()
675
+ ? record.snippet.trim()
676
+ : typeof record.content === 'string' && record.content.trim()
677
+ ? record.content.trim()
678
+ : undefined;
679
+ const publishDate = typeof record.timestamp_format === 'string' && record.timestamp_format.trim()
680
+ ? record.timestamp_format.trim()
681
+ : typeof record.timestamp === 'string' && record.timestamp.trim()
682
+ ? record.timestamp.trim()
683
+ : typeof record.time === 'string' && record.time.trim()
684
+ ? record.time.trim()
685
+ : undefined;
686
+ const media = typeof record.source === 'string' && record.source.trim() ? record.source.trim() : undefined;
687
+ hits.push({
688
+ title,
689
+ link: linkCandidate,
690
+ content,
691
+ publish_date: publishDate,
692
+ media
693
+ });
694
+ if (hits.length >= count) {
695
+ break;
696
+ }
697
+ }
698
+ const summary = message || (hits.length ? formatHitsSummary(hits) : '');
699
+ const ok = status === 0 || hits.length > 0;
700
+ return {
701
+ summary,
702
+ hits,
703
+ ok
704
+ };
705
+ }
589
706
  function injectWebSearchToolResult(base, toolCall, engine, query, backendResult) {
590
707
  const cloned = cloneJson(base);
591
708
  const existingOutputs = Array.isArray(cloned.tool_outputs)
@@ -8,7 +8,6 @@ import './handlers/clock.js';
8
8
  import './handlers/clock-auto.js';
9
9
  import './handlers/exec-command-guard.js';
10
10
  import './handlers/apply-patch-guard.js';
11
- import './handlers/recursive-detection-guard.js';
12
11
  import './handlers/continue-execution.js';
13
12
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
14
13
  export declare function extractToolCalls(chatResponse: JsonObject): ToolCall[];
@@ -10,7 +10,6 @@ import './handlers/clock.js';
10
10
  import './handlers/clock-auto.js';
11
11
  import './handlers/exec-command-guard.js';
12
12
  import './handlers/apply-patch-guard.js';
13
- import './handlers/recursive-detection-guard.js';
14
13
  import './handlers/continue-execution.js';
15
14
  import { runPreCommandHooks } from './pre-command-hooks.js';
16
15
  import { readRuntimeMetadata } from '../conversion/shared/runtime-metadata.js';
@@ -242,10 +241,12 @@ export async function runServerSideToolEngine(options) {
242
241
  if (!base) {
243
242
  return { mode: 'passthrough', finalChatResponse: options.chatResponse };
244
243
  }
245
- if (isClientDisconnected(options.adapterContext)) {
244
+ const toolCalls = extractToolCalls(base);
245
+ if (isClientDisconnected(options.adapterContext) && toolCalls.length > 0) {
246
+ // When client is already disconnected, skip executing explicit tool_call servertools.
247
+ // Auto hooks (e.g. stop_message_auto) still need to run to keep session state consistent.
246
248
  return { mode: 'passthrough', finalChatResponse: base };
247
249
  }
248
- const toolCalls = extractToolCalls(base);
249
250
  const contextBase = {
250
251
  base,
251
252
  toolCalls,
@@ -831,6 +831,22 @@ export class ResponsesResponseBuilder {
831
831
  }
832
832
  return { success: true, response: this.response };
833
833
  }
834
+ // Further salvage: some upstreams end the stream without any terminal events
835
+ // (no output_item.done/response.completed/response.done) but still materialize
836
+ // output_item.added + content deltas. Use current aggregated items as completed.
837
+ if (this.outputItemBuilders.size > 0) {
838
+ this.response.status = 'completed';
839
+ try {
840
+ const cur = this.response.output;
841
+ if (!Array.isArray(cur) || cur.length === 0) {
842
+ this.response.output = this.buildOutputItems();
843
+ }
844
+ }
845
+ catch {
846
+ this.response.output = this.buildOutputItems();
847
+ }
848
+ return { success: true, response: this.response };
849
+ }
834
850
  }
835
851
  catch { /* ignore */ }
836
852
  return { success: false, error: new Error('Building not completed') };
@@ -39,6 +39,7 @@ export declare class ChatSseToJsonConverter {
39
39
  * 处理chat_chunk事件
40
40
  */
41
41
  private processChatChunk;
42
+ private parseChatChunkPayload;
42
43
  private ensureChoiceBuilder;
43
44
  private tryProcessDeepSeekWebPatchEvent;
44
45
  private getDeepSeekPatchState;
@@ -110,24 +110,40 @@ export class ChatSseToJsonConverter {
110
110
  parseSseChunk(chunk) {
111
111
  const lines = chunk.trim().split('\n');
112
112
  let rawEventType;
113
- let dataValue = '';
113
+ const dataLines = [];
114
114
  for (const line of lines) {
115
115
  if (line.startsWith('event:')) {
116
116
  rawEventType = line.substring(6).trim();
117
117
  }
118
118
  else if (line.startsWith('data:')) {
119
- dataValue = line.substring(5).trim();
119
+ dataLines.push(line.substring(5).trim());
120
120
  }
121
121
  }
122
- const normalizeEventType = (candidate) => {
122
+ const dataValue = dataLines.join('\n');
123
+ const parsedData = (() => {
124
+ if (!dataValue) {
125
+ return undefined;
126
+ }
127
+ try {
128
+ const parsed = JSON.parse(dataValue);
129
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
130
+ return parsed;
131
+ }
132
+ }
133
+ catch {
134
+ // best effort
135
+ }
136
+ return undefined;
137
+ })();
138
+ const normalizeEventType = (candidate, payload) => {
123
139
  if (!candidate)
124
140
  return undefined;
125
- const v = candidate.trim();
141
+ const v = candidate.trim().toLowerCase();
126
142
  if (!v)
127
143
  return undefined;
128
144
  // OpenAI Chat Completions SSE does not include `event:` lines; we infer types elsewhere.
129
145
  // When upstream does include `event:`, accept common aliases for compatibility.
130
- if (v === 'chat_chunk' || v === 'chat_chunk'.toLowerCase())
146
+ if (v === 'chat_chunk')
131
147
  return 'chat_chunk';
132
148
  if (v === 'chat.done' || v === 'chat_done')
133
149
  return 'chat.done';
@@ -140,6 +156,16 @@ export class ChatSseToJsonConverter {
140
156
  return 'ping';
141
157
  if (v === 'finish' || v === 'close')
142
158
  return 'chat.done';
159
+ if (v === 'toast') {
160
+ const toastType = typeof payload?.type === 'string' ? payload.type.trim().toLowerCase() : '';
161
+ const finishReason = typeof payload?.finish_reason === 'string'
162
+ ? payload.finish_reason.trim().toLowerCase()
163
+ : '';
164
+ if (toastType === 'error' || finishReason === 'context_length_exceeded' || finishReason === 'rate_limit_exceeded') {
165
+ return 'error';
166
+ }
167
+ return 'ping';
168
+ }
143
169
  // Legacy aliases
144
170
  if (v === 'chunk')
145
171
  return 'chat_chunk';
@@ -147,7 +173,7 @@ export class ChatSseToJsonConverter {
147
173
  return 'chat.done';
148
174
  return undefined;
149
175
  };
150
- let eventType = normalizeEventType(rawEventType);
176
+ let eventType = normalizeEventType(rawEventType, parsedData);
151
177
  if (!eventType) {
152
178
  // OpenAI-compatible streams often omit `event:`; use `[DONE]` sentinel to mark completion.
153
179
  if (dataValue) {
@@ -240,27 +266,29 @@ export class ChatSseToJsonConverter {
240
266
  async processChatChunk(event, context) {
241
267
  try {
242
268
  const payload = typeof event.data === 'string' ? event.data : JSON.stringify(event.data ?? {});
243
- const parsed = JSON.parse(payload);
244
- if (this.tryProcessDeepSeekWebPatchEvent(parsed, context)) {
245
- return;
246
- }
247
- const chunk = parsed;
248
- // 验证chunk格式
249
- if (context.options.validateChunks) {
250
- this.validateChatChunk(chunk);
251
- }
252
- context.aggregatedChunks.push(chunk);
253
- // 初始化响应结构(如果是第一个chunk
254
- if (!context.currentResponse.id && chunk.id) {
255
- context.currentResponse.id = chunk.id;
256
- context.currentResponse.object = 'chat.completion';
257
- context.currentResponse.created = chunk.created;
258
- context.currentResponse.model = chunk.model;
259
- }
260
- // 处理choices
261
- if (chunk.choices && Array.isArray(chunk.choices)) {
262
- for (const choice of chunk.choices) {
263
- await this.processChoice(choice, context);
269
+ const parsedEntries = this.parseChatChunkPayload(payload);
270
+ for (const parsed of parsedEntries) {
271
+ if (this.tryProcessDeepSeekWebPatchEvent(parsed, context)) {
272
+ continue;
273
+ }
274
+ const chunk = parsed;
275
+ // 验证chunk格式
276
+ if (context.options.validateChunks) {
277
+ this.validateChatChunk(chunk);
278
+ }
279
+ context.aggregatedChunks.push(chunk);
280
+ // 初始化响应结构(如果是第一个chunk
281
+ if (!context.currentResponse.id && chunk.id) {
282
+ context.currentResponse.id = chunk.id;
283
+ context.currentResponse.object = 'chat.completion';
284
+ context.currentResponse.created = chunk.created;
285
+ context.currentResponse.model = chunk.model;
286
+ }
287
+ // 处理choices
288
+ if (chunk.choices && Array.isArray(chunk.choices)) {
289
+ for (const choice of chunk.choices) {
290
+ await this.processChoice(choice, context);
291
+ }
264
292
  }
265
293
  }
266
294
  }
@@ -268,6 +296,33 @@ export class ChatSseToJsonConverter {
268
296
  throw ErrorUtils.wrapError(error, 'Failed to parse chat_chunk');
269
297
  }
270
298
  }
299
+ parseChatChunkPayload(payload) {
300
+ try {
301
+ return [JSON.parse(payload)];
302
+ }
303
+ catch (primaryError) {
304
+ const segments = payload
305
+ .split('\n')
306
+ .map((entry) => entry.trim())
307
+ .filter((entry) => entry.length > 0);
308
+ if (segments.length <= 1) {
309
+ throw primaryError;
310
+ }
311
+ const parsed = [];
312
+ for (const segment of segments) {
313
+ try {
314
+ parsed.push(JSON.parse(segment));
315
+ }
316
+ catch {
317
+ // Ignore non-JSON lines so valid partial frames can still be recovered.
318
+ }
319
+ }
320
+ if (!parsed.length) {
321
+ throw primaryError;
322
+ }
323
+ return parsed;
324
+ }
325
+ }
271
326
  ensureChoiceBuilder(context, choiceIndex) {
272
327
  let choiceBuilder = context.choiceIndexMap.get(choiceIndex);
273
328
  if (!choiceBuilder) {
@@ -651,18 +706,36 @@ export class ChatSseToJsonConverter {
651
706
  * 处理error事件
652
707
  */
653
708
  async processErrorEvent(event, _context) {
709
+ const rawPayload = typeof event.data === 'string' ? event.data : JSON.stringify(event.data ?? {});
710
+ let errorData;
654
711
  try {
655
- const rawPayload = typeof event.data === 'string' ? event.data : JSON.stringify(event.data ?? {});
656
- const errorData = JSON.parse(rawPayload);
657
- const errorMessage = typeof errorData.error === 'string' ? errorData.error : 'Unknown SSE error';
658
- const code = typeof errorData.code === 'string' ? errorData.code : 'SSE_ERROR';
659
- const typedError = new Error(errorMessage);
660
- typedError.code = code;
661
- throw ErrorUtils.createError(typedError.message, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { errorData, event });
662
- }
663
- catch (parseError) {
664
- throw ErrorUtils.createError(`SSE error event: ${event.data}`, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { event });
712
+ const parsed = JSON.parse(rawPayload);
713
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
714
+ errorData = parsed;
715
+ }
665
716
  }
717
+ catch {
718
+ // keep raw payload only
719
+ }
720
+ const errorMessage = errorData
721
+ ? (typeof errorData.error === 'string'
722
+ ? errorData.error
723
+ : typeof errorData.message === 'string'
724
+ ? errorData.message
725
+ : typeof errorData.content === 'string'
726
+ ? errorData.content
727
+ : 'Unknown SSE error')
728
+ : `SSE error event: ${event.data}`;
729
+ const code = errorData
730
+ ? (typeof errorData.code === 'string'
731
+ ? errorData.code
732
+ : typeof errorData.finish_reason === 'string'
733
+ ? errorData.finish_reason
734
+ : 'SSE_ERROR')
735
+ : 'SSE_ERROR';
736
+ const typedError = new Error(errorMessage);
737
+ typedError.code = code;
738
+ throw ErrorUtils.createError(typedError.message, CHAT_CONVERSION_ERROR_CODES.STREAM_ERROR, { errorData, event });
666
739
  }
667
740
  /**
668
741
  * 构建部分响应
@@ -8,6 +8,12 @@ export interface VirtualRouterHitEvent {
8
8
  runtimeKey?: string;
9
9
  providerType?: string;
10
10
  modelId?: string;
11
+ reason?: string;
12
+ requestTokens?: number;
13
+ selectionPenalty?: number;
14
+ stopMessageActive?: boolean;
15
+ stopMessageMode?: 'on' | 'off' | 'auto' | 'unset';
16
+ stopMessageRemaining?: number;
11
17
  }
12
18
  export interface ProviderUsageEvent {
13
19
  requestId: string;
@@ -29,6 +35,9 @@ export interface RouterStatsBucket {
29
35
  poolHitCount: Record<string, number>;
30
36
  routeHitCount: Record<string, number>;
31
37
  providerHitCount: Record<string, number>;
38
+ reasonHitCount: Record<string, number>;
39
+ penaltyHitCount: Record<string, number>;
40
+ stopMessageActiveCount: number;
32
41
  }
33
42
  export interface RouterStatsSnapshot {
34
43
  global: RouterStatsBucket;
@@ -6,7 +6,10 @@ function createEmptyRouterBucket() {
6
6
  requestCount: 0,
7
7
  poolHitCount: {},
8
8
  routeHitCount: {},
9
- providerHitCount: {}
9
+ providerHitCount: {},
10
+ reasonHitCount: {},
11
+ penaltyHitCount: {},
12
+ stopMessageActiveCount: 0
10
13
  };
11
14
  }
12
15
  function createEmptyProviderBucket() {
@@ -135,6 +138,17 @@ class DefaultStatsCenter {
135
138
  if (ev.providerKey) {
136
139
  bucket.providerHitCount[ev.providerKey] = (bucket.providerHitCount[ev.providerKey] || 0) + 1;
137
140
  }
141
+ if (typeof ev.reason === 'string' && ev.reason.trim()) {
142
+ const reason = ev.reason.trim();
143
+ bucket.reasonHitCount[reason] = (bucket.reasonHitCount[reason] || 0) + 1;
144
+ }
145
+ if (typeof ev.selectionPenalty === 'number' && Number.isFinite(ev.selectionPenalty) && ev.selectionPenalty > 0) {
146
+ const key = String(Math.floor(ev.selectionPenalty));
147
+ bucket.penaltyHitCount[key] = (bucket.penaltyHitCount[key] || 0) + 1;
148
+ }
149
+ if (ev.stopMessageActive === true) {
150
+ bucket.stopMessageActiveCount += 1;
151
+ }
138
152
  }
139
153
  applyProviderUsageToBucket(bucket, ev) {
140
154
  bucket.requestCount += 1;
@@ -187,6 +201,7 @@ function printStatsToConsole(snapshot) {
187
201
  const totalRequests = router.global.requestCount;
188
202
  const poolEntries = Object.entries(router.global.poolHitCount);
189
203
  const providerEntries = Object.entries(router.global.providerHitCount);
204
+ const reasonEntries = Object.entries(router.global.reasonHitCount);
190
205
  // Router summary
191
206
  // eslint-disable-next-line no-console
192
207
  console.log('[stats] Virtual Router:');
@@ -210,6 +225,19 @@ function printStatsToConsole(snapshot) {
210
225
  console.log(` ${providerKey}: ${count}`);
211
226
  }
212
227
  }
228
+ if (reasonEntries.length) {
229
+ // eslint-disable-next-line no-console
230
+ console.log(' top reasons:');
231
+ const sortedReasons = reasonEntries.sort((a, b) => b[1] - a[1]).slice(0, 5);
232
+ for (const [reason, count] of sortedReasons) {
233
+ // eslint-disable-next-line no-console
234
+ console.log(` ${reason}: ${count}`);
235
+ }
236
+ }
237
+ if (router.global.stopMessageActiveCount > 0) {
238
+ // eslint-disable-next-line no-console
239
+ console.log(` stopMessage-active hits: ${router.global.stopMessageActiveCount}`);
240
+ }
213
241
  const globalProvider = providers.global;
214
242
  const totalProviderRequests = globalProvider.requestCount;
215
243
  const avgLatency = globalProvider.successCount > 0 ? globalProvider.latencySumMs / globalProvider.successCount : 0;
@@ -8,14 +8,7 @@ const resolveTopLevelFile = (record) => {
8
8
  asString(record.filename);
9
9
  if (direct)
10
10
  return direct;
11
- const targetAlias = asString(record.target);
12
- if (!targetAlias)
13
- return undefined;
14
- if (targetAlias.includes('\n') || targetAlias.includes('\r'))
15
- return undefined;
16
- if (!/[./\\]/.test(targetAlias))
17
- return undefined;
18
- return targetAlias;
11
+ return undefined;
19
12
  };
20
13
  const buildSingleChangePayload = (record) => {
21
14
  const kindRaw = asString(record.kind);
@@ -33,12 +26,11 @@ const buildSingleChangePayload = (record) => {
33
26
  const changeFile = asString(record.file) ??
34
27
  asString(record.path) ??
35
28
  asString(record.filepath) ??
36
- asString(record.filename) ??
37
- resolveTopLevelFile(record);
29
+ asString(record.filename);
38
30
  if (changeFile) {
39
31
  change.file = changeFile;
40
32
  }
41
- return { file: changeFile, changes: [change] };
33
+ return { ...(changeFile ? { file: changeFile } : {}), changes: [change] };
42
34
  };
43
35
  const coerceChangesArray = (value) => {
44
36
  const parsed = tryParseJson(value);
@@ -1,6 +1,7 @@
1
1
  export type ExecCommandValidationResult = {
2
2
  ok: boolean;
3
3
  reason?: string;
4
+ message?: string;
4
5
  normalizedArgs?: string;
5
6
  };
6
7
  export declare function validateExecCommandArgs(argsString: string, rawArgs: unknown): ExecCommandValidationResult;
@@ -9,6 +9,133 @@ const toJson = (value) => {
9
9
  return '{}';
10
10
  }
11
11
  };
12
+ const GIT_RESET_HARD_PATTERN = /\bgit\s+reset\s+--hard(?:\s|$)/i;
13
+ const GIT_CHECKOUT_PATTERN = /\bgit\s+checkout\b/i;
14
+ const SHELL_SEPARATORS = new Set([';', '&&', '||', '|', '&']);
15
+ function splitShellTokens(command) {
16
+ const tokens = [];
17
+ let current = '';
18
+ let quote = null;
19
+ let escaped = false;
20
+ const pushCurrent = () => {
21
+ if (current.length > 0) {
22
+ tokens.push(current);
23
+ current = '';
24
+ }
25
+ };
26
+ for (let i = 0; i < command.length; i += 1) {
27
+ const ch = command[i];
28
+ if (escaped) {
29
+ current += ch;
30
+ escaped = false;
31
+ continue;
32
+ }
33
+ if (ch === '\\') {
34
+ escaped = true;
35
+ continue;
36
+ }
37
+ if (quote) {
38
+ if (ch === quote) {
39
+ quote = null;
40
+ }
41
+ else {
42
+ current += ch;
43
+ }
44
+ continue;
45
+ }
46
+ if (ch === '"' || ch === "'") {
47
+ quote = ch;
48
+ continue;
49
+ }
50
+ if (/\s/.test(ch)) {
51
+ pushCurrent();
52
+ continue;
53
+ }
54
+ if (ch === ';') {
55
+ pushCurrent();
56
+ tokens.push(';');
57
+ continue;
58
+ }
59
+ if (ch === '|' || ch === '&') {
60
+ pushCurrent();
61
+ const next = command[i + 1];
62
+ if ((ch === '|' || ch === '&') && next === ch) {
63
+ tokens.push(ch + next);
64
+ i += 1;
65
+ }
66
+ else {
67
+ tokens.push(ch);
68
+ }
69
+ continue;
70
+ }
71
+ current += ch;
72
+ }
73
+ pushCurrent();
74
+ return tokens;
75
+ }
76
+ function evaluateGitCheckoutScope(command) {
77
+ const match = GIT_CHECKOUT_PATTERN.exec(command);
78
+ if (!match) {
79
+ return null;
80
+ }
81
+ const checkoutText = command.slice(match.index);
82
+ const tokens = splitShellTokens(checkoutText);
83
+ if (tokens.length < 3 || tokens[0]?.toLowerCase() !== 'git' || tokens[1]?.toLowerCase() !== 'checkout') {
84
+ return {
85
+ reason: 'forbidden_git_checkout_scope',
86
+ message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
87
+ };
88
+ }
89
+ const separatorIdx = tokens.findIndex((token, idx) => idx >= 2 && SHELL_SEPARATORS.has(token));
90
+ if (separatorIdx >= 0) {
91
+ return {
92
+ reason: 'forbidden_git_checkout_scope',
93
+ message: 'Command blocked: git checkout must be a standalone single-file command (no chained commands).'
94
+ };
95
+ }
96
+ const checkoutTokens = separatorIdx >= 0 ? tokens.slice(0, separatorIdx) : tokens;
97
+ const dashDashIdx = checkoutTokens.indexOf('--', 2);
98
+ if (dashDashIdx < 0) {
99
+ return {
100
+ reason: 'forbidden_git_checkout_scope',
101
+ message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
102
+ };
103
+ }
104
+ const beforeDashDash = checkoutTokens.slice(2, dashDashIdx);
105
+ if (beforeDashDash.length > 1 || beforeDashDash.some((token) => token.startsWith('-'))) {
106
+ return {
107
+ reason: 'forbidden_git_checkout_scope',
108
+ message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
109
+ };
110
+ }
111
+ const paths = checkoutTokens.slice(dashDashIdx + 1);
112
+ if (paths.length !== 1) {
113
+ return {
114
+ reason: 'forbidden_git_checkout_scope',
115
+ message: 'Command blocked: git checkout is restricted to a single file path. Use `git checkout -- <file>` or `git checkout <ref> -- <file>`.'
116
+ };
117
+ }
118
+ const path = paths[0];
119
+ if (!path || path === '.' || path === '/' || path === '*' || path.endsWith('/')) {
120
+ return {
121
+ reason: 'forbidden_git_checkout_scope',
122
+ message: 'Command blocked: git checkout is restricted to one concrete file path (directory/pathset restore is not allowed).'
123
+ };
124
+ }
125
+ return null;
126
+ }
127
+ function detectPolicyViolation(command) {
128
+ if (!command || !command.trim()) {
129
+ return null;
130
+ }
131
+ if (GIT_RESET_HARD_PATTERN.test(command)) {
132
+ return {
133
+ reason: 'forbidden_git_reset_hard',
134
+ message: 'Command blocked: `git reset --hard` is destructive. Use `git reset --mixed <ref>` or file-scoped restore commands instead.'
135
+ };
136
+ }
137
+ return evaluateGitCheckoutScope(command);
138
+ }
12
139
  export function validateExecCommandArgs(argsString, rawArgs) {
13
140
  const raw = typeof argsString === 'string' ? argsString : String(argsString ?? '');
14
141
  const parsed = isRecord(rawArgs) && Object.keys(rawArgs).length > 0
@@ -18,5 +145,10 @@ export function validateExecCommandArgs(argsString, rawArgs) {
18
145
  if (normalized.ok === false) {
19
146
  return { ok: false, reason: normalized.reason };
20
147
  }
148
+ const command = typeof normalized.normalized.cmd === 'string' ? normalized.normalized.cmd : '';
149
+ const violation = detectPolicyViolation(command);
150
+ if (violation) {
151
+ return { ok: false, reason: violation.reason, message: violation.message };
152
+ }
21
153
  return { ok: true, normalizedArgs: toJson(normalized.normalized) };
22
154
  }