@jsonstudio/llms 0.6.567 → 0.6.586

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 (62) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  5. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -2
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
  8. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  9. package/dist/conversion/hub/process/chat-process.js +68 -24
  10. package/dist/conversion/hub/response/provider-response.js +0 -8
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
  12. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
  13. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  15. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  16. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  17. package/dist/conversion/shared/args-mapping.js +11 -3
  18. package/dist/conversion/shared/responses-output-builder.js +42 -21
  19. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  20. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  21. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  22. package/dist/conversion/shared/text-markup-normalizer.js +118 -31
  23. package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
  24. package/dist/conversion/shared/tool-harvester.js +43 -12
  25. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  26. package/dist/conversion/shared/tool-mapping.js +33 -19
  27. package/dist/filters/index.d.ts +1 -0
  28. package/dist/filters/index.js +1 -0
  29. package/dist/filters/special/request-tools-normalize.js +14 -4
  30. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  31. package/dist/filters/special/response-apply-patch-toon-decode.js +117 -0
  32. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
  34. package/dist/guidance/index.js +71 -42
  35. package/dist/router/virtual-router/bootstrap.js +10 -5
  36. package/dist/router/virtual-router/classifier.js +16 -7
  37. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  38. package/dist/router/virtual-router/engine-health.js +217 -4
  39. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  40. package/dist/router/virtual-router/engine-logging.js +35 -3
  41. package/dist/router/virtual-router/engine.d.ts +17 -1
  42. package/dist/router/virtual-router/engine.js +184 -6
  43. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-instructions.js +19 -1
  45. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  46. package/dist/router/virtual-router/tool-signals.js +324 -119
  47. package/dist/router/virtual-router/types.d.ts +31 -1
  48. package/dist/router/virtual-router/types.js +2 -2
  49. package/dist/servertool/engine.js +3 -0
  50. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  51. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  52. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  53. package/dist/servertool/server-side-tools.d.ts +1 -0
  54. package/dist/servertool/server-side-tools.js +27 -0
  55. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  56. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
  57. package/dist/tools/apply-patch-structured.d.ts +20 -0
  58. package/dist/tools/apply-patch-structured.js +240 -0
  59. package/dist/tools/tool-description-utils.d.ts +5 -0
  60. package/dist/tools/tool-description-utils.js +50 -0
  61. package/dist/tools/tool-registry.js +11 -193
  62. package/package.json +1 -1
@@ -10,7 +10,7 @@ import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRo
10
10
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from './sticky-session-store.js';
11
11
  import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
12
12
  import { selectProviderImpl } from './engine-selection.js';
13
- import { applySeriesCooldownImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
13
+ import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
14
14
  export class VirtualRouterEngine {
15
15
  routing = {};
16
16
  providerRegistry = new ProviderRegistry();
@@ -26,7 +26,20 @@ export class VirtualRouterEngine {
26
26
  statsCenter = getStatsCenter();
27
27
  // Derived flags from VirtualRouterConfig/routing used by process / response layers.
28
28
  webSearchForce = false;
29
+ healthStore;
30
+ routingStateStore = {
31
+ loadSync: loadRoutingInstructionStateSync,
32
+ saveAsync: saveRoutingInstructionStateAsync
33
+ };
29
34
  routingInstructionState = new Map();
35
+ constructor(deps) {
36
+ if (deps?.healthStore) {
37
+ this.healthStore = deps.healthStore;
38
+ }
39
+ if (deps?.routingStateStore) {
40
+ this.routingStateStore = deps.routingStateStore;
41
+ }
42
+ }
30
43
  initialize(config) {
31
44
  this.validateConfig(config);
32
45
  this.routing = config.routing;
@@ -34,6 +47,8 @@ export class VirtualRouterEngine {
34
47
  this.healthManager.configure(config.health);
35
48
  this.healthConfig = config.health ?? null;
36
49
  this.healthManager.registerProviders(Object.keys(config.providers));
50
+ this.providerCooldowns.clear();
51
+ this.restoreHealthFromStore();
37
52
  this.loadBalancer = new RouteLoadBalancer(config.loadBalancing);
38
53
  this.classifier = new RoutingClassifier(config.classifier);
39
54
  this.contextRouting = config.contextRouting ?? { warnRatio: 0.9, hardLimit: false };
@@ -52,6 +67,15 @@ export class VirtualRouterEngine {
52
67
  if (metadataInstructions.length > 0) {
53
68
  routingState = applyRoutingInstructions(metadataInstructions, routingState);
54
69
  }
70
+ const disableStickyRoutes = metadata &&
71
+ typeof metadata === 'object' &&
72
+ metadata.disableStickyRoutes === true;
73
+ if (disableStickyRoutes && routingState.stickyTarget) {
74
+ routingState = {
75
+ ...routingState,
76
+ stickyTarget: undefined
77
+ };
78
+ }
55
79
  const instructions = parseRoutingInstructions(request.messages);
56
80
  if (instructions.length > 0) {
57
81
  routingState = applyRoutingInstructions(instructions, routingState);
@@ -98,7 +122,7 @@ export class VirtualRouterEngine {
98
122
  const hitReason = buildHitReason(selection.routeUsed, selection.providerKey, classification, features, routingMode, { providerRegistry: this.providerRegistry, contextRouting: this.contextRouting });
99
123
  const stickyScope = routingMode !== 'none' ? this.resolveSessionScope(metadata) : undefined;
100
124
  const routeForLog = routingMode === 'sticky' ? 'sticky' : selection.routeUsed;
101
- const formatted = formatVirtualRouterHit(routeForLog, selection.poolId, selection.providerKey, target.modelId || '', hitReason, stickyScope);
125
+ const formatted = formatVirtualRouterHit(routeForLog, selection.poolId, selection.providerKey, target.modelId || '', hitReason, stickyScope, routingState);
102
126
  if (formatted) {
103
127
  this.debug?.log?.(formatted);
104
128
  }
@@ -106,6 +130,36 @@ export class VirtualRouterEngine {
106
130
  this.debug?.log?.('[virtual-router-hit]', selection.routeUsed, selection.providerKey, target.modelId || '', hitReason ? `reason=${hitReason}` : '');
107
131
  }
108
132
  const didFallback = selection.routeUsed !== requestedRoute;
133
+ // 自动 sticky:对需要上下文 save/restore 的 Responses 会话,强制同一个 provider.key.model。
134
+ // 其它协议不启用粘滞,仅显式 routing 指令才会写入 stickyTarget。
135
+ const providerProtocol = metadata?.providerProtocol;
136
+ const disableSticky = metadata &&
137
+ typeof metadata === 'object' &&
138
+ metadata.disableStickyRoutes === true;
139
+ const shouldAutoStickyForResponses = providerProtocol === 'openai-responses' && !disableSticky;
140
+ if (shouldAutoStickyForResponses) {
141
+ const stickyKeyForState = this.resolveStickyKey(metadata);
142
+ if (stickyKeyForState) {
143
+ const stateKey = stickyKeyForState;
144
+ const state = this.getRoutingInstructionState(stateKey);
145
+ if (!state.stickyTarget) {
146
+ const providerId = this.extractProviderId(selection.providerKey);
147
+ if (providerId) {
148
+ const parts = selection.providerKey.split('.');
149
+ const keyAlias = parts.length >= 3 ? parts[1] : undefined;
150
+ const modelId = target.modelId;
151
+ state.stickyTarget = {
152
+ provider: providerId,
153
+ keyAlias,
154
+ model: modelId,
155
+ // pathLength=3 表示 provider.key.model 形式,对应 alias 显式 sticky;
156
+ // 若缺少别名或模型,则退化为更短 pathLength。
157
+ pathLength: keyAlias && modelId ? 3 : keyAlias ? 2 : 1
158
+ };
159
+ }
160
+ }
161
+ }
162
+ }
109
163
  return {
110
164
  target,
111
165
  decision: {
@@ -132,6 +186,24 @@ export class VirtualRouterEngine {
132
186
  handleProviderFailureImpl(event, this.healthManager, this.providerHealthConfig(), (key, ttl) => this.markProviderCooldown(key, ttl));
133
187
  }
134
188
  handleProviderError(event) {
189
+ if (this.healthStore && typeof this.healthStore.recordProviderError === 'function') {
190
+ try {
191
+ this.healthStore.recordProviderError(event);
192
+ }
193
+ catch {
194
+ // ignore persistence errors
195
+ }
196
+ }
197
+ // 配额恢复事件优先处理:一旦识别到 virtualRouterQuotaRecovery,
198
+ // 直接清理健康状态/冷却 TTL,避免继续走常规错误映射逻辑。
199
+ const handledByQuota = applyQuotaRecoveryImpl(event, this.healthManager, (key) => this.clearProviderCooldown(key), this.debug);
200
+ if (handledByQuota) {
201
+ return;
202
+ }
203
+ const handledByQuotaDepleted = applyQuotaDepletedImpl(event, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
204
+ if (handledByQuotaDepleted) {
205
+ return;
206
+ }
135
207
  applySeriesCooldownImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
136
208
  const derived = mapProviderErrorImpl(event, this.providerHealthConfig());
137
209
  if (!derived) {
@@ -262,7 +334,9 @@ export class VirtualRouterEngine {
262
334
  disabledModels: new Map(),
263
335
  stopMessageText: undefined,
264
336
  stopMessageMaxRepeats: undefined,
265
- stopMessageUsed: undefined
337
+ stopMessageUsed: undefined,
338
+ stopMessageUpdatedAt: undefined,
339
+ stopMessageLastUsedAt: undefined
266
340
  };
267
341
  }
268
342
  this.routingInstructionState.set(key, initial);
@@ -763,17 +837,28 @@ export class VirtualRouterEngine {
763
837
  const noDisabledProviders = state.disabledProviders.size === 0;
764
838
  const noDisabledKeys = state.disabledKeys.size === 0;
765
839
  const noDisabledModels = state.disabledModels.size === 0;
766
- return noForced && noSticky && noAllowed && noDisabledProviders && noDisabledKeys && noDisabledModels;
840
+ const noStopMessage = (!state.stopMessageText || !state.stopMessageText.trim()) &&
841
+ (typeof state.stopMessageMaxRepeats !== 'number' || !Number.isFinite(state.stopMessageMaxRepeats)) &&
842
+ (typeof state.stopMessageUsed !== 'number' || !Number.isFinite(state.stopMessageUsed)) &&
843
+ (typeof state.stopMessageUpdatedAt !== 'number' || !Number.isFinite(state.stopMessageUpdatedAt)) &&
844
+ (typeof state.stopMessageLastUsedAt !== 'number' || !Number.isFinite(state.stopMessageLastUsedAt));
845
+ return (noForced &&
846
+ noSticky &&
847
+ noAllowed &&
848
+ noDisabledProviders &&
849
+ noDisabledKeys &&
850
+ noDisabledModels &&
851
+ noStopMessage);
767
852
  }
768
853
  persistRoutingInstructionState(key, state) {
769
854
  if (!key || (!key.startsWith('session:') && !key.startsWith('conversation:'))) {
770
855
  return;
771
856
  }
772
857
  if (this.isRoutingStateEmpty(state)) {
773
- saveRoutingInstructionStateAsync(key, null);
858
+ this.routingStateStore.saveAsync(key, null);
774
859
  return;
775
860
  }
776
- saveRoutingInstructionStateAsync(key, state);
861
+ this.routingStateStore.saveAsync(key, state);
777
862
  }
778
863
  markProviderCooldown(providerKey, cooldownMs) {
779
864
  if (!providerKey) {
@@ -784,6 +869,15 @@ export class VirtualRouterEngine {
784
869
  return;
785
870
  }
786
871
  this.providerCooldowns.set(providerKey, Date.now() + ttl);
872
+ this.persistHealthSnapshot();
873
+ }
874
+ clearProviderCooldown(providerKey) {
875
+ if (!providerKey) {
876
+ return;
877
+ }
878
+ if (this.providerCooldowns.delete(providerKey)) {
879
+ this.persistHealthSnapshot();
880
+ }
787
881
  }
788
882
  isProviderCoolingDown(providerKey) {
789
883
  if (!providerKey) {
@@ -799,4 +893,88 @@ export class VirtualRouterEngine {
799
893
  }
800
894
  return true;
801
895
  }
896
+ restoreHealthFromStore() {
897
+ if (!this.healthStore || typeof this.healthStore.loadInitialSnapshot !== 'function') {
898
+ return;
899
+ }
900
+ let snapshot = null;
901
+ try {
902
+ snapshot = this.healthStore.loadInitialSnapshot();
903
+ }
904
+ catch {
905
+ snapshot = null;
906
+ }
907
+ if (!snapshot) {
908
+ return;
909
+ }
910
+ const now = Date.now();
911
+ const providerKeys = new Set();
912
+ for (const pools of Object.values(this.routing)) {
913
+ for (const pool of pools) {
914
+ for (const key of pool.targets) {
915
+ if (typeof key === 'string' && key) {
916
+ providerKeys.add(key);
917
+ }
918
+ }
919
+ }
920
+ }
921
+ const byKey = new Map();
922
+ for (const entry of snapshot.cooldowns || []) {
923
+ if (!entry || !entry.providerKey) {
924
+ continue;
925
+ }
926
+ if (!providerKeys.has(entry.providerKey)) {
927
+ continue;
928
+ }
929
+ if (!Number.isFinite(entry.cooldownExpiresAt) || entry.cooldownExpiresAt <= now) {
930
+ continue;
931
+ }
932
+ byKey.set(entry.providerKey, entry);
933
+ this.providerCooldowns.set(entry.providerKey, entry.cooldownExpiresAt);
934
+ }
935
+ for (const state of snapshot.providers || []) {
936
+ if (!state || !state.providerKey) {
937
+ continue;
938
+ }
939
+ if (!providerKeys.has(state.providerKey)) {
940
+ continue;
941
+ }
942
+ if (state.cooldownExpiresAt && state.cooldownExpiresAt > now) {
943
+ const ttl = state.cooldownExpiresAt - now;
944
+ if (ttl > 0) {
945
+ this.healthManager.tripProvider(state.providerKey, state.reason, ttl);
946
+ if (!byKey.has(state.providerKey)) {
947
+ this.providerCooldowns.set(state.providerKey, state.cooldownExpiresAt);
948
+ }
949
+ }
950
+ }
951
+ }
952
+ }
953
+ buildHealthSnapshot() {
954
+ const providers = this.healthManager.getSnapshot();
955
+ const cooldowns = [];
956
+ const now = Date.now();
957
+ for (const [providerKey, expiry] of this.providerCooldowns.entries()) {
958
+ if (!expiry || expiry <= now) {
959
+ continue;
960
+ }
961
+ cooldowns.push({
962
+ providerKey,
963
+ cooldownExpiresAt: expiry
964
+ });
965
+ }
966
+ return { providers, cooldowns };
967
+ }
968
+ persistHealthSnapshot() {
969
+ if (!this.healthStore || typeof this.healthStore.persistSnapshot !== 'function') {
970
+ return;
971
+ }
972
+ try {
973
+ const snapshot = this.buildHealthSnapshot();
974
+ this.healthStore.persistSnapshot(snapshot);
975
+ }
976
+ catch {
977
+ // 持久化失败不影响路由主流程
978
+ }
979
+ }
802
980
  }
@@ -31,6 +31,8 @@ export interface RoutingInstructionState {
31
31
  stopMessageText?: string;
32
32
  stopMessageMaxRepeats?: number;
33
33
  stopMessageUsed?: number;
34
+ stopMessageUpdatedAt?: number;
35
+ stopMessageLastUsedAt?: number;
34
36
  }
35
37
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
36
38
  export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
@@ -275,7 +275,9 @@ export function applyRoutingInstructions(instructions, currentState) {
275
275
  disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)])),
276
276
  stopMessageText: currentState.stopMessageText,
277
277
  stopMessageMaxRepeats: currentState.stopMessageMaxRepeats,
278
- stopMessageUsed: currentState.stopMessageUsed
278
+ stopMessageUsed: currentState.stopMessageUsed,
279
+ stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
280
+ stopMessageLastUsedAt: currentState.stopMessageLastUsedAt
279
281
  };
280
282
  let allowReset = false;
281
283
  let disableReset = false;
@@ -399,6 +401,8 @@ export function applyRoutingInstructions(instructions, currentState) {
399
401
  newState.stopMessageText = text;
400
402
  newState.stopMessageMaxRepeats = maxRepeats;
401
403
  newState.stopMessageUsed = 0;
404
+ newState.stopMessageUpdatedAt = Date.now();
405
+ newState.stopMessageLastUsedAt = undefined;
402
406
  }
403
407
  break;
404
408
  }
@@ -406,6 +410,8 @@ export function applyRoutingInstructions(instructions, currentState) {
406
410
  newState.stopMessageText = undefined;
407
411
  newState.stopMessageMaxRepeats = undefined;
408
412
  newState.stopMessageUsed = undefined;
413
+ newState.stopMessageUpdatedAt = undefined;
414
+ newState.stopMessageLastUsedAt = undefined;
409
415
  break;
410
416
  }
411
417
  }
@@ -455,6 +461,12 @@ export function serializeRoutingInstructionState(state) {
455
461
  : {}),
456
462
  ...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
457
463
  ? { stopMessageUsed: state.stopMessageUsed }
464
+ : {}),
465
+ ...(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
466
+ ? { stopMessageUpdatedAt: state.stopMessageUpdatedAt }
467
+ : {}),
468
+ ...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
469
+ ? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
458
470
  : {})
459
471
  };
460
472
  }
@@ -505,5 +517,11 @@ export function deserializeRoutingInstructionState(data) {
505
517
  if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
506
518
  state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
507
519
  }
520
+ if (typeof data.stopMessageUpdatedAt === 'number' && Number.isFinite(data.stopMessageUpdatedAt)) {
521
+ state.stopMessageUpdatedAt = data.stopMessageUpdatedAt;
522
+ }
523
+ if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
524
+ state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
525
+ }
508
526
  return state;
509
527
  }
@@ -1,5 +1,5 @@
1
1
  import type { StandardizedMessage, StandardizedRequest } from '../../conversion/hub/types/standardized.js';
2
- export type ToolCategory = 'read' | 'write' | 'search' | 'other';
2
+ export type ToolCategory = 'read' | 'write' | 'search' | 'websearch' | 'other';
3
3
  export type ToolClassification = {
4
4
  category: ToolCategory;
5
5
  name: string;
@@ -10,4 +10,5 @@ export declare function detectCodingTool(request: StandardizedRequest): boolean;
10
10
  export declare function detectWebTool(request: StandardizedRequest): boolean;
11
11
  export declare function extractMeaningfulDeclaredToolNames(tools: StandardizedRequest['tools'] | undefined): string[];
12
12
  export declare function detectLastAssistantToolCategory(messages: StandardizedMessage[]): ToolClassification | undefined;
13
+ export declare function classifyToolCallForReport(call: StandardizedMessage['tool_calls'][number]): ToolClassification | undefined;
13
14
  export declare function canonicalizeToolName(rawName: string): string;