@jsonstudio/llms 0.6.3409 → 0.6.3541

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 (85) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.d.ts +12 -3
  2. package/dist/conversion/codecs/anthropic-openai-codec.js +32 -92
  3. package/dist/conversion/codecs/gemini-openai-codec.d.ts +6 -5
  4. package/dist/conversion/codecs/gemini-openai-codec.js +48 -685
  5. package/dist/conversion/codecs/openai-openai-codec.d.ts +1 -1
  6. package/dist/conversion/codecs/openai-openai-codec.js +34 -100
  7. package/dist/conversion/codecs/responses-openai-codec.d.ts +1 -1
  8. package/dist/conversion/codecs/responses-openai-codec.js +47 -159
  9. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +2 -6
  10. package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +29 -245
  11. package/dist/conversion/compat/actions/anthropic-claude-code-user-id.d.ts +3 -0
  12. package/dist/conversion/compat/actions/anthropic-claude-code-user-id.js +30 -0
  13. package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +21 -232
  14. package/dist/conversion/compat/actions/deepseek-web-request.js +41 -276
  15. package/dist/conversion/compat/actions/deepseek-web-response.js +117 -855
  16. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +1 -1
  17. package/dist/conversion/compat/actions/gemini-cli-request.js +20 -613
  18. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -15
  19. package/dist/conversion/compat/actions/gemini-web-search.js +22 -69
  20. package/dist/conversion/compat/actions/glm-tool-extraction.d.ts +3 -2
  21. package/dist/conversion/compat/actions/glm-tool-extraction.js +28 -257
  22. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +0 -8
  23. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +24 -206
  24. package/dist/conversion/compat/actions/qwen-transform.d.ts +3 -2
  25. package/dist/conversion/compat/actions/qwen-transform.js +30 -271
  26. package/dist/conversion/compat/actions/tool-text-request-guidance.js +3 -173
  27. package/dist/conversion/compat/actions/universal-shape-filter.d.ts +6 -23
  28. package/dist/conversion/compat/actions/universal-shape-filter.js +4 -383
  29. package/dist/conversion/hub/pipeline/compat/native-adapter-context.js +1 -0
  30. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.d.ts +1 -2
  31. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +50 -104
  32. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +12 -10
  33. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.d.ts +0 -2
  34. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +46 -67
  35. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +15 -40
  36. package/dist/conversion/responses/responses-openai-bridge/response-payload.js +47 -348
  37. package/dist/conversion/responses/responses-openai-bridge.js +129 -611
  38. package/dist/conversion/shared/chat-output-normalizer.js +6 -0
  39. package/dist/conversion/shared/chat-request-filters.js +1 -1
  40. package/dist/conversion/shared/output-content-normalizer.js +10 -0
  41. package/dist/conversion/shared/responses-conversation-store.js +22 -135
  42. package/dist/conversion/shared/responses-output-builder.d.ts +0 -2
  43. package/dist/conversion/shared/responses-output-builder.js +28 -318
  44. package/dist/conversion/shared/responses-response-utils.js +35 -86
  45. package/dist/conversion/shared/streaming-text-extractor.d.ts +1 -2
  46. package/dist/conversion/shared/streaming-text-extractor.js +13 -14
  47. package/dist/native/router_hotpath_napi.node +0 -0
  48. package/dist/quota/quota-state.js +29 -7
  49. package/dist/quota/types.d.ts +1 -0
  50. package/dist/router/virtual-router/bootstrap/routing-config.js +11 -3
  51. package/dist/router/virtual-router/engine-legacy.d.ts +3 -3
  52. package/dist/router/virtual-router/engine-legacy.js +15 -7
  53. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.d.ts +16 -0
  54. package/dist/router/virtual-router/engine-selection/native-compat-action-semantics.js +434 -46
  55. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +83 -0
  56. package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +295 -0
  57. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.d.ts +1 -0
  58. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.d.ts +7 -0
  59. package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js +8 -1
  60. package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +383 -298
  61. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +20 -0
  62. package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +201 -0
  63. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.d.ts +1 -0
  64. package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.js +37 -0
  65. package/dist/router/virtual-router/engine.js +0 -38
  66. package/dist/router/virtual-router/features.js +44 -3
  67. package/dist/router/virtual-router/routing-instructions/parse.d.ts +0 -12
  68. package/dist/router/virtual-router/routing-instructions/parse.js +9 -389
  69. package/dist/router/virtual-router/stop-message-state-sync.d.ts +3 -6
  70. package/dist/router/virtual-router/stop-message-state-sync.js +50 -21
  71. package/dist/servertool/handlers/followup-request-builder.js +12 -2
  72. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  73. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +26 -0
  74. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +12 -2
  75. package/package.json +1 -1
  76. package/dist/router/virtual-router/engine-legacy/route-finalize.d.ts +0 -9
  77. package/dist/router/virtual-router/engine-legacy/route-finalize.js +0 -84
  78. package/dist/router/virtual-router/engine-legacy/route-selection.d.ts +0 -17
  79. package/dist/router/virtual-router/engine-legacy/route-selection.js +0 -205
  80. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.d.ts +0 -3
  81. package/dist/router/virtual-router/engine-legacy/route-state-allowlist.js +0 -36
  82. package/dist/router/virtual-router/engine-legacy/route-state.d.ts +0 -12
  83. package/dist/router/virtual-router/engine-legacy/route-state.js +0 -386
  84. package/dist/router/virtual-router/engine-legacy/routing.d.ts +0 -8
  85. package/dist/router/virtual-router/engine-legacy/routing.js +0 -8
@@ -1,404 +1,24 @@
1
- import { extractMessageText, getLatestUserMessage } from '../message-utils.js';
2
- import { parseStopMessageInstruction } from '../routing-stop-message-parser.js';
3
- import { parsePreCommandInstruction } from '../routing-pre-command-parser.js';
4
- import { stripCodeSegments } from './clean.js';
5
- import { ROUTING_INSTRUCTION_MARKER_PATTERN } from './types.js';
1
+ import { parseRoutingInstructionsWithNative } from '../engine-selection/native-virtual-router-routing-instructions-semantics.js';
6
2
  export function parseRoutingInstructions(messages) {
7
- const instructions = [];
8
- // 只解析“当前最新一条消息”中的 marker,避免历史 user marker 在后续轮次被重复回放。
9
- const latestMessage = getLatestUserMessage(messages);
10
- if (!latestMessage) {
11
- return instructions;
12
- }
13
- const content = extractMessageText(latestMessage);
14
- if (!content) {
15
- return instructions;
16
- }
17
- const sanitized = stripCodeSegments(content);
18
- if (!sanitized || !ROUTING_INSTRUCTION_MARKER_PATTERN.test(sanitized)) {
19
- return instructions;
20
- }
21
- const regex = /<\*\*([\s\S]*?)\*\*>/g;
22
- let match;
23
- while ((match = regex.exec(sanitized)) !== null) {
24
- const instruction = match[1].trim();
25
- if (!instruction) {
26
- continue;
27
- }
28
- const segments = expandInstructionSegments(instruction);
29
- for (const segment of segments) {
30
- const parsed = parseSingleInstruction(segment);
31
- if (parsed) {
32
- instructions.push(parsed);
33
- }
34
- }
3
+ if (!Array.isArray(messages) || messages.length === 0) {
4
+ return [];
35
5
  }
36
- return normalizeStopMessageInstructionPrecedence(instructions);
6
+ return parseRoutingInstructionsWithNative(messages);
37
7
  }
38
- /**
39
- * 解析并预处理路由指令,优先处理 clear 指令,确保新指令能够覆盖旧状态。
40
- * 返回清理后的指令列表,移除冗余的 stopMessageSet 指令。
41
- */
42
8
  export function parseAndPreprocessRoutingInstructions(messages) {
43
9
  const rawInstructions = parseRoutingInstructions(messages);
44
10
  if (rawInstructions.length === 0) {
45
11
  return [];
46
12
  }
47
- // 检查是否有 clear 指令
48
- const hasClear = rawInstructions.some((inst) => inst.type === 'clear');
49
- if (!hasClear) {
13
+ const clearIndex = rawInstructions.findIndex((inst) => inst.type === 'clear');
14
+ if (clearIndex < 0) {
50
15
  return rawInstructions;
51
16
  }
52
- // 如果有 clear 指令,clear 之后的指令才有效,clear 之前的指令被清除
53
- const clearIndex = rawInstructions.findIndex((inst) => inst.type === 'clear');
54
- const effectiveInstructions = rawInstructions.slice(clearIndex + 1);
55
- // 移除 clear 后冗余的 stopMessageSet 指令(如果与原指令相同)
56
- // 这里的逻辑会在 applyRoutingInstructions 中处理,
57
- // 所以我们只需要返回 clear 之后的指令即可
58
- return effectiveInstructions;
17
+ return rawInstructions.slice(clearIndex + 1);
59
18
  }
60
- /**
61
- * 提取 clear 指令(如果存在)。用于在路由选择前优先执行清理操作。
62
- * @returns 是否存在 clear 指令
63
- */
64
19
  export function extractClearInstruction(messages) {
65
- const instructions = parseRoutingInstructions(messages);
66
- return instructions.some((inst) => inst.type === 'clear');
20
+ return parseRoutingInstructions(messages).some((inst) => inst.type === 'clear');
67
21
  }
68
- /**
69
- * 提取 stopMessageClear 指令(如果存在)。
70
- * @returns 是否存在 stopMessageClear 指令
71
- */
72
22
  export function extractStopMessageClearInstruction(messages) {
73
- const instructions = parseRoutingInstructions(messages);
74
- return instructions.some((inst) => inst.type === 'stopMessageClear');
75
- }
76
- function expandInstructionSegments(instruction) {
77
- const trimmed = instruction.trim();
78
- if (!trimmed) {
79
- return [];
80
- }
81
- const normalizedLeading = normalizeInstructionLeading(trimmed);
82
- // stopMessage 指令需要整体解析,不能按逗号拆分,否则类似
83
- // "<**stopMessage:\"继续\",3**>" 会被错误拆成 ["stopMessage:\"继续\"", "3"]。
84
- if (/^(?:"|')?stopMessage(?:"|')?\s*[:,]/i.test(normalizedLeading)) {
85
- return [normalizeStopMessageCommandPrefix(normalizedLeading)];
86
- }
87
- if (/^precommand(?:\s*:|$)/i.test(normalizedLeading)) {
88
- return [normalizedLeading];
89
- }
90
- const quotedStopMessage = normalizeQuotedStopMessageShorthand(normalizedLeading);
91
- if (quotedStopMessage) {
92
- return [normalizeStopMessageCommandPrefix(quotedStopMessage)];
93
- }
94
- const prefix = trimmed[0];
95
- if (prefix === '!' || prefix === '#' || prefix === '@') {
96
- const tokens = splitInstructionTargets(trimmed.substring(1));
97
- return tokens
98
- .map((token) => token.replace(/^[!#@]+/, '').trim())
99
- .filter((token) => token.length > 0)
100
- .map((token) => `${prefix}${token}`);
101
- }
102
- const splitTokens = splitInstructionTargets(trimmed);
103
- const recoveredStopMessage = recoverSplitStopMessageInstruction(splitTokens);
104
- if (recoveredStopMessage) {
105
- return [recoveredStopMessage];
106
- }
107
- return splitTokens;
108
- }
109
- function splitInstructionTargets(content) {
110
- return content
111
- .split(',')
112
- .map((segment) => segment.trim())
113
- .filter((segment) => segment.length > 0);
114
- }
115
- function normalizeInstructionLeading(content) {
116
- // Remove common zero-width prefixes that may be injected by client/editor copies.
117
- return content.replace(/^[\u200B-\u200D\u2060\uFEFF]+/, '').trimStart();
118
- }
119
- function normalizeStopMessageCommandPrefix(content) {
120
- const normalized = normalizeInstructionLeading(content);
121
- return normalized.replace(/^(?:"|')?stopMessage(?:"|')?\s*([:,])/i, 'stopMessage$1');
122
- }
123
- function normalizeSplitStopMessageHeadToken(token) {
124
- return normalizeInstructionLeading(token)
125
- .replace(/^["']+|["']+$/g, '')
126
- .trim();
127
- }
128
- function normalizeQuotedStopMessageShorthand(content) {
129
- const normalized = normalizeInstructionLeading(content);
130
- const quote = normalized[0];
131
- if (quote !== '"' && quote !== "'") {
132
- return null;
133
- }
134
- let escaped = false;
135
- for (let idx = 1; idx < normalized.length; idx += 1) {
136
- const ch = normalized[idx];
137
- if (escaped) {
138
- escaped = false;
139
- continue;
140
- }
141
- if (ch === '\\') {
142
- escaped = true;
143
- continue;
144
- }
145
- if (ch === quote) {
146
- return `stopMessage:${normalized}`;
147
- }
148
- }
149
- return null;
150
- }
151
- function recoverSplitStopMessageInstruction(tokens) {
152
- if (!Array.isArray(tokens) || tokens.length < 2) {
153
- return null;
154
- }
155
- const head = normalizeSplitStopMessageHeadToken(tokens[0]);
156
- if (!/^stopmessage$/i.test(head)) {
157
- return null;
158
- }
159
- const tail = tokens.slice(1).join(',').trim();
160
- if (!tail) {
161
- return null;
162
- }
163
- return `stopMessage:${tail}`;
164
- }
165
- function splitTargetAndProcessMode(rawTarget) {
166
- const trimmed = typeof rawTarget === 'string' ? rawTarget.trim() : '';
167
- if (!trimmed) {
168
- return { target: '' };
169
- }
170
- const separatorIndex = trimmed.lastIndexOf(':');
171
- if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
172
- return { target: trimmed };
173
- }
174
- const target = trimmed.slice(0, separatorIndex).trim();
175
- const modeToken = trimmed.slice(separatorIndex + 1).trim().toLowerCase();
176
- if (!target) {
177
- return { target: trimmed };
178
- }
179
- if (modeToken === 'passthrough') {
180
- return { target, processMode: 'passthrough' };
181
- }
182
- if (modeToken === 'chat') {
183
- return { target, processMode: 'chat' };
184
- }
185
- return { target };
186
- }
187
- function parseNamedTargetInstruction(instruction, prefix) {
188
- const re = new RegExp('^' + prefix + '\\s*:', 'i');
189
- if (!re.test(instruction)) {
190
- return null;
191
- }
192
- const body = instruction.slice(instruction.indexOf(':') + 1).trim();
193
- if (!body) {
194
- return null;
195
- }
196
- const { target, processMode } = splitTargetAndProcessMode(body);
197
- if (!target) {
198
- return null;
199
- }
200
- const parsed = parseTarget(target);
201
- if (!parsed) {
202
- return null;
203
- }
204
- const normalized = normalizeStickyOrForceTarget(parsed);
205
- return { type: prefix, ...normalized, ...(processMode ? { processMode } : {}) };
206
- }
207
- function parseSingleInstruction(instruction) {
208
- if (/^clear$/i.test(instruction)) {
209
- return { type: 'clear' };
210
- }
211
- const preCommandInstruction = parsePreCommandInstruction(instruction);
212
- if (preCommandInstruction) {
213
- return preCommandInstruction;
214
- }
215
- const stopMessageInstruction = parseStopMessageInstruction(instruction);
216
- if (stopMessageInstruction) {
217
- return stopMessageInstruction;
218
- }
219
- const stickyInstruction = parseNamedTargetInstruction(instruction, 'sticky');
220
- if (stickyInstruction) {
221
- return stickyInstruction;
222
- }
223
- const forceInstruction = parseNamedTargetInstruction(instruction, 'force');
224
- if (forceInstruction) {
225
- return forceInstruction;
226
- }
227
- const preferInstruction = parseNamedTargetInstruction(instruction, 'prefer');
228
- if (preferInstruction) {
229
- return preferInstruction;
230
- }
231
- if (instruction.startsWith('!')) {
232
- const rawTarget = instruction.substring(1).trim();
233
- const { target, processMode } = splitTargetAndProcessMode(rawTarget);
234
- if (!target) {
235
- return null;
236
- }
237
- const parsed = parseTarget(target);
238
- if (!parsed) {
239
- return null;
240
- }
241
- // 约定:
242
- // - "!providerA,providerB":允许列表(whitelist),用于快速限制可用 provider 集合;
243
- // - "!provider.model" / "!provider[alias].model" / "!provider.2":prefer 语义;
244
- // model 可用时只命中该 model(忽略路由),不可用则自动清除并回退到正常路由命中。
245
- //
246
- // 这样可以在不破坏既有 "!glm,openai" 语义的前提下,引入基于模型的优先命中行为。
247
- if (!target.includes('.')) {
248
- if (parsed.provider) {
249
- return { type: 'allow', provider: parsed.provider, pathLength: parsed.pathLength };
250
- }
251
- return null;
252
- }
253
- const normalized = normalizeStickyOrForceTarget(parsed);
254
- return { type: 'prefer', ...normalized, ...(processMode ? { processMode } : {}) };
255
- }
256
- if (instruction.startsWith('#')) {
257
- const target = instruction.substring(1).trim();
258
- const parsed = parseTarget(target);
259
- if (parsed) {
260
- return { type: 'disable', ...parsed };
261
- }
262
- }
263
- else if (instruction.startsWith('@')) {
264
- const target = instruction.substring(1).trim();
265
- const parsed = parseTarget(target);
266
- if (parsed) {
267
- return { type: 'enable', ...parsed };
268
- }
269
- }
270
- else if (isValidProviderModel(instruction)) {
271
- const parsed = parseTarget(instruction);
272
- if (parsed) {
273
- const normalized = normalizeStickyOrForceTarget(parsed);
274
- return { type: 'force', ...normalized };
275
- }
276
- }
277
- else if (isValidIdentifier(instruction)) {
278
- // 仅 provider 标识(无 .)时,视为 provider 级白名单,等价于 "<**!provider**>"。
279
- // 这样可以用 "<**antigravity**>" 快速激活当前 routing 中所有 antigravity 相关池子,
280
- // 并保证路由仅命中该 provider 的所有模型/key。
281
- return {
282
- type: 'allow',
283
- provider: instruction,
284
- pathLength: 1
285
- };
286
- }
287
- return null;
288
- }
289
- function normalizeStopMessageInstructionPrecedence(instructions) {
290
- if (!Array.isArray(instructions) || instructions.length <= 1) {
291
- return instructions;
292
- }
293
- const isStopDirective = (inst) => inst.type === 'stopMessageSet' || inst.type === 'stopMessageMode' || inst.type === 'stopMessageClear';
294
- const hasGlobalClear = instructions.some((inst) => inst.type === 'clear');
295
- const hasStopClear = instructions.some((inst) => inst.type === 'stopMessageClear');
296
- if (hasGlobalClear) {
297
- const lastGlobalClearIndex = instructions.map((inst) => inst.type).lastIndexOf('clear');
298
- return lastGlobalClearIndex >= 0 ? [instructions[lastGlobalClearIndex]] : instructions;
299
- }
300
- if (hasStopClear) {
301
- const lastStopClearIndex = instructions.map((inst) => inst.type).lastIndexOf('stopMessageClear');
302
- return lastStopClearIndex >= 0 ? [instructions[lastStopClearIndex]] : instructions;
303
- }
304
- let lastStopIndex = -1;
305
- for (let idx = instructions.length - 1; idx >= 0; idx -= 1) {
306
- if (isStopDirective(instructions[idx])) {
307
- lastStopIndex = idx;
308
- break;
309
- }
310
- }
311
- if (lastStopIndex < 0) {
312
- return instructions;
313
- }
314
- return instructions.filter((inst, idx) => !isStopDirective(inst) || idx === lastStopIndex);
315
- }
316
- function parseTarget(target) {
317
- if (!target) {
318
- return null;
319
- }
320
- // Accept "provider[alias].model" (as printed in virtual-router-hit logs) to avoid users
321
- // needing to translate bracket notation back to dot notation manually.
322
- // With the alias disambiguated, allow dots in model ids (e.g. gpt-5.2) without ambiguity.
323
- const bracketMatch = target.match(/^([a-zA-Z0-9_-]+)\[([a-zA-Z0-9_-]*)\](?:\.(.+))?$/);
324
- if (bracketMatch) {
325
- const provider = bracketMatch[1];
326
- const keyAliasRaw = bracketMatch[2];
327
- const keyAlias = typeof keyAliasRaw === 'string' ? keyAliasRaw.trim() : '';
328
- const model = typeof bracketMatch[3] === 'string' ? bracketMatch[3].trim() : '';
329
- if (!provider || !isValidIdentifier(provider)) {
330
- return null;
331
- }
332
- // Allow omitting the alias: "provider[].model" means "provider.model across all aliases".
333
- // This also enables disambiguating model ids that contain dots, without requiring the user
334
- // to specify the alias.
335
- if (!keyAlias) {
336
- if (!model) {
337
- return { provider, pathLength: 1 };
338
- }
339
- if (!/^[a-zA-Z0-9_.-]+$/.test(model)) {
340
- return null;
341
- }
342
- return { provider, model, pathLength: 2 };
343
- }
344
- if (!isValidIdentifier(keyAlias)) {
345
- return null;
346
- }
347
- if (!model) {
348
- // Treat as explicit alias selection. Use pathLength=3 so engine resolves keyAlias directly.
349
- return { provider, keyAlias, pathLength: 3 };
350
- }
351
- // Model ids may contain dots (e.g. gpt-5.2); allow them here because alias is already explicit.
352
- if (!/^[a-zA-Z0-9_.-]+$/.test(model)) {
353
- return null;
354
- }
355
- return { provider, keyAlias, model, pathLength: 3 };
356
- }
357
- // Dot syntax: align with config parsing used by routing entries.
358
- // - "provider.modelId" -> modelId may contain dots; it always means model across all aliases.
359
- // - Key/alias selection must use bracket syntax: "provider[alias]" or "provider[alias].modelId".
360
- const firstDot = target.indexOf('.');
361
- if (firstDot < 0) {
362
- const provider = target.trim();
363
- if (!provider || !isValidIdentifier(provider)) {
364
- return null;
365
- }
366
- return { provider, pathLength: 1 };
367
- }
368
- const provider = target.slice(0, firstDot).trim();
369
- const remainder = target.slice(firstDot + 1).trim();
370
- if (!provider || !isValidIdentifier(provider) || !remainder) {
371
- return null;
372
- }
373
- // Support "provider.2" key-index notation (only when remainder is a plain integer).
374
- if (/^\d+$/.test(remainder)) {
375
- const keyIndex = Number.parseInt(remainder, 10);
376
- if (Number.isFinite(keyIndex) && keyIndex > 0) {
377
- return { provider, keyIndex, pathLength: 2 };
378
- }
379
- }
380
- // Treat everything after the first dot as the model id, allowing dots.
381
- if (!/^[a-zA-Z0-9_.-]+$/.test(remainder)) {
382
- return null;
383
- }
384
- return { provider, model: remainder, pathLength: 2 };
385
- }
386
- function normalizeStickyOrForceTarget(target) {
387
- if (target &&
388
- target.pathLength === 2 &&
389
- typeof target.model === 'string' &&
390
- typeof target.keyAlias === 'string' &&
391
- target.model === target.keyAlias) {
392
- const clone = { ...target };
393
- delete clone.keyAlias;
394
- return clone;
395
- }
396
- return target;
397
- }
398
- function isValidIdentifier(id) {
399
- return /^[a-zA-Z0-9_-]+$/.test(id);
400
- }
401
- function isValidProviderModel(providerModel) {
402
- const pattern = /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)+$/;
403
- return pattern.test(providerModel);
23
+ return parseRoutingInstructions(messages).some((inst) => inst.type === 'stopMessageClear');
404
24
  }
@@ -8,13 +8,10 @@ type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'st
8
8
  * - Persisted state must still be able to update usage counters (stop_message_auto).
9
9
  *
10
10
  * Strategy:
11
- * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
11
+ * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config by default.
12
+ * - However, when persisted carries the same stopMessage config but newer usage progress,
13
+ * prefer the persisted counters so Virtual Router logs/state reflect real stop_message_auto consumption.
12
14
  * - Otherwise → adopt persisted fully.
13
- *
14
- * Note:
15
- * - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
16
- * A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
17
- * would make re-arming flaky until the async persistence catches up.
18
15
  */
19
16
  export declare function mergeStopMessageFromPersisted(existing: StopMessageSubset, persisted: StopMessageSubset | null): StopMessageSubset;
20
17
  export {};
@@ -11,6 +11,29 @@ function lastUsedAtOf(state) {
11
11
  return null;
12
12
  return isFiniteNumber(state.stopMessageLastUsedAt) ? state.stopMessageLastUsedAt : null;
13
13
  }
14
+ function usedOf(state) {
15
+ if (!state)
16
+ return null;
17
+ return isFiniteNumber(state.stopMessageUsed) ? state.stopMessageUsed : null;
18
+ }
19
+ function normalizeText(value) {
20
+ return typeof value === 'string' ? value.trim() : '';
21
+ }
22
+ function sameStopMessageConfig(existing, persisted) {
23
+ return normalizeText(existing.stopMessageText) === normalizeText(persisted.stopMessageText)
24
+ && existing.stopMessageMaxRepeats === persisted.stopMessageMaxRepeats
25
+ && normalizeText(existing.stopMessageStageMode) === normalizeText(persisted.stopMessageStageMode)
26
+ && normalizeText(existing.stopMessageAiMode) === normalizeText(persisted.stopMessageAiMode);
27
+ }
28
+ function overlayPersistedUsage(existing, persisted) {
29
+ return {
30
+ ...existing,
31
+ stopMessageUsed: persisted.stopMessageUsed,
32
+ stopMessageLastUsedAt: persisted.stopMessageLastUsedAt,
33
+ stopMessageAiSeedPrompt: persisted.stopMessageAiSeedPrompt,
34
+ stopMessageAiHistory: persisted.stopMessageAiHistory
35
+ };
36
+ }
14
37
  /**
15
38
  * Decide whether we should overwrite in-memory stopMessage fields with persisted ones.
16
39
  *
@@ -19,13 +42,10 @@ function lastUsedAtOf(state) {
19
42
  * - Persisted state must still be able to update usage counters (stop_message_auto).
20
43
  *
21
44
  * Strategy:
22
- * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
45
+ * - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config by default.
46
+ * - However, when persisted carries the same stopMessage config but newer usage progress,
47
+ * prefer the persisted counters so Virtual Router logs/state reflect real stop_message_auto consumption.
23
48
  * - Otherwise → adopt persisted fully.
24
- *
25
- * Note:
26
- * - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
27
- * A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
28
- * would make re-arming flaky until the async persistence catches up.
29
49
  */
30
50
  export function mergeStopMessageFromPersisted(existing, persisted) {
31
51
  if (!persisted) {
@@ -34,20 +54,29 @@ export function mergeStopMessageFromPersisted(existing, persisted) {
34
54
  const existingUpdatedAt = updatedAtOf(existing);
35
55
  const persistedUpdatedAt = updatedAtOf(persisted);
36
56
  const existingIsNewer = existingUpdatedAt !== null && (persistedUpdatedAt === null || persistedUpdatedAt < existingUpdatedAt);
37
- if (!existingIsNewer) {
38
- return {
39
- ...existing,
40
- stopMessageSource: persisted.stopMessageSource,
41
- stopMessageText: persisted.stopMessageText,
42
- stopMessageMaxRepeats: persisted.stopMessageMaxRepeats,
43
- stopMessageUsed: persisted.stopMessageUsed,
44
- stopMessageStageMode: persisted.stopMessageStageMode,
45
- stopMessageAiMode: persisted.stopMessageAiMode,
46
- stopMessageAiSeedPrompt: persisted.stopMessageAiSeedPrompt,
47
- stopMessageAiHistory: persisted.stopMessageAiHistory,
48
- stopMessageUpdatedAt: persisted.stopMessageUpdatedAt,
49
- stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
50
- };
57
+ if (existingIsNewer) {
58
+ const existingUsed = usedOf(existing) ?? 0;
59
+ const persistedUsed = usedOf(persisted) ?? 0;
60
+ const existingLastUsedAt = lastUsedAtOf(existing);
61
+ const persistedLastUsedAt = lastUsedAtOf(persisted);
62
+ const persistedHasUsageProgress = persistedUsed > existingUsed
63
+ && (existingLastUsedAt === null || (persistedLastUsedAt !== null && persistedLastUsedAt >= existingLastUsedAt));
64
+ if (persistedHasUsageProgress && sameStopMessageConfig(existing, persisted)) {
65
+ return overlayPersistedUsage(existing, persisted);
66
+ }
67
+ return { ...existing };
51
68
  }
52
- return { ...existing };
69
+ return {
70
+ ...existing,
71
+ stopMessageSource: persisted.stopMessageSource,
72
+ stopMessageText: persisted.stopMessageText,
73
+ stopMessageMaxRepeats: persisted.stopMessageMaxRepeats,
74
+ stopMessageUsed: persisted.stopMessageUsed,
75
+ stopMessageStageMode: persisted.stopMessageStageMode,
76
+ stopMessageAiMode: persisted.stopMessageAiMode,
77
+ stopMessageAiSeedPrompt: persisted.stopMessageAiSeedPrompt,
78
+ stopMessageAiHistory: persisted.stopMessageAiHistory,
79
+ stopMessageUpdatedAt: persisted.stopMessageUpdatedAt,
80
+ stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
81
+ };
53
82
  }
@@ -1,4 +1,5 @@
1
1
  import { buildChatRequestFromResponses, captureResponsesContext } from '../../conversion/responses/responses-openai-bridge.js';
2
+ import { stripHistoricalImageAttachments } from '../../conversion/hub/process/chat-process-media.js';
2
3
  import { cloneJson } from '../server-side-tools.js';
3
4
  import { trimOpenAiMessagesForFollowup } from './followup-message-trimmer.js';
4
5
  function extractResponsesTopLevelParameters(record) {
@@ -74,9 +75,13 @@ export function normalizeFollowupParameters(value) {
74
75
  return undefined;
75
76
  }
76
77
  const cloned = cloneJson(value);
77
- // Followup requests are always non-streaming (servertool orchestration enforces this),
78
- // so remove any inherited stream hints to avoid conflicting flags.
78
+ // Followup requests are always re-entered as a fresh hop:
79
+ // - non-streaming (servertool orchestration enforces this)
80
+ // - no inherited tool-selection hints, otherwise the resumed turn can be biased toward
81
+ // immediately calling tools again instead of consuming the tool outputs that were just injected.
82
+ // Keep `parallel_tool_calls` inherited; provider compat can still disable it selectively.
79
83
  delete cloned.stream;
84
+ delete cloned.tool_choice;
80
85
  return Object.keys(cloned).length ? cloned : undefined;
81
86
  }
82
87
  export function dropToolByFunctionName(tools, dropName) {
@@ -418,6 +423,11 @@ export function buildServerToolFollowupChatPayloadFromInjection(args) {
418
423
  return null;
419
424
  }
420
425
  let messages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
426
+ // ServerTool followups must enter marker/routing/chat-process analysis with the same
427
+ // historical-media invariants as normal chat-process requests:
428
+ // only the latest live user turn may keep inline image payloads; earlier user turns
429
+ // are scrubbed to placeholders before any followup ops append new assistant/user items.
430
+ messages = stripHistoricalImageAttachments(messages);
421
431
  const ops = Array.isArray(args.injection?.ops) ? args.injection.ops : [];
422
432
  // Followup is a normal request hop: inherit tool schema from the captured request and
423
433
  // let compat/tool-governance apply standard sanitization rules.
@@ -9,6 +9,7 @@ export declare class AnthropicSseToJsonConverter {
9
9
  private contexts;
10
10
  constructor(config?: Partial<AnthropicSseToJsonConverterConfig>);
11
11
  convertSseToJson(sseStream: AsyncIterable<string | Buffer>, options: SseToAnthropicJsonOptions): Promise<AnthropicMessageResponse>;
12
+ private isTerminatedError;
12
13
  private createContext;
13
14
  private chunkStrings;
14
15
  private updateStats;
@@ -62,12 +62,38 @@ export class AnthropicSseToJsonConverter {
62
62
  }
63
63
  catch (error) {
64
64
  context.eventStats.errors = (context.eventStats.errors ?? 0) + 1;
65
+ if (this.isTerminatedError(error)) {
66
+ try {
67
+ const salvaged = builder.getResult();
68
+ if (salvaged.success && salvaged.response) {
69
+ context.isCompleted = true;
70
+ context.eventStats.endTime = Date.now();
71
+ return salvaged.response;
72
+ }
73
+ }
74
+ catch {
75
+ // ignore salvage failure, fall through to wrapped error
76
+ }
77
+ }
65
78
  throw this.wrapError('ANTHROPIC_SSE_TO_JSON_FAILED', error, options.requestId);
66
79
  }
67
80
  finally {
68
81
  this.contexts.delete(options.requestId);
69
82
  }
70
83
  }
84
+ isTerminatedError(error) {
85
+ if (!error || typeof error !== 'object') {
86
+ return false;
87
+ }
88
+ const message = error.message;
89
+ if (typeof message !== 'string') {
90
+ return false;
91
+ }
92
+ const normalized = message.toLowerCase();
93
+ return (normalized.includes('terminated') ||
94
+ normalized.includes('upstream_stream_idle_timeout') ||
95
+ normalized.includes('upstream_stream_timeout'));
96
+ }
71
97
  createContext(options) {
72
98
  return {
73
99
  requestId: options.requestId,
@@ -24,6 +24,16 @@ export function createAnthropicResponseBuilder(options) {
24
24
  role: 'assistant',
25
25
  completed: false
26
26
  };
27
+ const inferStopReason = () => {
28
+ if (state.stopReason) {
29
+ return state.stopReason;
30
+ }
31
+ const currentKind = state.currentBlock?.kind;
32
+ if (currentKind === 'tool_use' || state.content.some((block) => block.type === 'tool_use')) {
33
+ return 'tool_use';
34
+ }
35
+ return 'end_turn';
36
+ };
27
37
  const flushCurrent = () => {
28
38
  if (!state.currentBlock)
29
39
  return;
@@ -174,7 +184,7 @@ export function createAnthropicResponseBuilder(options) {
174
184
  model: state.model || 'unknown',
175
185
  content: state.content,
176
186
  usage: state.usage,
177
- stop_reason: state.stopReason ?? 'end_turn',
187
+ stop_reason: inferStopReason(),
178
188
  stop_sequence: state.stopSequence
179
189
  }
180
190
  };
@@ -190,7 +200,7 @@ export function createAnthropicResponseBuilder(options) {
190
200
  model: state.model || 'unknown',
191
201
  content: state.content,
192
202
  usage: state.usage,
193
- stop_reason: state.stopReason ?? 'end_turn',
203
+ stop_reason: inferStopReason(),
194
204
  stop_sequence: state.stopSequence
195
205
  }
196
206
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsonstudio/llms",
3
- "version": "0.6.3409",
3
+ "version": "0.6.3541",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,9 +0,0 @@
1
- import type { RoutingInstruction, RoutingInstructionState } from '../routing-instructions.js';
2
- import type { RouterMetadataInput, TargetMetadata, RoutingDecision, RoutingDiagnostics } from '../types.js';
3
- import type { VirtualRouterEngine } from '../engine-legacy.js';
4
- import type { RoutingSelectionResult } from './route-selection.js';
5
- export declare function finalizeRoutingDecision(engine: VirtualRouterEngine, metadata: RouterMetadataInput, routingState: RoutingInstructionState, metadataInstructions: RoutingInstruction[], instructions: RoutingInstruction[], selectionResult: RoutingSelectionResult): {
6
- target: TargetMetadata;
7
- decision: RoutingDecision;
8
- diagnostics: RoutingDiagnostics;
9
- };