@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,720 +1 @@
1
- const SERIES_COOLDOWN_DETAIL_KEY = 'virtualRouterSeriesCooldown';
2
- const QUOTA_RECOVERY_DETAIL_KEY = 'virtualRouterQuotaRecovery';
3
- const QUOTA_DEPLETED_DETAIL_KEY = 'virtualRouterQuotaDepleted';
4
- function parseDurationToMs(value) {
5
- if (!value || typeof value !== 'string') {
6
- return null;
7
- }
8
- const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h)/gi;
9
- let totalMs = 0;
10
- let matched = false;
11
- let match;
12
- while ((match = pattern.exec(value)) !== null) {
13
- matched = true;
14
- const amount = Number.parseFloat(match[1]);
15
- if (!Number.isFinite(amount)) {
16
- continue;
17
- }
18
- const unit = match[2].toLowerCase();
19
- if (unit === 'ms') {
20
- totalMs += amount;
21
- }
22
- else if (unit === 'h') {
23
- totalMs += amount * 3_600_000;
24
- }
25
- else if (unit === 'm') {
26
- totalMs += amount * 60_000;
27
- }
28
- else if (unit === 's') {
29
- totalMs += amount * 1_000;
30
- }
31
- }
32
- if (!matched) {
33
- const seconds = Number.parseFloat(value);
34
- if (Number.isFinite(seconds)) {
35
- totalMs = seconds * 1_000;
36
- matched = true;
37
- }
38
- }
39
- if (!matched || totalMs <= 0) {
40
- return null;
41
- }
42
- return Math.round(totalMs);
43
- }
44
- function readEnvSchedule(name, fallback) {
45
- const raw = (process.env[name] || '').trim();
46
- if (!raw) {
47
- return fallback;
48
- }
49
- const parts = raw.split(',').map((token) => token.trim()).filter(Boolean);
50
- const parsed = [];
51
- for (const part of parts) {
52
- const ms = parseDurationToMs(part);
53
- if (ms && ms > 0) {
54
- parsed.push(ms);
55
- }
56
- }
57
- return parsed.length ? parsed : fallback;
58
- }
59
- function readEnvDuration(name, fallbackMs) {
60
- const raw = (process.env[name] || '').trim();
61
- if (!raw) {
62
- return fallbackMs;
63
- }
64
- const ms = parseDurationToMs(raw);
65
- return ms && ms > 0 ? ms : fallbackMs;
66
- }
67
- /**
68
- * 对没有 quotaResetDelay 的 429 错误,在 VirtualRouter 内部维护一个简单的阶梯退避策略:
69
- * - 默认:第 1 次 5 分钟,第 2 次 1 小时,第 3 次 6 小时,第 4 次及以上 24 小时封顶;
70
- * - 可通过环境变量 ROUTECODEX_RL_SCHEDULE / RCC_RL_SCHEDULE 调整(例如 "5m,1h,6h,24h")。
71
- *
72
- * 这里的“次数”针对 providerKey 计数,并带有简单的时间窗口:若距离上次 429 超过 24 小时,则重置计数。
73
- * 该状态仅用于路由决策,不反映在 healthConfig 上,使 Host 与 VirtualRouter 对 429 处理职责清晰分层。
74
- */
75
- const NO_QUOTA_RATE_LIMIT_SCHEDULE_MS = readEnvSchedule('ROUTECODEX_RL_SCHEDULE', [
76
- 5 * 60_000,
77
- 60 * 60_000,
78
- 6 * 60 * 60_000,
79
- 24 * 60 * 60_000
80
- ]);
81
- const rateLimitBackoffByProvider = new Map();
82
- const RATE_LIMIT_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_RL_RESET_WINDOW', 24 * 60 * 60_000);
83
- function computeRateLimitCooldownMsForProvider(providerKey, now) {
84
- const prev = rateLimitBackoffByProvider.get(providerKey);
85
- let nextCount = 1;
86
- if (prev) {
87
- const elapsed = now - prev.lastAt;
88
- if (Number.isFinite(elapsed) && elapsed >= 0 && elapsed < RATE_LIMIT_RESET_WINDOW_MS) {
89
- nextCount = prev.count + 1;
90
- }
91
- }
92
- const idx = Math.min(nextCount - 1, NO_QUOTA_RATE_LIMIT_SCHEDULE_MS.length - 1);
93
- const ttl = NO_QUOTA_RATE_LIMIT_SCHEDULE_MS[idx];
94
- rateLimitBackoffByProvider.set(providerKey, { count: nextCount, lastAt: now });
95
- return ttl;
96
- }
97
- export function resetRateLimitBackoffForProvider(providerKey) {
98
- if (!providerKey) {
99
- return;
100
- }
101
- rateLimitBackoffByProvider.delete(providerKey);
102
- }
103
- const antigravityRiskBySignature = new Map();
104
- const ANTIGRAVITY_RISK_RESET_WINDOW_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_RESET_WINDOW', 30 * 60_000);
105
- const ANTIGRAVITY_RISK_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_COOLDOWN', 5 * 60_000);
106
- const ANTIGRAVITY_RISK_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_RISK_BAN', 24 * 60 * 60_000);
107
- const ANTIGRAVITY_AUTH_VERIFY_BAN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_AUTH_VERIFY_BAN', 24 * 60 * 60_000);
108
- const ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS = readEnvDuration('ROUTECODEX_ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN', 5 * 60_000);
109
- function isAntigravityEvent(event) {
110
- const runtime = event?.runtime;
111
- if (!runtime || typeof runtime !== 'object') {
112
- return false;
113
- }
114
- const providerId = typeof runtime.providerId === 'string' ? runtime.providerId.trim().toLowerCase() : '';
115
- if (providerId === 'antigravity') {
116
- return true;
117
- }
118
- const providerKey = typeof runtime.providerKey === 'string' ? runtime.providerKey.trim().toLowerCase() : '';
119
- return providerKey.startsWith('antigravity.');
120
- }
121
- function isGoogleAccountVerificationRequired(event) {
122
- const sources = [];
123
- const message = typeof event.message === 'string' ? event.message : '';
124
- if (message)
125
- sources.push(message);
126
- const details = event.details;
127
- if (details && typeof details === 'object' && !Array.isArray(details)) {
128
- const upstreamMessage = details.upstreamMessage;
129
- if (typeof upstreamMessage === 'string' && upstreamMessage.trim()) {
130
- sources.push(upstreamMessage);
131
- }
132
- const meta = details.meta;
133
- if (meta && typeof meta === 'object' && !Array.isArray(meta)) {
134
- const metaUpstream = meta.upstreamMessage;
135
- if (typeof metaUpstream === 'string' && metaUpstream.trim()) {
136
- sources.push(metaUpstream);
137
- }
138
- const metaMessage = meta.message;
139
- if (typeof metaMessage === 'string' && metaMessage.trim()) {
140
- sources.push(metaMessage);
141
- }
142
- }
143
- }
144
- if (sources.length === 0) {
145
- return false;
146
- }
147
- const lowered = sources.join(' | ').toLowerCase();
148
- return (lowered.includes('verify your account') ||
149
- // Antigravity-Manager alignment: 403 validation gating keywords.
150
- lowered.includes('validation_required') ||
151
- lowered.includes('validation required') ||
152
- lowered.includes('validation_url') ||
153
- lowered.includes('validation url') ||
154
- lowered.includes('accounts.google.com/signin/continue') ||
155
- lowered.includes('support.google.com/accounts?p=al_alert'));
156
- }
157
- function resolveAntigravityRuntimeKey(event) {
158
- const runtime = event.runtime;
159
- if (!runtime || typeof runtime !== 'object') {
160
- return null;
161
- }
162
- const target = runtime.target && typeof runtime.target === 'object' ? runtime.target : null;
163
- const byTarget = target && typeof target.runtimeKey === 'string' && target.runtimeKey.trim() ? target.runtimeKey.trim() : '';
164
- if (byTarget) {
165
- return byTarget;
166
- }
167
- const providerKey = (typeof runtime.providerKey === 'string' && runtime.providerKey.trim() ? runtime.providerKey.trim() : '') ||
168
- (target && typeof target.providerKey === 'string' && String(target.providerKey).trim()
169
- ? String(target.providerKey).trim()
170
- : '');
171
- if (!providerKey) {
172
- return null;
173
- }
174
- const parts = providerKey.split('.').filter(Boolean);
175
- if (parts.length < 2) {
176
- return null;
177
- }
178
- return `${parts[0]}.${parts[1]}`;
179
- }
180
- function shouldTriggerAntigravityRiskPolicy(event) {
181
- const status = typeof event.status === 'number' ? event.status : undefined;
182
- if (typeof status === 'number' && Number.isFinite(status)) {
183
- // Focus on "dirty request" / auth / permission class issues. Avoid 429 which already has backoff.
184
- return status >= 400 && status < 500 && status !== 429;
185
- }
186
- // If status is missing, fall back to known HTTP-ish error codes.
187
- return typeof event.code === 'string' && /^HTTP_4\d\d$/.test(event.code.trim());
188
- }
189
- function computeAntigravityRiskSignature(event) {
190
- const status = typeof event.status === 'number' && Number.isFinite(event.status) ? String(event.status) : '';
191
- const code = typeof event.code === 'string' && event.code.trim() ? event.code.trim() : '';
192
- const stage = typeof event.stage === 'string' && event.stage.trim() ? event.stage.trim() : '';
193
- const parts = [status, code, stage].filter((p) => p.length > 0);
194
- return parts.length ? parts.join(':') : 'unknown';
195
- }
196
- function isAntigravityThoughtSignatureMissing(event) {
197
- const code = typeof event.code === 'string' ? event.code.trim().toLowerCase() : '';
198
- if (code.includes('thought') && code.includes('signature')) {
199
- return true;
200
- }
201
- const sources = [];
202
- const message = typeof event.message === 'string' ? event.message : '';
203
- if (message)
204
- sources.push(message);
205
- const details = event.details;
206
- if (details && typeof details === 'object' && !Array.isArray(details)) {
207
- const upstreamMessage = details.upstreamMessage;
208
- if (typeof upstreamMessage === 'string' && upstreamMessage.trim()) {
209
- sources.push(upstreamMessage);
210
- }
211
- const meta = details.meta;
212
- if (meta && typeof meta === 'object' && !Array.isArray(meta)) {
213
- const metaUpstream = meta.upstreamMessage;
214
- if (typeof metaUpstream === 'string' && metaUpstream.trim()) {
215
- sources.push(metaUpstream);
216
- }
217
- const metaMessage = meta.message;
218
- if (typeof metaMessage === 'string' && metaMessage.trim()) {
219
- sources.push(metaMessage);
220
- }
221
- }
222
- }
223
- if (sources.length === 0) {
224
- return false;
225
- }
226
- for (const source of sources) {
227
- const lowered = source.toLowerCase();
228
- // Match both "thoughtSignature" and "thought signature" variants, as well as "reasoning signature".
229
- const mentionsSignature = lowered.includes('thoughtsignature') ||
230
- lowered.includes('thought signature') ||
231
- lowered.includes('reasoning_signature') ||
232
- lowered.includes('reasoning signature');
233
- if (!mentionsSignature) {
234
- continue;
235
- }
236
- // Match common failure patterns in upstream error messages.
237
- if (lowered.includes('missing') ||
238
- lowered.includes('required') ||
239
- lowered.includes('invalid') ||
240
- lowered.includes('not provided') ||
241
- lowered.includes('签名') ||
242
- lowered.includes('缺少') ||
243
- lowered.includes('无效')) {
244
- return true;
245
- }
246
- }
247
- return false;
248
- }
249
- function shouldApplyGeminiSeriesSignatureFreeze(event) {
250
- const status = typeof event.status === 'number' ? event.status : undefined;
251
- // Signature missing is a request-shape contract failure; treat as 4xx.
252
- if (status !== undefined && status >= 400 && status < 500) {
253
- return true;
254
- }
255
- const code = typeof event.code === 'string' ? event.code.trim().toUpperCase() : '';
256
- return /^HTTP_4\d\d$/.test(code) || code.includes('INVALID_ARGUMENT') || code.includes('FAILED_PRECONDITION');
257
- }
258
- export function applyAntigravityRiskPolicyImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
259
- if (!event) {
260
- return;
261
- }
262
- if (!isAntigravityEvent(event)) {
263
- return;
264
- }
265
- if (!shouldTriggerAntigravityRiskPolicy(event)) {
266
- return;
267
- }
268
- const runtimeKey = resolveAntigravityRuntimeKey(event);
269
- const verificationRequired = Boolean(runtimeKey) && isGoogleAccountVerificationRequired(event);
270
- // Account verification errors are per-account and must be handled immediately:
271
- // - blacklist only the affected runtimeKey (do NOT penalize other Antigravity accounts)
272
- // - require user to complete OAuth verification to recover
273
- if (verificationRequired && runtimeKey) {
274
- const providerKeys = providerRegistry
275
- .listProviderKeys('antigravity')
276
- .filter((key) => typeof key === 'string' && key.startsWith(`${runtimeKey}.`));
277
- if (providerKeys.length === 0) {
278
- return;
279
- }
280
- for (const key of providerKeys) {
281
- try {
282
- if (!healthManager.isAvailable(key)) {
283
- continue;
284
- }
285
- healthManager.tripProvider(key, 'auth_verify', ANTIGRAVITY_AUTH_VERIFY_BAN_MS);
286
- markProviderCooldown(key, ANTIGRAVITY_AUTH_VERIFY_BAN_MS);
287
- }
288
- catch {
289
- // ignore lookup failures
290
- }
291
- }
292
- debug?.log?.('[virtual-router] antigravity auth verify blacklist', {
293
- runtimeKey,
294
- cooldownMs: ANTIGRAVITY_AUTH_VERIFY_BAN_MS,
295
- affected: providerKeys
296
- });
297
- return;
298
- }
299
- // Thought signature missing is a deterministic incompatibility for Gemini-family flows that require it.
300
- // If we keep routing into Antigravity Gemini models without a signature, we create a request storm.
301
- // Freeze the *Gemini* series (pro/flash) immediately so the router falls back to non-Antigravity providers.
302
- if (isAntigravityThoughtSignatureMissing(event) && shouldApplyGeminiSeriesSignatureFreeze(event)) {
303
- const allProviderKeys = providerRegistry.listProviderKeys('antigravity');
304
- const providerKeys = runtimeKey
305
- ? allProviderKeys.filter((key) => typeof key === 'string' && key.startsWith(`${runtimeKey}.`))
306
- : allProviderKeys;
307
- const affected = [];
308
- for (const key of providerKeys) {
309
- try {
310
- if (!healthManager.isAvailable(key)) {
311
- continue;
312
- }
313
- const profile = providerRegistry.get(key);
314
- const modelSeries = resolveModelSeries(profile.modelId);
315
- if (modelSeries !== 'gemini-pro' && modelSeries !== 'gemini-flash') {
316
- continue;
317
- }
318
- healthManager.tripProvider(key, 'signature_missing', ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS);
319
- markProviderCooldown(key, ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS);
320
- affected.push(key);
321
- }
322
- catch {
323
- // ignore lookup failures
324
- }
325
- }
326
- if (affected.length) {
327
- debug?.log?.('[virtual-router] antigravity thoughtSignature missing: freeze gemini series', {
328
- ...(runtimeKey ? { runtimeKey } : {}),
329
- cooldownMs: ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS,
330
- affected
331
- });
332
- }
333
- return;
334
- }
335
- const signature = computeAntigravityRiskSignature(event);
336
- if (!signature || signature === 'unknown') {
337
- return;
338
- }
339
- // Antigravity-Manager alignment: account verification errors are per-account (runtimeKey) and should not
340
- // penalize all other Antigravity accounts. Request-shape/auth policy failures are still treated globally.
341
- const runtimeScoped = Boolean(runtimeKey) && isGoogleAccountVerificationRequired(event);
342
- const riskKey = runtimeScoped ? `${signature}|${runtimeKey}` : signature;
343
- const now = Date.now();
344
- const prev = antigravityRiskBySignature.get(riskKey);
345
- let count = 1;
346
- if (prev) {
347
- const elapsed = now - prev.lastAt;
348
- if (Number.isFinite(elapsed) && elapsed >= 0 && elapsed < ANTIGRAVITY_RISK_RESET_WINDOW_MS) {
349
- count = prev.count + 1;
350
- }
351
- }
352
- antigravityRiskBySignature.set(riskKey, { count, lastAt: now });
353
- // Escalation ladder (Antigravity account safety):
354
- // 1) First/second occurrence: normal retry/fallback logic handles per-request behavior.
355
- // 2) Third occurrence: cooldown Antigravity providerKeys for 5 minutes (scoped for account verification errors).
356
- // 3) Fourth+ occurrence: effectively remove Antigravity from routing (long ban window; scoped for account verification errors).
357
- const allProviderKeys = providerRegistry.listProviderKeys('antigravity');
358
- const providerKeys = runtimeScoped
359
- ? allProviderKeys.filter((key) => {
360
- return typeof key === 'string' && runtimeKey ? key.startsWith(`${runtimeKey}.`) : false;
361
- })
362
- : allProviderKeys;
363
- if (providerKeys.length === 0) {
364
- return;
365
- }
366
- if (count === 3) {
367
- for (const key of providerKeys) {
368
- try {
369
- healthManager.tripProvider(key, 'risk_cooldown', ANTIGRAVITY_RISK_COOLDOWN_MS);
370
- markProviderCooldown(key, ANTIGRAVITY_RISK_COOLDOWN_MS);
371
- }
372
- catch {
373
- // ignore lookup failures
374
- }
375
- }
376
- debug?.log?.('[virtual-router] antigravity risk cooldown', {
377
- signature,
378
- ...(runtimeScoped ? { runtimeKey } : {}),
379
- count,
380
- cooldownMs: ANTIGRAVITY_RISK_COOLDOWN_MS,
381
- affected: providerKeys
382
- });
383
- }
384
- else if (count >= 4) {
385
- const ttl = Math.max(ANTIGRAVITY_RISK_BAN_MS, ANTIGRAVITY_RISK_COOLDOWN_MS);
386
- for (const key of providerKeys) {
387
- try {
388
- healthManager.tripProvider(key, 'risk_blacklist', ttl);
389
- markProviderCooldown(key, ttl);
390
- }
391
- catch {
392
- // ignore lookup failures
393
- }
394
- }
395
- debug?.log?.('[virtual-router] antigravity risk blacklist', {
396
- signature,
397
- ...(runtimeScoped ? { runtimeKey } : {}),
398
- count,
399
- cooldownMs: ttl,
400
- affected: providerKeys
401
- });
402
- }
403
- }
404
- export function handleProviderFailureImpl(event, healthManager, healthConfig, markProviderCooldown) {
405
- if (!event || !event.providerKey) {
406
- return;
407
- }
408
- if (event.affectsHealth === false) {
409
- return;
410
- }
411
- if (event.fatal) {
412
- healthManager.tripProvider(event.providerKey, event.reason, event.cooldownOverrideMs);
413
- }
414
- else if (event.reason === 'rate_limit' && event.statusCode === 429) {
415
- // 对非致命的 429 错误:
416
- // - 若 ProviderErrorEvent 已携带显式 cooldownOverrideMs(例如来自 quotaResetDelay),则直接使用;
417
- // - 否则针对该 providerKey 启用阶梯退避策略(5min → 1h → 6h → 24h),
418
- // 在冷却期内从路由池中移除该 alias,避免持续命中上游。
419
- const providerKey = event.providerKey;
420
- let ttl = event.cooldownOverrideMs;
421
- if (!ttl || !Number.isFinite(ttl) || ttl <= 0) {
422
- ttl = computeRateLimitCooldownMsForProvider(providerKey, Date.now());
423
- }
424
- healthManager.cooldownProvider(providerKey, event.reason, ttl);
425
- markProviderCooldown(providerKey, ttl);
426
- }
427
- else {
428
- healthManager.recordFailure(event.providerKey, event.reason);
429
- }
430
- }
431
- export function mapProviderErrorImpl(event, healthConfig) {
432
- if (!event || !event.runtime) {
433
- return null;
434
- }
435
- const runtime = event.runtime;
436
- const providerKey = runtime.providerKey ||
437
- (runtime.target && typeof runtime.target === 'object'
438
- ? runtime.target.providerKey
439
- : undefined);
440
- if (!providerKey) {
441
- return null;
442
- }
443
- const routeName = runtime.routeName;
444
- const statusCode = event.status;
445
- const code = event.code?.toUpperCase() ?? 'ERR_UNKNOWN';
446
- const stage = event.stage?.toLowerCase() ?? 'unknown';
447
- const recoverable = event.recoverable === true;
448
- const providerFamily = runtime.providerFamily &&
449
- typeof runtime.providerFamily === 'string'
450
- ? runtime.providerFamily
451
- : undefined;
452
- const providerId = runtime.providerId &&
453
- typeof runtime.providerId === 'string'
454
- ? runtime.providerId
455
- : undefined;
456
- const providerTag = (providerFamily || providerId || '').toLowerCase();
457
- const isOAuthAuth406 = statusCode === 406 && (providerTag === 'iflow' || providerTag === 'qwen');
458
- let fatal = !recoverable;
459
- let reason = deriveReason(code, stage, statusCode);
460
- let cooldownOverrideMs;
461
- if (statusCode === 401 || statusCode === 402 || statusCode === 403 || code.includes('AUTH') || isOAuthAuth406) {
462
- fatal = true;
463
- cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
464
- reason = 'auth';
465
- }
466
- else if (statusCode === 429 && !recoverable) {
467
- fatal = true;
468
- cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
469
- reason = 'rate_limit';
470
- }
471
- else if (statusCode && statusCode >= 500) {
472
- fatal = true;
473
- cooldownOverrideMs = Math.max(5 * 60_000, healthConfig.fatalCooldownMs ?? 5 * 60_000);
474
- reason = 'upstream_error';
475
- }
476
- else if (stage.includes('compat')) {
477
- fatal = true;
478
- cooldownOverrideMs = Math.max(10 * 60_000, healthConfig.fatalCooldownMs ?? 10 * 60_000);
479
- reason = 'compatibility';
480
- }
481
- return {
482
- providerKey,
483
- routeName,
484
- reason,
485
- fatal,
486
- statusCode,
487
- errorCode: code,
488
- retryable: recoverable,
489
- affectsHealth: event.affectsHealth !== false,
490
- cooldownOverrideMs,
491
- metadata: {
492
- ...event.runtime,
493
- stage,
494
- eventCode: code,
495
- originalMessage: event.message,
496
- statusCode
497
- }
498
- };
499
- }
500
- export function applySeriesCooldownImpl(event, providerRegistry, healthManager, markProviderCooldown, debug) {
501
- const seriesDetail = extractSeriesCooldownDetail(event);
502
- if (!seriesDetail) {
503
- return;
504
- }
505
- const targetKeys = resolveSeriesCooldownTargets(seriesDetail, event, providerRegistry);
506
- if (targetKeys.length === 0) {
507
- debug?.log?.('[virtual-router] series cooldown skipped: no targets', {
508
- providerId: seriesDetail.providerId,
509
- providerKey: seriesDetail.providerKey,
510
- series: seriesDetail.series
511
- });
512
- return;
513
- }
514
- const affected = [];
515
- for (const providerKey of targetKeys) {
516
- try {
517
- const profile = providerRegistry.get(providerKey);
518
- const modelSeries = resolveModelSeries(profile.modelId);
519
- if (modelSeries !== seriesDetail.series) {
520
- continue;
521
- }
522
- healthManager.tripProvider(providerKey, 'rate_limit', seriesDetail.cooldownMs);
523
- markProviderCooldown(providerKey, seriesDetail.cooldownMs);
524
- affected.push(providerKey);
525
- }
526
- catch {
527
- // ignore lookup failures; invalid keys may show up if config drifted
528
- }
529
- }
530
- if (affected.length) {
531
- debug?.log?.('[virtual-router] series cooldown', {
532
- providerId: seriesDetail.providerId,
533
- providerKey: seriesDetail.providerKey,
534
- series: seriesDetail.series,
535
- cooldownMs: seriesDetail.cooldownMs,
536
- affected
537
- });
538
- }
539
- }
540
- function extractQuotaRecoveryDetail(event) {
541
- if (!event || !event.details || typeof event.details !== 'object') {
542
- return null;
543
- }
544
- const raw = event.details[QUOTA_RECOVERY_DETAIL_KEY];
545
- if (!raw || typeof raw !== 'object') {
546
- return null;
547
- }
548
- const record = raw;
549
- const providerKeyRaw = record.providerKey;
550
- if (typeof providerKeyRaw !== 'string' || !providerKeyRaw.trim()) {
551
- return null;
552
- }
553
- const reason = typeof record.reason === 'string' && record.reason.trim()
554
- ? record.reason.trim()
555
- : undefined;
556
- return {
557
- providerKey: providerKeyRaw.trim(),
558
- reason
559
- };
560
- }
561
- /**
562
- * 处理来自 Host 侧的配额恢复事件:
563
- * - 清除指定 providerKey 在健康管理器中的熔断/冷却状态;
564
- * - 清理对应的速率退避计数;
565
- * - 调用调用方提供的 clearProviderCooldown 回调移除显式 cooldown TTL。
566
- *
567
- * 返回值表示是否已处理(true=已处理且后续应跳过常规错误映射逻辑)。
568
- */
569
- export function applyQuotaRecoveryImpl(event, healthManager, clearProviderCooldown, debug) {
570
- const detail = extractQuotaRecoveryDetail(event);
571
- if (!detail) {
572
- return false;
573
- }
574
- const providerKey = detail.providerKey;
575
- try {
576
- healthManager.recordSuccess(providerKey);
577
- resetRateLimitBackoffForProvider(providerKey);
578
- clearProviderCooldown(providerKey);
579
- }
580
- catch {
581
- // 恢复失败不得影响主路由流程
582
- }
583
- return true;
584
- }
585
- function extractQuotaDepletedDetail(event) {
586
- if (!event || !event.details || typeof event.details !== 'object') {
587
- return null;
588
- }
589
- const raw = event.details[QUOTA_DEPLETED_DETAIL_KEY];
590
- if (!raw || typeof raw !== 'object') {
591
- return null;
592
- }
593
- const record = raw;
594
- const providerKeyRaw = record.providerKey;
595
- if (typeof providerKeyRaw !== 'string' || !providerKeyRaw.trim()) {
596
- return null;
597
- }
598
- const cooldownMs = typeof record.cooldownMs === 'number' && Number.isFinite(record.cooldownMs) && record.cooldownMs > 0
599
- ? record.cooldownMs
600
- : undefined;
601
- const reason = typeof record.reason === 'string' && record.reason.trim()
602
- ? record.reason.trim()
603
- : undefined;
604
- return {
605
- providerKey: providerKeyRaw.trim(),
606
- cooldownMs,
607
- reason
608
- };
609
- }
610
- export function applyQuotaDepletedImpl(event, healthManager, markProviderCooldown, debug) {
611
- const detail = extractQuotaDepletedDetail(event);
612
- if (!detail) {
613
- return false;
614
- }
615
- const ttl = detail.cooldownMs;
616
- try {
617
- healthManager.cooldownProvider(detail.providerKey, 'rate_limit', ttl);
618
- markProviderCooldown(detail.providerKey, ttl);
619
- debug?.log?.('[virtual-router] quota depleted', {
620
- providerKey: detail.providerKey,
621
- cooldownMs: ttl,
622
- reason: detail.reason
623
- });
624
- }
625
- catch {
626
- // ignore failures
627
- }
628
- return true;
629
- }
630
- function resolveSeriesCooldownTargets(detail, event, providerRegistry) {
631
- const candidates = new Set();
632
- const push = (key) => {
633
- if (typeof key !== 'string') {
634
- return;
635
- }
636
- const trimmed = key.trim();
637
- if (!trimmed) {
638
- return;
639
- }
640
- if (providerRegistry.has(trimmed)) {
641
- candidates.add(trimmed);
642
- }
643
- };
644
- push(detail.providerKey);
645
- const runtimeKey = (event.runtime?.target && typeof event.runtime.target === 'object'
646
- ? event.runtime.target.providerKey
647
- : undefined) || event.runtime?.providerKey;
648
- push(runtimeKey);
649
- return Array.from(candidates);
650
- }
651
- function extractSeriesCooldownDetail(event) {
652
- if (!event || !event.details || typeof event.details !== 'object') {
653
- return null;
654
- }
655
- const raw = event.details[SERIES_COOLDOWN_DETAIL_KEY];
656
- if (!raw || typeof raw !== 'object') {
657
- return null;
658
- }
659
- const record = raw;
660
- const providerIdRaw = record.providerId;
661
- const seriesRaw = record.series;
662
- const providerKeyRaw = record.providerKey;
663
- const cooldownRaw = record.cooldownMs;
664
- if (typeof providerIdRaw !== 'string' || !providerIdRaw.trim()) {
665
- return null;
666
- }
667
- const normalizedSeries = typeof seriesRaw === 'string' ? seriesRaw.trim().toLowerCase() : '';
668
- if (normalizedSeries !== 'gemini-pro' && normalizedSeries !== 'gemini-flash' && normalizedSeries !== 'claude') {
669
- return null;
670
- }
671
- const cooldownMs = typeof cooldownRaw === 'number'
672
- ? cooldownRaw
673
- : typeof cooldownRaw === 'string'
674
- ? Number.parseFloat(cooldownRaw)
675
- : Number.NaN;
676
- if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
677
- return null;
678
- }
679
- return {
680
- providerId: providerIdRaw.trim(),
681
- ...(typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length
682
- ? { providerKey: providerKeyRaw.trim() }
683
- : {}),
684
- series: normalizedSeries,
685
- cooldownMs: Math.round(cooldownMs)
686
- };
687
- }
688
- export function deriveReason(code, stage, statusCode) {
689
- if (code.includes('RATE') || code.includes('429'))
690
- return 'rate_limit';
691
- if (code.includes('AUTH') || statusCode === 401 || statusCode === 403)
692
- return 'auth';
693
- if (stage.includes('compat'))
694
- return 'compatibility';
695
- if (code.includes('SSE'))
696
- return 'sse';
697
- if (code.includes('TIMEOUT') || statusCode === 408 || statusCode === 504)
698
- return 'timeout';
699
- if (statusCode && statusCode >= 500)
700
- return 'upstream_error';
701
- if (statusCode && statusCode >= 400)
702
- return 'client_error';
703
- return 'unknown';
704
- }
705
- function resolveModelSeries(modelId) {
706
- if (!modelId) {
707
- return 'default';
708
- }
709
- const lower = modelId.toLowerCase();
710
- if (lower.includes('claude') || lower.includes('opus')) {
711
- return 'claude';
712
- }
713
- if (lower.includes('flash')) {
714
- return 'gemini-flash';
715
- }
716
- if (lower.includes('gemini') || lower.includes('pro')) {
717
- return 'gemini-pro';
718
- }
719
- return 'default';
720
- }
1
+ export { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl, resetRateLimitBackoffForProvider, deriveReason } from './engine/health/index.js';