@jsonstudio/llms 0.6.1643 → 0.6.1739

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 (96) hide show
  1. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.d.ts +10 -0
  2. package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +121 -0
  3. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.d.ts +10 -0
  4. package/dist/conversion/compat/actions/iflow-kimi-cli-defaults.js +80 -0
  5. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.d.ts +7 -0
  6. package/dist/conversion/compat/actions/iflow-kimi-history-media-placeholder.js +161 -0
  7. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.d.ts +12 -0
  8. package/dist/conversion/compat/actions/iflow-kimi-thinking-reasoning-fill.js +67 -0
  9. package/dist/conversion/compat/actions/iflow-response-body-unwrap.d.ts +9 -0
  10. package/dist/conversion/compat/actions/iflow-response-body-unwrap.js +140 -0
  11. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.d.ts +10 -0
  12. package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +59 -0
  13. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.d.ts +14 -0
  14. package/dist/conversion/compat/actions/lmstudio-responses-input-stringify.js +125 -0
  15. package/dist/conversion/compat/actions/normalize-tool-call-ids.d.ts +11 -0
  16. package/dist/conversion/compat/actions/normalize-tool-call-ids.js +140 -0
  17. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.d.ts +2 -0
  18. package/dist/conversion/compat/actions/strip-orphan-function-calls-tag.js +152 -0
  19. package/dist/conversion/compat/antigravity-session-signature.d.ts +1 -1
  20. package/dist/conversion/compat/antigravity-session-signature.js +5 -4
  21. package/dist/conversion/compat/profiles/chat-iflow.json +6 -0
  22. package/dist/conversion/compat/profiles/chat-lmstudio.json +7 -1
  23. package/dist/conversion/hub/operation-table/operation-table-runner.js +1 -1
  24. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +19 -2
  25. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +101 -5
  26. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.d.ts +2 -0
  27. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +63 -0
  28. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +18 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline.js +1 -1
  30. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +8 -5
  31. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +5 -1
  32. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +113 -0
  33. package/dist/conversion/hub/pipeline/target-utils.js +3 -0
  34. package/dist/conversion/hub/response/provider-response.js +27 -1
  35. package/dist/conversion/responses/responses-openai-bridge.js +32 -6
  36. package/dist/conversion/shared/anthropic-message-utils.js +20 -5
  37. package/dist/conversion/shared/bridge-id-utils.d.ts +2 -0
  38. package/dist/conversion/shared/bridge-id-utils.js +52 -15
  39. package/dist/conversion/shared/responses-conversation-store.js +40 -5
  40. package/dist/conversion/shared/responses-output-builder.js +23 -7
  41. package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
  42. package/dist/conversion/shared/responses-tool-utils.js +30 -13
  43. package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
  44. package/dist/conversion/shared/text-markup-normalizer.js +269 -1
  45. package/dist/router/virtual-router/bootstrap.js +31 -7
  46. package/dist/router/virtual-router/classifier.js +1 -1
  47. package/dist/router/virtual-router/engine/antigravity/alias-lease.d.ts +33 -0
  48. package/dist/router/virtual-router/engine/antigravity/alias-lease.js +247 -0
  49. package/dist/router/virtual-router/engine/health/index.d.ts +23 -0
  50. package/dist/router/virtual-router/engine/health/index.js +720 -0
  51. package/dist/router/virtual-router/engine/provider-key/parse.d.ts +6 -0
  52. package/dist/router/virtual-router/engine/provider-key/parse.js +43 -0
  53. package/dist/router/virtual-router/engine/routing-pools/index.d.ts +13 -0
  54. package/dist/router/virtual-router/engine/routing-pools/index.js +225 -0
  55. package/dist/router/virtual-router/engine/routing-state/keys.d.ts +3 -0
  56. package/dist/router/virtual-router/engine/routing-state/keys.js +30 -0
  57. package/dist/router/virtual-router/engine/routing-state/metadata.d.ts +6 -0
  58. package/dist/router/virtual-router/engine/routing-state/metadata.js +132 -0
  59. package/dist/router/virtual-router/engine/routing-state/store.d.ts +11 -0
  60. package/dist/router/virtual-router/engine/routing-state/store.js +107 -0
  61. package/dist/router/virtual-router/engine-health.d.ts +1 -23
  62. package/dist/router/virtual-router/engine-health.js +1 -720
  63. package/dist/router/virtual-router/engine-selection/route-utils.js +57 -0
  64. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +8 -48
  65. package/dist/router/virtual-router/engine-selection/tier-selection.js +34 -17
  66. package/dist/router/virtual-router/engine-selection.d.ts +1 -13
  67. package/dist/router/virtual-router/engine-selection.js +1 -225
  68. package/dist/router/virtual-router/engine.d.ts +2 -23
  69. package/dist/router/virtual-router/engine.js +130 -603
  70. package/dist/router/virtual-router/message-utils.js +15 -5
  71. package/dist/servertool/engine.js +4 -4
  72. package/dist/servertool/handlers/followup-request-builder.js +46 -0
  73. package/dist/servertool/handlers/gemini-empty-reply-continue.js +48 -47
  74. package/dist/servertool/handlers/stop-message-auto.js +64 -7
  75. package/dist/servertool/handlers/vision.js +10 -0
  76. package/dist/servertool/types.d.ts +3 -0
  77. package/dist/sse/sse-to-json/builders/response-builder.js +6 -0
  78. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +32 -2
  79. package/dist/sse/sse-to-json/parsers/sse-parser.js +34 -0
  80. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -0
  81. package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +33 -1
  82. package/dist/tools/apply-patch/args-normalizer/default-actions.d.ts +2 -0
  83. package/dist/tools/apply-patch/args-normalizer/default-actions.js +12 -0
  84. package/dist/tools/apply-patch/args-normalizer/extract-patch.d.ts +2 -0
  85. package/dist/tools/apply-patch/args-normalizer/extract-patch.js +15 -0
  86. package/dist/tools/apply-patch/args-normalizer/index.d.ts +2 -0
  87. package/dist/tools/apply-patch/args-normalizer/index.js +164 -0
  88. package/dist/tools/apply-patch/args-normalizer/structured-builders.d.ts +7 -0
  89. package/dist/tools/apply-patch/args-normalizer/structured-builders.js +85 -0
  90. package/dist/tools/apply-patch/args-normalizer/types.d.ts +54 -0
  91. package/dist/tools/apply-patch/args-normalizer/types.js +1 -0
  92. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +1 -0
  93. package/dist/tools/apply-patch/patch-text/normalize.js +104 -5
  94. package/dist/tools/apply-patch/structured/coercion.js +28 -4
  95. package/dist/tools/apply-patch/validator.js +7 -146
  96. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import { createHash } from 'node:crypto';
2
+ const MAX_RESPONSES_ITEM_ID_LENGTH = 64;
1
3
  function sanitizeCore(value) {
2
4
  return value
3
5
  .replace(/[^A-Za-z0-9_-]/g, '_')
@@ -5,7 +7,26 @@ function sanitizeCore(value) {
5
7
  .replace(/^_+/, '')
6
8
  .replace(/_+$/, '');
7
9
  }
8
- function stripPrefix(value) {
10
+ function shortHash(value) {
11
+ try {
12
+ return createHash('sha1').update(value).digest('hex').slice(0, 10);
13
+ }
14
+ catch {
15
+ return Math.random().toString(36).slice(2, 12);
16
+ }
17
+ }
18
+ function clampPrefixedId(prefix, core, hashSource) {
19
+ const sanitized = sanitizeCore(core) || Math.random().toString(36).slice(2, 10);
20
+ const direct = `${prefix}${sanitized}`;
21
+ if (direct.length <= MAX_RESPONSES_ITEM_ID_LENGTH) {
22
+ return direct;
23
+ }
24
+ const hash = shortHash(`${prefix}|${hashSource}|${sanitized}`);
25
+ const room = Math.max(1, MAX_RESPONSES_ITEM_ID_LENGTH - prefix.length - 1 - hash.length);
26
+ const head = sanitizeCore(sanitized.slice(0, room)) || 'id';
27
+ return `${prefix}${head}_${hash}`;
28
+ }
29
+ function extractCore(value) {
9
30
  if (typeof value !== 'string')
10
31
  return null;
11
32
  const trimmed = value.trim();
@@ -20,23 +41,39 @@ function stripPrefix(value) {
20
41
  else if (/^call[_-]/i.test(sanitized)) {
21
42
  sanitized = sanitized.replace(/^call[_-]?/i, '');
22
43
  }
23
- if (!sanitized) {
24
- sanitized = Math.random().toString(36).slice(2, 10);
25
- }
26
- return `fc_${sanitized}`;
44
+ return sanitizeCore(sanitized) || null;
27
45
  }
28
- function normalizeWithFallback(options) {
29
- const normalized = stripPrefix(options.callId);
30
- if (normalized)
31
- return normalized;
32
- const fallbackNormalized = stripPrefix(options.fallback);
33
- if (fallbackNormalized)
34
- return fallbackNormalized;
35
- return `fc_${Math.random().toString(36).slice(2, 10)}`;
46
+ function normalizeWithFallback(options, prefix) {
47
+ const callCore = extractCore(options.callId);
48
+ if (callCore) {
49
+ return clampPrefixedId(prefix, callCore, String(options.callId));
50
+ }
51
+ const fallbackCore = extractCore(options.fallback);
52
+ if (fallbackCore) {
53
+ return clampPrefixedId(prefix, fallbackCore, String(options.fallback));
54
+ }
55
+ const randomCore = Math.random().toString(36).slice(2, 10);
56
+ return clampPrefixedId(prefix, randomCore, randomCore);
36
57
  }
37
58
  export function normalizeFunctionCallId(options) {
38
- return normalizeWithFallback(options);
59
+ return normalizeWithFallback(options, 'fc_');
39
60
  }
40
61
  export function normalizeFunctionCallOutputId(options) {
41
- return normalizeWithFallback(options);
62
+ return normalizeWithFallback(options, 'fc_');
63
+ }
64
+ export function normalizeResponsesCallId(options) {
65
+ return normalizeWithFallback(options, 'call_');
66
+ }
67
+ export function clampResponsesInputItemId(raw) {
68
+ const trimmed = typeof raw === 'string' ? raw.trim() : '';
69
+ if (!trimmed) {
70
+ return undefined;
71
+ }
72
+ if (trimmed.length <= MAX_RESPONSES_ITEM_ID_LENGTH) {
73
+ return trimmed;
74
+ }
75
+ const hash = shortHash(trimmed);
76
+ const room = Math.max(1, MAX_RESPONSES_ITEM_ID_LENGTH - 1 - hash.length);
77
+ const head = trimmed.slice(0, room);
78
+ return `${head}_${hash}`;
42
79
  }
@@ -1,4 +1,5 @@
1
1
  import { ProviderProtocolError } from './errors.js';
2
+ import { normalizeFunctionCallOutputId } from './bridge-id-utils.js';
2
3
  const TTL_MS = 1000 * 60 * 30; // 30min
3
4
  function cloneDeep(value) {
4
5
  try {
@@ -111,7 +112,7 @@ function convertOutputToInputItems(response) {
111
112
  }
112
113
  return items;
113
114
  }
114
- function normalizeSubmittedToolOutputs(toolOutputs) {
115
+ function normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId) {
115
116
  const items = [];
116
117
  const submitted = [];
117
118
  toolOutputs.forEach((entry, index) => {
@@ -126,7 +127,17 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
126
127
  ? rec.id
127
128
  : undefined;
128
129
  const trimmed = typeof rawId === 'string' ? rawId.trim() : '';
129
- const callId = trimmed.length ? trimmed : `fc_resume_${index}`;
130
+ const callId = trimmed.length ? trimmed : `call_resume_${index}`;
131
+ const mappedItemId = callIdToFunctionItemId && typeof callId === 'string' && callIdToFunctionItemId.has(callId)
132
+ ? callIdToFunctionItemId.get(callId)
133
+ : undefined;
134
+ const mappedIdTrimmed = typeof mappedItemId === 'string' ? mappedItemId.trim() : '';
135
+ const outputId = mappedIdTrimmed.length
136
+ ? normalizeFunctionCallOutputId({ callId: mappedIdTrimmed, fallback: mappedIdTrimmed })
137
+ : normalizeFunctionCallOutputId({
138
+ callId,
139
+ fallback: trimmed.length ? trimmed : `fc_resume_${index}`
140
+ });
130
141
  const outputValue = rec.output ?? null;
131
142
  const normalizedOutput = typeof outputValue === 'string'
132
143
  ? outputValue
@@ -140,7 +151,7 @@ function normalizeSubmittedToolOutputs(toolOutputs) {
140
151
  })();
141
152
  items.push({
142
153
  type: 'function_call_output',
143
- id: callId,
154
+ id: outputId,
144
155
  call_id: callId,
145
156
  output: normalizedOutput
146
157
  });
@@ -244,11 +255,35 @@ class ResponsesConversationStore {
244
255
  });
245
256
  }
246
257
  const mergedInput = coerceInputArray(entry.input);
247
- const normalizedOutputs = normalizeSubmittedToolOutputs(toolOutputs);
258
+ const callIdToFunctionItemId = new Map();
259
+ for (const item of mergedInput) {
260
+ if (!item || typeof item !== 'object')
261
+ continue;
262
+ const type = typeof item.type === 'string' ? String(item.type) : '';
263
+ if (type !== 'function_call')
264
+ continue;
265
+ const id = typeof item.id === 'string' ? String(item.id).trim() : '';
266
+ const callId = typeof item.call_id === 'string' ? String(item.call_id).trim() : '';
267
+ if (id) {
268
+ callIdToFunctionItemId.set(id, id);
269
+ }
270
+ if (callId) {
271
+ callIdToFunctionItemId.set(callId, id || callId);
272
+ }
273
+ }
274
+ const normalizedOutputs = normalizeSubmittedToolOutputs(toolOutputs, callIdToFunctionItemId);
248
275
  mergedInput.push(...normalizedOutputs.items);
249
276
  const payload = cloneDeep(entry.basePayload);
250
277
  payload.input = mergedInput;
251
- payload.stream = true;
278
+ // Preserve the caller's streaming intent for the resume request.
279
+ // Do not force-enable streaming here: some upstreams return JSON even when we send `Accept: text/event-stream`,
280
+ // and forcing `stream=true` can trip SSE decoders on non-SSE bodies.
281
+ payload.stream =
282
+ typeof submitPayload.stream === 'boolean'
283
+ ? Boolean(submitPayload.stream)
284
+ : typeof entry.basePayload.stream === 'boolean'
285
+ ? Boolean(entry.basePayload.stream)
286
+ : false;
252
287
  payload.previous_response_id = responseId;
253
288
  if (Array.isArray(entry.tools) && entry.tools.length) {
254
289
  payload.tools = cloneDeep(entry.tools);
@@ -1,4 +1,4 @@
1
- import { normalizeFunctionCallId } from './bridge-id-utils.js';
1
+ import { normalizeFunctionCallId, normalizeFunctionCallOutputId } from './bridge-id-utils.js';
2
2
  import { normalizeContentPart } from './output-content-normalizer.js';
3
3
  import { expandResponsesMessageItem } from '../../sse/shared/responses-output-normalizer.js';
4
4
  function buildToolOutputIndex(response) {
@@ -134,10 +134,18 @@ export function buildResponsesOutputFromChat(options) {
134
134
  const pendingToolCalls = normalizedToolCalls.filter((entry) => !executedIds.has(entry.id));
135
135
  const hasNormalizedToolCalls = pendingToolCalls.length > 0;
136
136
  if (hasNormalizedToolCalls) {
137
+ const pendingIds = new Set(pendingToolCalls.map((entry) => entry.id));
137
138
  for (const item of outputItems) {
138
- if (item.type === 'message') {
139
+ const type = item.type;
140
+ if (type === 'message') {
139
141
  item.status = 'in_progress';
140
142
  }
143
+ if (type === 'function_call') {
144
+ const callId = item.call_id;
145
+ if (typeof callId === 'string' && pendingIds.has(callId)) {
146
+ item.status = 'in_progress';
147
+ }
148
+ }
141
149
  }
142
150
  }
143
151
  const requiredAction = hasNormalizedToolCalls
@@ -221,14 +229,22 @@ function buildFunctionCallOutput(call, allocateOutputId, sanitizeFunctionName, b
221
229
  const originalCallId = typeof call.id === 'string' && call.id.trim().length
222
230
  ? String(call.id)
223
231
  : (typeof call.call_id === 'string' && call.call_id.trim().length ? String(call.call_id) : undefined);
224
- const callId = normalizeFunctionCallId({
225
- callId: originalCallId,
226
- fallback: `fc_call_${baseCount + offset}`
232
+ // Preserve original tool call IDs when present (OpenAI Responses contract expects stable call_id
233
+ // across tool_call → submit_tool_outputs). Only generate/normalize when missing.
234
+ const callId = typeof originalCallId === 'string' && originalCallId.trim().length
235
+ ? originalCallId.trim()
236
+ : normalizeFunctionCallId({
237
+ callId: originalCallId,
238
+ fallback: `fc_call_${baseCount + offset}`
239
+ });
240
+ const outputId = normalizeFunctionCallOutputId({
241
+ callId,
242
+ fallback: allocateOutputId('fc')
227
243
  });
228
244
  const output = {
229
- id: allocateOutputId('function_call'),
245
+ id: outputId,
230
246
  type: 'function_call',
231
- status: 'in_progress',
247
+ status: 'completed',
232
248
  name: sanitized,
233
249
  call_id: callId,
234
250
  arguments: argsStr
@@ -2,6 +2,7 @@ export type ToolCallIdStyle = 'preserve' | 'fc';
2
2
  type BridgeInputItem = Record<string, unknown>;
3
3
  export interface CallIdTransformer {
4
4
  normalizeCallId(raw: unknown): string;
5
+ normalizeItemId(raw: unknown, callId: string): string;
5
6
  normalizeOutputId(callId: string, raw: unknown): string;
6
7
  }
7
8
  export declare function createToolCallIdTransformer(style: ToolCallIdStyle): CallIdTransformer | null;
@@ -1,25 +1,33 @@
1
- import { normalizeFunctionCallId, normalizeFunctionCallOutputId } from './bridge-id-utils.js';
1
+ import { normalizeFunctionCallId, normalizeFunctionCallOutputId, normalizeResponsesCallId } from './bridge-id-utils.js';
2
2
  export function createToolCallIdTransformer(style) {
3
3
  if (style !== 'fc') {
4
4
  return null;
5
5
  }
6
6
  let callCounter = 0;
7
+ let itemCounter = 0;
7
8
  let outputCounter = 0;
8
- const alias = new Map();
9
+ const callAlias = new Map();
9
10
  const normalizeCallId = (raw) => {
10
11
  const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
11
- if (rawStr && alias.has(rawStr)) {
12
- return alias.get(rawStr);
12
+ if (rawStr && callAlias.has(rawStr)) {
13
+ return callAlias.get(rawStr);
13
14
  }
14
- const normalized = normalizeFunctionCallId({
15
+ const normalized = normalizeResponsesCallId({
15
16
  callId: rawStr,
16
- fallback: `fc_call_${++callCounter}`
17
+ fallback: `call_${++callCounter}`
17
18
  });
18
19
  if (rawStr) {
19
- alias.set(rawStr, normalized);
20
+ callAlias.set(rawStr, normalized);
20
21
  }
21
22
  return normalized;
22
23
  };
24
+ const normalizeItemId = (raw, callId) => {
25
+ const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
26
+ return normalizeFunctionCallId({
27
+ callId: rawStr ?? callId,
28
+ fallback: `fc_item_${++itemCounter}`
29
+ });
30
+ };
23
31
  const normalizeOutputId = (callId, raw) => {
24
32
  const rawStr = typeof raw === 'string' && raw.trim().length ? raw.trim() : undefined;
25
33
  return normalizeFunctionCallOutputId({
@@ -29,6 +37,7 @@ export function createToolCallIdTransformer(style) {
29
37
  };
30
38
  return {
31
39
  normalizeCallId,
40
+ normalizeItemId,
32
41
  normalizeOutputId
33
42
  };
34
43
  }
@@ -38,19 +47,27 @@ export function enforceToolCallIdStyle(input, transformer) {
38
47
  continue;
39
48
  const type = typeof entry.type === 'string' ? entry.type.toLowerCase() : '';
40
49
  if (type === 'function_call') {
41
- const normalized = transformer.normalizeCallId(entry.call_id ?? entry.id);
42
- entry.call_id = normalized;
43
- entry.id = normalized;
50
+ const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.id);
51
+ entry.call_id = normalizedCallId;
52
+ entry.id = transformer.normalizeItemId(entry.id ?? normalizedCallId, normalizedCallId);
44
53
  continue;
45
54
  }
46
55
  if (type === 'function_call_output' || type === 'tool_result' || type === 'tool_message') {
47
- const normalized = transformer.normalizeCallId(entry.call_id ?? entry.id);
48
- entry.call_id = normalized;
49
- entry.id = transformer.normalizeOutputId(normalized, entry.id);
56
+ const normalizedCallId = transformer.normalizeCallId(entry.call_id ?? entry.tool_call_id ?? entry.id);
57
+ entry.call_id = normalizedCallId;
58
+ // OpenAI `/v1/responses` request schema uses `call_id` for tool outputs.
59
+ // Some internal carriers may include `tool_call_id`; strip it before sending upstream
60
+ // to avoid strict schema errors (e.g. "Unknown parameter: input[N].tool_call_id").
61
+ if (entry.tool_call_id !== undefined) {
62
+ delete entry.tool_call_id;
63
+ }
64
+ entry.id = transformer.normalizeOutputId(normalizedCallId, entry.id);
50
65
  }
51
66
  }
52
67
  }
53
68
  export function resolveToolCallIdStyle(metadata) {
69
+ // Standard OpenAI `/v1/responses` requires function_call item ids to start with `fc_`.
70
+ // Default to `fc` unless explicitly overridden (e.g. LM Studio compat).
54
71
  if (!metadata)
55
72
  return 'fc';
56
73
  const raw = metadata.toolCallIdStyle;
@@ -21,4 +21,5 @@ export declare function extractXMLToolCallsFromText(text: string): ToolCallLite[
21
21
  * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
22
22
  */
23
23
  export declare function extractSimpleXmlToolsFromText(text: string): ToolCallLite[] | null;
24
+ export declare function extractQwenToolCallTokensFromText(text: string): ToolCallLite[] | null;
24
25
  export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>): Record<string, any>;
@@ -117,6 +117,151 @@ function enabled() {
117
117
  return true;
118
118
  }
119
119
  }
120
+ function escapeControlCharsInsideJsonStrings(raw) {
121
+ try {
122
+ if (typeof raw !== 'string' || !raw)
123
+ return null;
124
+ let out = '';
125
+ let changed = false;
126
+ let inString = false;
127
+ let escaped = false;
128
+ for (let i = 0; i < raw.length; i++) {
129
+ const ch = raw[i];
130
+ if (inString) {
131
+ if (!escaped && ch === '"') {
132
+ inString = false;
133
+ out += ch;
134
+ continue;
135
+ }
136
+ if (!escaped && ch === '\\') {
137
+ escaped = true;
138
+ out += ch;
139
+ continue;
140
+ }
141
+ // JSON disallows unescaped control chars inside strings; models sometimes emit raw newlines/tabs.
142
+ if (!escaped) {
143
+ if (ch === '\n') {
144
+ out += '\\n';
145
+ changed = true;
146
+ continue;
147
+ }
148
+ if (ch === '\r') {
149
+ if (raw[i + 1] === '\n') {
150
+ i++;
151
+ }
152
+ out += '\\n';
153
+ changed = true;
154
+ continue;
155
+ }
156
+ if (ch === '\t') {
157
+ out += '\\t';
158
+ changed = true;
159
+ continue;
160
+ }
161
+ const code = ch.charCodeAt(0);
162
+ if (code >= 0 && code < 0x20) {
163
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
164
+ changed = true;
165
+ continue;
166
+ }
167
+ }
168
+ // fallthrough: normal character inside string
169
+ out += ch;
170
+ escaped = false;
171
+ continue;
172
+ }
173
+ if (ch === '"') {
174
+ inString = true;
175
+ out += ch;
176
+ continue;
177
+ }
178
+ out += ch;
179
+ }
180
+ return changed ? out : null;
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
186
+ function tryParseJsonWithModelRepairs(raw) {
187
+ try {
188
+ if (typeof raw !== 'string')
189
+ return null;
190
+ const text = raw.trim();
191
+ if (!text)
192
+ return null;
193
+ try {
194
+ return JSON.parse(text);
195
+ }
196
+ catch {
197
+ const repaired = escapeControlCharsInsideJsonStrings(text);
198
+ if (!repaired)
199
+ return null;
200
+ try {
201
+ return JSON.parse(repaired);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ }
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }
212
+ function salvageToolArgsFromRawText(toolName, rawArgs) {
213
+ try {
214
+ const lname = String(toolName || '').toLowerCase();
215
+ const text = String(rawArgs || '');
216
+ const out = {};
217
+ const pickString = (re) => {
218
+ const m = text.match(re);
219
+ if (!m)
220
+ return undefined;
221
+ return String(m[1] ?? '');
222
+ };
223
+ const pickNumber = (re) => {
224
+ const m = text.match(re);
225
+ if (!m)
226
+ return undefined;
227
+ const n = Number.parseInt(String(m[1] ?? ''), 10);
228
+ return Number.isFinite(n) ? n : undefined;
229
+ };
230
+ if (lname === 'exec_command') {
231
+ const cmd = pickString(/"(?:cmd|command)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
232
+ const workdir = pickString(/"(?:workdir|cwd)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
233
+ const timeout = pickNumber(/"(?:timeout_ms|timeout)"\s*:\s*(\d+)\s*(?:,|})/i);
234
+ if (cmd !== undefined)
235
+ out.cmd = cmd;
236
+ if (cmd !== undefined)
237
+ out.command = cmd;
238
+ if (workdir !== undefined)
239
+ out.workdir = workdir;
240
+ if (timeout !== undefined)
241
+ out.timeout_ms = timeout;
242
+ return Object.keys(out).length ? out : null;
243
+ }
244
+ if (lname === 'write_stdin') {
245
+ const sessionId = pickNumber(/"(?:session_id|sessionId)"\s*:\s*(\d+)\s*(?:,|})/i);
246
+ const chars = pickString(/"(?:chars|text|input|data)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
247
+ if (sessionId !== undefined)
248
+ out.session_id = sessionId;
249
+ if (chars !== undefined)
250
+ out.chars = chars;
251
+ return Object.keys(out).length ? out : null;
252
+ }
253
+ if (lname === 'apply_patch') {
254
+ const patch = pickString(/"(?:patch|text|input|instructions)"\s*:\s*"([\s\S]*?)"\s*(?:,|})/i);
255
+ if (patch !== undefined)
256
+ out.patch = patch;
257
+ return Object.keys(out).length ? out : null;
258
+ }
259
+ return null;
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ }
120
265
  // 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
121
266
  // Extract <tool:NAME>...</tool:NAME> blocks used by some tool-mode outputs.
122
267
  // Example:
@@ -1054,6 +1199,128 @@ export function extractSimpleXmlToolsFromText(text) {
1054
1199
  return null;
1055
1200
  }
1056
1201
  }
1202
+ // Extract Qwen-style tool call tokens that appear in plain text, e.g.:
1203
+ // <|tool_calls_section_begin|>
1204
+ // <|tool_call_begin|> functions.exec_command:66 <|tool_call_argument_begin|> {"cmd":"pwd"} <|tool_call_end|>
1205
+ // <|tool_calls_section_end|>
1206
+ // Some upstreams emit this even when OpenAI tool_calls are supported; we harvest it into tool_calls.
1207
+ export function extractQwenToolCallTokensFromText(text) {
1208
+ try {
1209
+ if (typeof text !== 'string' || !text)
1210
+ return null;
1211
+ // Upstreams (notably Qwen-family models behind OpenAI-compatible shells) may insert
1212
+ // whitespace/newlines inside token markers, e.g. "<| tool_call_begin|>".
1213
+ // Avoid brittle substring checks; do a cheap regex presence test instead.
1214
+ if (!/<\|\s*tool_call_begin\s*\|>/i.test(text) && !/<\|\s*tool_call_argument_begin\s*\|>/i.test(text)) {
1215
+ return null;
1216
+ }
1217
+ const out = [];
1218
+ // tolerate whitespace/newlines and missing argument_end token
1219
+ const re = /<\|\s*tool_call_begin\s*\|>\s*([A-Za-z0-9_.-]+)(?::(\d+))?\s*(?:<\|\s*tool_call_argument_begin\s*\|>\s*([\s\S]*?)\s*)?<\|\s*tool_call_end\s*\|>/gi;
1220
+ let m;
1221
+ while ((m = re.exec(text)) !== null) {
1222
+ const rawName = String(m[1] || '').trim();
1223
+ const numericId = typeof m[2] === 'string' && m[2].trim().length ? m[2].trim() : undefined;
1224
+ const rawArgs = typeof m[3] === 'string' ? String(m[3]).trim() : '';
1225
+ if (!rawName)
1226
+ continue;
1227
+ let name = rawName;
1228
+ if (name.startsWith('functions.')) {
1229
+ name = name.slice('functions.'.length);
1230
+ }
1231
+ const lname = name.toLowerCase();
1232
+ if (!lname)
1233
+ continue;
1234
+ let parsedArgs = null;
1235
+ if (!rawArgs) {
1236
+ parsedArgs = {};
1237
+ }
1238
+ else {
1239
+ parsedArgs = tryParseJsonWithModelRepairs(rawArgs);
1240
+ if (parsedArgs === null) {
1241
+ parsedArgs = salvageToolArgsFromRawText(lname, rawArgs) ?? { _raw: rawArgs };
1242
+ }
1243
+ }
1244
+ const argsObj = parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs)
1245
+ ? parsedArgs
1246
+ : { value: parsedArgs };
1247
+ // Alias normalization (models often use "command" instead of "cmd", etc.)
1248
+ if (lname === 'exec_command') {
1249
+ const cmd = typeof argsObj.cmd === 'string' ? String(argsObj.cmd) : '';
1250
+ const command = typeof argsObj.command === 'string' ? String(argsObj.command) : '';
1251
+ if (!cmd && command) {
1252
+ argsObj.cmd = command;
1253
+ }
1254
+ const cwd = typeof argsObj.cwd === 'string' ? String(argsObj.cwd) : '';
1255
+ const workdir = typeof argsObj.workdir === 'string' ? String(argsObj.workdir) : '';
1256
+ if (!workdir && cwd) {
1257
+ argsObj.workdir = cwd;
1258
+ }
1259
+ }
1260
+ if (lname === 'write_stdin') {
1261
+ const chars = typeof argsObj.chars === 'string' ? String(argsObj.chars) : '';
1262
+ const textVal = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
1263
+ const input = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
1264
+ if (!chars && (textVal || input)) {
1265
+ argsObj.chars = textVal || input;
1266
+ }
1267
+ const sid = argsObj.session_id;
1268
+ const sidAlt = argsObj.sessionId;
1269
+ if ((sid === undefined || sid === null || String(sid).trim() === '') && (sidAlt !== undefined && sidAlt !== null)) {
1270
+ argsObj.session_id = sidAlt;
1271
+ }
1272
+ }
1273
+ if (lname === 'apply_patch') {
1274
+ const patch = typeof argsObj.patch === 'string' ? String(argsObj.patch) : '';
1275
+ const textPatch = typeof argsObj.text === 'string' ? String(argsObj.text) : '';
1276
+ const inputPatch = typeof argsObj.input === 'string' ? String(argsObj.input) : '';
1277
+ const instructions = typeof argsObj.instructions === 'string' ? String(argsObj.instructions) : '';
1278
+ if (!patch && (textPatch || inputPatch || instructions)) {
1279
+ argsObj.patch = textPatch || inputPatch || instructions;
1280
+ }
1281
+ }
1282
+ const filtered = filterArgsForTool(lname, argsObj);
1283
+ // Basic guards for known tools
1284
+ if (lname === 'exec_command') {
1285
+ const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
1286
+ if (!cmd)
1287
+ continue;
1288
+ }
1289
+ if (lname === 'write_stdin') {
1290
+ // Allow empty args: our servertool schema may inject session_id automatically when a session is active.
1291
+ // Some models emit polling writes as `{}`.
1292
+ }
1293
+ if (lname === 'apply_patch') {
1294
+ const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
1295
+ if (!patchText)
1296
+ continue;
1297
+ }
1298
+ let argsStr = '{}';
1299
+ try {
1300
+ argsStr = JSON.stringify(filtered);
1301
+ }
1302
+ catch {
1303
+ argsStr = '{}';
1304
+ }
1305
+ const stableId = (() => {
1306
+ if (numericId && /^[0-9]{1,10}$/.test(numericId)) {
1307
+ const namePart = lname.replace(/[^A-Za-z0-9_-]/g, '_');
1308
+ const id = `call_${namePart}_${numericId}`
1309
+ .replace(/_{2,}/g, '_')
1310
+ .replace(/^_+/, '')
1311
+ .replace(/_+$/, '');
1312
+ return id || undefined;
1313
+ }
1314
+ return undefined;
1315
+ })();
1316
+ out.push({ id: stableId ?? `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
1317
+ }
1318
+ return out.length ? out : null;
1319
+ }
1320
+ catch {
1321
+ return null;
1322
+ }
1323
+ }
1057
1324
  export function normalizeAssistantTextToToolCalls(message) {
1058
1325
  if (!enabled())
1059
1326
  return message;
@@ -1094,7 +1361,8 @@ export function normalizeAssistantTextToToolCalls(message) {
1094
1361
  for (const text of candidates) {
1095
1362
  // Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等) → bare command salvage
1096
1363
  calls =
1097
- extractToolNamespaceXmlBlocksFromText(text) ||
1364
+ extractQwenToolCallTokensFromText(text) ||
1365
+ extractToolNamespaceXmlBlocksFromText(text) ||
1098
1366
  extractParameterXmlToolsFromText(text) ||
1099
1367
  extractInvokeToolsFromText(text) ||
1100
1368
  extractXMLToolCallsFromText(text) ||
@@ -515,7 +515,13 @@ function normalizeProvider(providerId, raw) {
515
515
  const compatibilityProfile = resolveCompatibilityProfile(providerId, provider);
516
516
  const headers = maybeInjectClaudeCodeHeaders(providerId, providerType, compatibilityProfile, normalizeHeaders(provider.headers));
517
517
  const responsesNode = asRecord(provider.responses);
518
- const responsesConfig = normalizeResponsesConfig(provider, responsesNode);
518
+ const responsesConfig = normalizeResponsesConfig({
519
+ providerId,
520
+ providerType,
521
+ compatibilityProfile,
522
+ provider,
523
+ node: responsesNode
524
+ });
519
525
  const processMode = normalizeProcessMode(provider.process);
520
526
  const streaming = resolveProviderStreamingPreference(provider, responsesNode);
521
527
  const modelStreaming = normalizeModelStreaming(provider);
@@ -660,16 +666,23 @@ function coerceStreamingPreference(value) {
660
666
  }
661
667
  return undefined;
662
668
  }
663
- function normalizeResponsesConfig(provider, node) {
664
- const source = node ?? asRecord(provider.responses);
665
- if (!source) {
666
- return undefined;
667
- }
669
+ function normalizeResponsesConfig(options) {
670
+ const source = options.node ?? asRecord(options.provider.responses);
668
671
  const rawStyle = typeof source.toolCallIdStyle === 'string' ? source.toolCallIdStyle.trim().toLowerCase() : undefined;
669
672
  if (rawStyle === 'fc' || rawStyle === 'preserve') {
670
673
  return { toolCallIdStyle: rawStyle };
671
674
  }
672
- return undefined;
675
+ const providerType = typeof options.providerType === 'string' ? options.providerType.trim().toLowerCase() : '';
676
+ if (!providerType.includes('responses')) {
677
+ return undefined;
678
+ }
679
+ const providerId = typeof options.providerId === 'string' ? options.providerId.trim().toLowerCase() : '';
680
+ const compat = typeof options.compatibilityProfile === 'string' ? options.compatibilityProfile.trim().toLowerCase() : '';
681
+ // Default tool-call id style:
682
+ // - Standard OpenAI /v1/responses requires function_call ids to start with "fc_".
683
+ // - LM Studio (OpenAI-compatible) often emits `call_*` ids and expects them to be preserved.
684
+ const isLmstudio = providerId === 'lmstudio' || compat === 'chat:lmstudio';
685
+ return { toolCallIdStyle: isLmstudio ? 'preserve' : 'fc' };
673
686
  }
674
687
  function resolveProviderStreamingPreference(provider, responsesNode) {
675
688
  const configNode = asRecord(provider.config);
@@ -1046,6 +1059,17 @@ function extractProviderAuthEntries(providerId, raw) {
1046
1059
  pushEntry(undefined, buildAuthCandidate(baseTypeSource, fallbackExtras));
1047
1060
  }
1048
1061
  }
1062
+ // Allow explicit apiKey auth with empty value (local no-auth providers like LM Studio).
1063
+ // If the config declares an auth node (even with an empty apiKey), treat it as an intentional no-auth setup.
1064
+ if (!entries.length && baseType === 'apiKey') {
1065
+ const authDeclared = Object.prototype.hasOwnProperty.call(provider, 'auth') ||
1066
+ Object.prototype.hasOwnProperty.call(provider, 'apiKey') ||
1067
+ Object.prototype.hasOwnProperty.call(provider, 'apiKeys') ||
1068
+ Object.prototype.hasOwnProperty.call(provider, 'authType');
1069
+ if (authDeclared) {
1070
+ pushEntry(undefined, buildAuthCandidate(baseTypeSource, { value: '' }));
1071
+ }
1072
+ }
1049
1073
  if (!entries.length) {
1050
1074
  throw new VirtualRouterError(`Provider ${providerId} is missing auth configuration`, VirtualRouterErrorCode.CONFIG_ERROR);
1051
1075
  }