@jsonstudio/llms 0.6.1449 → 0.6.1643
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.
- package/dist/conversion/codecs/gemini-openai-codec.js +6 -1
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.d.ts +4 -6
- package/dist/conversion/compat/actions/anthropic-claude-code-system-prompt.js +179 -41
- package/dist/conversion/compat/actions/antigravity-thought-signature-cache.js +73 -14
- package/dist/conversion/compat/actions/antigravity-thought-signature-prepare.js +165 -10
- package/dist/conversion/compat/actions/gemini-cli-request.js +72 -13
- package/dist/conversion/compat/antigravity-session-signature.d.ts +68 -1
- package/dist/conversion/compat/antigravity-session-signature.js +833 -21
- package/dist/conversion/compat/profiles/anthropic-claude-code.json +17 -0
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +1 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +33 -8
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +17 -1
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +12 -3
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +20 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +26 -1
- package/dist/conversion/hub/process/chat-process.js +300 -67
- package/dist/conversion/hub/response/provider-response.js +4 -3
- package/dist/conversion/shared/gemini-tool-utils.js +134 -9
- package/dist/conversion/shared/text-markup-normalizer.js +90 -1
- package/dist/conversion/shared/thought-signature-validator.d.ts +1 -1
- package/dist/conversion/shared/thought-signature-validator.js +2 -1
- package/dist/quota/apikey-reset.d.ts +17 -0
- package/dist/quota/apikey-reset.js +43 -0
- package/dist/quota/index.d.ts +2 -0
- package/dist/quota/index.js +1 -0
- package/dist/quota/quota-manager.d.ts +44 -0
- package/dist/quota/quota-manager.js +491 -0
- package/dist/quota/quota-state.d.ts +6 -0
- package/dist/quota/quota-state.js +167 -0
- package/dist/quota/types.d.ts +61 -0
- package/dist/quota/types.js +1 -0
- package/dist/router/virtual-router/bootstrap.js +103 -6
- package/dist/router/virtual-router/engine-health.js +104 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +1 -2
- package/dist/router/virtual-router/engine-selection/tier-priority.js +2 -2
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +34 -10
- package/dist/router/virtual-router/engine-selection/tier-selection.js +250 -6
- package/dist/router/virtual-router/engine-selection.js +2 -2
- package/dist/router/virtual-router/engine.d.ts +16 -1
- package/dist/router/virtual-router/engine.js +320 -42
- package/dist/router/virtual-router/features.js +20 -2
- package/dist/router/virtual-router/success-center.d.ts +10 -0
- package/dist/router/virtual-router/success-center.js +32 -0
- package/dist/router/virtual-router/types.d.ts +48 -0
- package/dist/servertool/clock/config.d.ts +2 -0
- package/dist/servertool/clock/config.js +10 -2
- package/dist/servertool/clock/daemon.js +3 -0
- package/dist/servertool/clock/ntp.d.ts +18 -0
- package/dist/servertool/clock/ntp.js +318 -0
- package/dist/servertool/clock/paths.d.ts +1 -0
- package/dist/servertool/clock/paths.js +3 -0
- package/dist/servertool/clock/state.d.ts +2 -0
- package/dist/servertool/clock/state.js +15 -2
- package/dist/servertool/clock/tasks.d.ts +1 -0
- package/dist/servertool/clock/tasks.js +24 -1
- package/dist/servertool/clock/types.d.ts +21 -0
- package/dist/servertool/engine.js +105 -1
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.d.ts +1 -0
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +201 -0
- package/dist/servertool/handlers/clock-auto.js +39 -4
- package/dist/servertool/handlers/clock.js +145 -16
- package/dist/servertool/handlers/followup-request-builder.js +84 -0
- package/dist/servertool/handlers/stop-message-auto.js +1 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +1 -0
- package/dist/servertool/types.d.ts +2 -0
- package/dist/tools/apply-patch/execution-capturer.js +24 -3
- package/package.json +3 -2
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { computeNextDailyResetAtMs } from './apikey-reset.js';
|
|
2
|
+
import { applyErrorEvent, applySuccessEvent, createInitialQuotaState, tickQuotaStateTime } from './quota-state.js';
|
|
3
|
+
function safeTrim(value) {
|
|
4
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
5
|
+
}
|
|
6
|
+
function readEnvDurationMs(envKey, fallbackMs) {
|
|
7
|
+
const raw = String(process.env[envKey] ?? '').trim();
|
|
8
|
+
if (!raw)
|
|
9
|
+
return fallbackMs;
|
|
10
|
+
const n = Number(raw);
|
|
11
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
12
|
+
return fallbackMs;
|
|
13
|
+
return Math.floor(n);
|
|
14
|
+
}
|
|
15
|
+
const ANTIGRAVITY_AUTH_VERIFY_BAN_MS = readEnvDurationMs('ROUTECODEX_ANTIGRAVITY_AUTH_VERIFY_BAN', 24 * 60 * 60_000);
|
|
16
|
+
const ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS = readEnvDurationMs('ROUTECODEX_ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN', 5 * 60_000);
|
|
17
|
+
function parseProviderKey(ev) {
|
|
18
|
+
const key = safeTrim(ev?.providerKey);
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
function parseHttpStatus(ev) {
|
|
22
|
+
const status = ev?.status;
|
|
23
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
24
|
+
return status;
|
|
25
|
+
}
|
|
26
|
+
const runtime = ev?.runtime;
|
|
27
|
+
const maybe = runtime && typeof runtime === 'object' ? runtime.status : null;
|
|
28
|
+
if (typeof maybe === 'number' && Number.isFinite(maybe)) {
|
|
29
|
+
return maybe;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function extractResetAtIso(details) {
|
|
34
|
+
if (!details || typeof details !== 'object') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const v = details.resetAt;
|
|
38
|
+
const raw = typeof v === 'string' ? v.trim() : '';
|
|
39
|
+
if (raw) {
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
const meta = details.meta && typeof details.meta === 'object' && !Array.isArray(details.meta)
|
|
43
|
+
? details.meta
|
|
44
|
+
: null;
|
|
45
|
+
const metaResetAt = meta && typeof meta.resetAt === 'string' ? String(meta.resetAt).trim() : '';
|
|
46
|
+
if (metaResetAt) {
|
|
47
|
+
return metaResetAt;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function extractResetAtIsoFromText(text) {
|
|
52
|
+
const raw = typeof text === 'string' ? text.trim() : '';
|
|
53
|
+
if (!raw) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Try to parse an embedded JSON payload (common in provider error messages).
|
|
57
|
+
const firstBrace = raw.indexOf('{');
|
|
58
|
+
const lastBrace = raw.lastIndexOf('}');
|
|
59
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
60
|
+
const jsonCandidate = raw.slice(firstBrace, lastBrace + 1);
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(jsonCandidate);
|
|
63
|
+
const direct = parsed && typeof parsed.resetAt === 'string' ? String(parsed.resetAt).trim() : '';
|
|
64
|
+
if (direct) {
|
|
65
|
+
return direct;
|
|
66
|
+
}
|
|
67
|
+
const nested = parsed && parsed.error && typeof parsed.error === 'object' ? parsed.error : null;
|
|
68
|
+
const nestedResetAt = nested && typeof nested.resetAt === 'string' ? String(nested.resetAt).trim() : '';
|
|
69
|
+
if (nestedResetAt) {
|
|
70
|
+
return nestedResetAt;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// ignore JSON parse failures; fall back to regex matching
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const m = raw.match(/"resetAt"\s*:\s*"([^"]+)"/i) ||
|
|
78
|
+
raw.match(/"reset_at"\s*:\s*"([^"]+)"/i) ||
|
|
79
|
+
raw.match(/resetAt\s*[:=]\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2}T[^"\s]+)"?/i) ||
|
|
80
|
+
raw.match(/reset_at\s*[:=]\s*"?([0-9]{4}-[0-9]{2}-[0-9]{2}T[^"\s]+)"?/i);
|
|
81
|
+
const captured = m && typeof m[1] === 'string' ? m[1].trim() : '';
|
|
82
|
+
return captured ? captured : null;
|
|
83
|
+
}
|
|
84
|
+
function extractResetAtIsoFromSources(sources) {
|
|
85
|
+
for (const source of sources) {
|
|
86
|
+
const candidate = extractResetAtIsoFromText(source);
|
|
87
|
+
if (candidate) {
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function collectUpstreamErrorSources(ev) {
|
|
94
|
+
const sources = [];
|
|
95
|
+
const msg = typeof ev.message === 'string' ? ev.message : '';
|
|
96
|
+
if (msg)
|
|
97
|
+
sources.push(msg);
|
|
98
|
+
const details = ev.details && typeof ev.details === 'object' && !Array.isArray(ev.details) ? ev.details : null;
|
|
99
|
+
if (details) {
|
|
100
|
+
const upstreamMessage = typeof details.upstreamMessage === 'string' ? details.upstreamMessage : '';
|
|
101
|
+
if (upstreamMessage && upstreamMessage.trim())
|
|
102
|
+
sources.push(upstreamMessage);
|
|
103
|
+
const meta = details.meta && typeof details.meta === 'object' && !Array.isArray(details.meta) ? details.meta : null;
|
|
104
|
+
if (meta) {
|
|
105
|
+
const metaUpstream = typeof meta.upstreamMessage === 'string' ? meta.upstreamMessage : '';
|
|
106
|
+
const metaMessage = typeof meta.message === 'string' ? meta.message : '';
|
|
107
|
+
if (metaUpstream && metaUpstream.trim())
|
|
108
|
+
sources.push(metaUpstream);
|
|
109
|
+
if (metaMessage && metaMessage.trim())
|
|
110
|
+
sources.push(metaMessage);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return sources;
|
|
114
|
+
}
|
|
115
|
+
function isAntigravityProviderKey(providerKey) {
|
|
116
|
+
return providerKey.toLowerCase().startsWith('antigravity.');
|
|
117
|
+
}
|
|
118
|
+
function resolveAntigravityRuntimeKey(providerKey, ev) {
|
|
119
|
+
const rtKey = safeTrim(ev?.runtime?.target?.runtimeKey);
|
|
120
|
+
if (rtKey)
|
|
121
|
+
return rtKey;
|
|
122
|
+
const parts = providerKey.split('.').filter(Boolean);
|
|
123
|
+
if (parts.length >= 2) {
|
|
124
|
+
return `${parts[0]}.${parts[1]}`;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function isGoogleAccountVerificationRequired(ev, sources) {
|
|
129
|
+
if (!sources.length) {
|
|
130
|
+
sources = collectUpstreamErrorSources(ev);
|
|
131
|
+
}
|
|
132
|
+
const lowered = sources.join(' | ').toLowerCase();
|
|
133
|
+
return (lowered.includes('verify your account') ||
|
|
134
|
+
lowered.includes('validation_required') ||
|
|
135
|
+
lowered.includes('validation required') ||
|
|
136
|
+
lowered.includes('validation_url') ||
|
|
137
|
+
lowered.includes('validation url') ||
|
|
138
|
+
lowered.includes('accounts.google.com/signin/continue') ||
|
|
139
|
+
lowered.includes('support.google.com/accounts?p=al_alert'));
|
|
140
|
+
}
|
|
141
|
+
function isThoughtSignatureMissing(ev, sources) {
|
|
142
|
+
const code = typeof ev.code === 'string' ? ev.code.trim().toLowerCase() : '';
|
|
143
|
+
if (code.includes('thought') && code.includes('signature')) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (!sources.length) {
|
|
147
|
+
sources = collectUpstreamErrorSources(ev);
|
|
148
|
+
}
|
|
149
|
+
for (const source of sources) {
|
|
150
|
+
const lowered = source.toLowerCase();
|
|
151
|
+
const mentionsSignature = lowered.includes('thoughtsignature') ||
|
|
152
|
+
lowered.includes('thought signature') ||
|
|
153
|
+
lowered.includes('reasoning_signature') ||
|
|
154
|
+
lowered.includes('reasoning signature');
|
|
155
|
+
if (!mentionsSignature)
|
|
156
|
+
continue;
|
|
157
|
+
if (lowered.includes('missing') ||
|
|
158
|
+
lowered.includes('required') ||
|
|
159
|
+
lowered.includes('invalid') ||
|
|
160
|
+
lowered.includes('not provided') ||
|
|
161
|
+
lowered.includes('签名') ||
|
|
162
|
+
lowered.includes('缺少') ||
|
|
163
|
+
lowered.includes('无效')) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function extractFirstUrl(sources) {
|
|
170
|
+
for (const source of sources) {
|
|
171
|
+
const m = String(source || '').match(/https?:\/\/\S+/);
|
|
172
|
+
if (m && m[0]) {
|
|
173
|
+
const url = m[0].replace(/[)\],.]+$/g, '');
|
|
174
|
+
return url || null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
export class QuotaManager {
|
|
180
|
+
staticConfigs = new Map();
|
|
181
|
+
states = new Map();
|
|
182
|
+
store;
|
|
183
|
+
persistTimer = null;
|
|
184
|
+
dirty = false;
|
|
185
|
+
constructor(options) {
|
|
186
|
+
this.store = options?.store ?? null;
|
|
187
|
+
}
|
|
188
|
+
async hydrateFromStore() {
|
|
189
|
+
if (!this.store) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const snapshot = await this.store.load().catch(() => null);
|
|
193
|
+
if (!snapshot || !snapshot.providers || typeof snapshot.providers !== 'object') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const nowMs = Date.now();
|
|
197
|
+
for (const [providerKey, raw] of Object.entries(snapshot.providers)) {
|
|
198
|
+
if (!raw || typeof raw !== 'object')
|
|
199
|
+
continue;
|
|
200
|
+
const key = safeTrim(providerKey) || safeTrim(raw.providerKey);
|
|
201
|
+
if (!key)
|
|
202
|
+
continue;
|
|
203
|
+
const state = raw;
|
|
204
|
+
this.states.set(key, tickQuotaStateTime({ ...state, providerKey: key }, nowMs));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
registerProviderStaticConfig(providerKey, cfg) {
|
|
208
|
+
const key = safeTrim(providerKey);
|
|
209
|
+
if (!key)
|
|
210
|
+
return;
|
|
211
|
+
this.staticConfigs.set(key, { ...cfg });
|
|
212
|
+
if (!this.states.has(key)) {
|
|
213
|
+
this.states.set(key, createInitialQuotaState(key, cfg, Date.now()));
|
|
214
|
+
this.markDirty();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
ensureProvider(providerKey) {
|
|
218
|
+
const key = safeTrim(providerKey);
|
|
219
|
+
if (!key) {
|
|
220
|
+
throw new Error('providerKey is required');
|
|
221
|
+
}
|
|
222
|
+
const existing = this.states.get(key);
|
|
223
|
+
if (existing) {
|
|
224
|
+
return existing;
|
|
225
|
+
}
|
|
226
|
+
const cfg = this.staticConfigs.get(key);
|
|
227
|
+
const seeded = createInitialQuotaState(key, cfg, Date.now());
|
|
228
|
+
this.states.set(key, seeded);
|
|
229
|
+
this.markDirty();
|
|
230
|
+
return seeded;
|
|
231
|
+
}
|
|
232
|
+
disableProvider(options) {
|
|
233
|
+
const key = safeTrim(options.providerKey);
|
|
234
|
+
if (!key)
|
|
235
|
+
return;
|
|
236
|
+
const nowMs = Date.now();
|
|
237
|
+
const state = this.ensureProvider(key);
|
|
238
|
+
const until = nowMs + Math.max(0, Math.floor(options.durationMs));
|
|
239
|
+
const next = options.mode === 'blacklist'
|
|
240
|
+
? { ...state, blacklistUntil: Math.max(state.blacklistUntil ?? 0, until) || until, reason: 'blacklist', inPool: false }
|
|
241
|
+
: { ...state, cooldownUntil: Math.max(state.cooldownUntil ?? 0, until) || until, reason: 'cooldown', inPool: false };
|
|
242
|
+
this.states.set(key, tickQuotaStateTime(next, nowMs));
|
|
243
|
+
this.markDirty();
|
|
244
|
+
}
|
|
245
|
+
recoverProvider(providerKey) {
|
|
246
|
+
const key = safeTrim(providerKey);
|
|
247
|
+
if (!key)
|
|
248
|
+
return;
|
|
249
|
+
const nowMs = Date.now();
|
|
250
|
+
const state = this.ensureProvider(key);
|
|
251
|
+
const next = {
|
|
252
|
+
...state,
|
|
253
|
+
cooldownUntil: null,
|
|
254
|
+
blacklistUntil: null,
|
|
255
|
+
authIssue: null,
|
|
256
|
+
reason: 'ok',
|
|
257
|
+
inPool: true
|
|
258
|
+
};
|
|
259
|
+
this.states.set(key, tickQuotaStateTime(next, nowMs));
|
|
260
|
+
this.markDirty();
|
|
261
|
+
}
|
|
262
|
+
resetProvider(providerKey) {
|
|
263
|
+
const key = safeTrim(providerKey);
|
|
264
|
+
if (!key)
|
|
265
|
+
return;
|
|
266
|
+
const nowMs = Date.now();
|
|
267
|
+
const cfg = this.staticConfigs.get(key);
|
|
268
|
+
this.states.set(key, createInitialQuotaState(key, cfg, nowMs));
|
|
269
|
+
this.markDirty();
|
|
270
|
+
}
|
|
271
|
+
onProviderError(ev) {
|
|
272
|
+
const providerKey = parseProviderKey(ev.runtime) || safeTrim(ev?.runtime?.providerKey) || safeTrim(ev?.providerKey);
|
|
273
|
+
if (!providerKey)
|
|
274
|
+
return;
|
|
275
|
+
const nowMs = typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : Date.now();
|
|
276
|
+
const status = parseHttpStatus(ev);
|
|
277
|
+
const code = safeTrim(ev.code) || safeTrim(ev?.runtime?.errorCode);
|
|
278
|
+
const details = ev.details && typeof ev.details === 'object' ? ev.details : undefined;
|
|
279
|
+
const state = this.ensureProvider(providerKey);
|
|
280
|
+
// Antigravity account-scope auth verification required: blacklist the whole runtimeKey group.
|
|
281
|
+
if (isAntigravityProviderKey(providerKey)) {
|
|
282
|
+
const sources = collectUpstreamErrorSources(ev);
|
|
283
|
+
const runtimeKey = resolveAntigravityRuntimeKey(providerKey, ev);
|
|
284
|
+
if (runtimeKey && isGoogleAccountVerificationRequired(ev, sources)) {
|
|
285
|
+
const url = extractFirstUrl(sources);
|
|
286
|
+
const banUntil = nowMs + ANTIGRAVITY_AUTH_VERIFY_BAN_MS;
|
|
287
|
+
for (const key of this.states.keys()) {
|
|
288
|
+
if (!key.startsWith(`${runtimeKey}.`))
|
|
289
|
+
continue;
|
|
290
|
+
const s = this.ensureProvider(key);
|
|
291
|
+
const next = {
|
|
292
|
+
...s,
|
|
293
|
+
inPool: false,
|
|
294
|
+
reason: 'authVerify',
|
|
295
|
+
authIssue: {
|
|
296
|
+
kind: 'google_account_verification',
|
|
297
|
+
...(url ? { url } : {}),
|
|
298
|
+
message: 'account verification required'
|
|
299
|
+
},
|
|
300
|
+
blacklistUntil: Math.max(s.blacklistUntil ?? 0, banUntil) || banUntil,
|
|
301
|
+
cooldownUntil: null,
|
|
302
|
+
lastErrorAtMs: nowMs,
|
|
303
|
+
lastErrorCode: code || 'AUTH_VERIFY',
|
|
304
|
+
lastErrorSeries: 'EFATAL',
|
|
305
|
+
consecutiveErrorCount: 0
|
|
306
|
+
};
|
|
307
|
+
this.states.set(key, tickQuotaStateTime(next, nowMs));
|
|
308
|
+
}
|
|
309
|
+
this.markDirty();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Thought signature missing: freeze Gemini series under runtimeKey to avoid request storms.
|
|
313
|
+
if (isThoughtSignatureMissing(ev, sources)) {
|
|
314
|
+
const freezeUntil = nowMs + ANTIGRAVITY_THOUGHT_SIGNATURE_MISSING_COOLDOWN_MS;
|
|
315
|
+
const prefix = runtimeKey ? `${runtimeKey}.` : `${providerKey.split('.').slice(0, 2).join('.')}.`;
|
|
316
|
+
for (const key of this.states.keys()) {
|
|
317
|
+
if (!key.startsWith(prefix))
|
|
318
|
+
continue;
|
|
319
|
+
const parts = key.split('.');
|
|
320
|
+
const modelId = parts.length >= 3 ? parts.slice(2).join('.') : '';
|
|
321
|
+
const lowerModel = modelId.toLowerCase();
|
|
322
|
+
const isGeminiSeries = lowerModel.includes('gemini') && (lowerModel.includes('pro') || lowerModel.includes('flash'));
|
|
323
|
+
if (!isGeminiSeries)
|
|
324
|
+
continue;
|
|
325
|
+
const s = this.ensureProvider(key);
|
|
326
|
+
const next = {
|
|
327
|
+
...s,
|
|
328
|
+
inPool: false,
|
|
329
|
+
reason: 'cooldown',
|
|
330
|
+
cooldownUntil: Math.max(s.cooldownUntil ?? 0, freezeUntil) || freezeUntil,
|
|
331
|
+
lastErrorAtMs: nowMs,
|
|
332
|
+
lastErrorCode: code || 'SIGNATURE_MISSING',
|
|
333
|
+
lastErrorSeries: 'EOTHER',
|
|
334
|
+
consecutiveErrorCount: 0
|
|
335
|
+
};
|
|
336
|
+
this.states.set(key, tickQuotaStateTime(next, nowMs));
|
|
337
|
+
}
|
|
338
|
+
this.markDirty();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// HTTP 402: treat as quota depleted -> blacklist until reset.
|
|
343
|
+
if (status === 402 || code.toUpperCase() === 'HTTP_402') {
|
|
344
|
+
const resetAtIso = extractResetAtIso(details) ??
|
|
345
|
+
extractResetAtIsoFromSources(collectUpstreamErrorSources(ev));
|
|
346
|
+
let resetAtMs = null;
|
|
347
|
+
if (resetAtIso) {
|
|
348
|
+
const parsed = Date.parse(resetAtIso);
|
|
349
|
+
if (Number.isFinite(parsed) && parsed > nowMs) {
|
|
350
|
+
resetAtMs = parsed;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (resetAtMs === null) {
|
|
354
|
+
const staticCfg = this.staticConfigs.get(providerKey);
|
|
355
|
+
const configuredResetTime = staticCfg && typeof staticCfg.apikeyDailyResetTime === 'string'
|
|
356
|
+
? String(staticCfg.apikeyDailyResetTime).trim()
|
|
357
|
+
: null;
|
|
358
|
+
resetAtMs = computeNextDailyResetAtMs({ nowMs, resetTime: configuredResetTime }).resetAtMs;
|
|
359
|
+
}
|
|
360
|
+
const next = {
|
|
361
|
+
...state,
|
|
362
|
+
inPool: false,
|
|
363
|
+
reason: 'blacklist',
|
|
364
|
+
blacklistUntil: Math.max(state.blacklistUntil ?? 0, resetAtMs) || resetAtMs,
|
|
365
|
+
lastErrorAtMs: nowMs,
|
|
366
|
+
lastErrorCode: code || 'HTTP_402',
|
|
367
|
+
lastErrorSeries: 'EOTHER',
|
|
368
|
+
consecutiveErrorCount: 0
|
|
369
|
+
};
|
|
370
|
+
this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
|
|
371
|
+
this.markDirty();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const authIssue = details?.authIssue && typeof details.authIssue === 'object'
|
|
375
|
+
? details.authIssue
|
|
376
|
+
: null;
|
|
377
|
+
const mapped = {
|
|
378
|
+
providerKey,
|
|
379
|
+
code: code || null,
|
|
380
|
+
httpStatus: typeof status === 'number' ? status : null,
|
|
381
|
+
fatal: Boolean(ev.recoverable === false || ev.affectsHealth === false ? false : ev?.fatal) || null,
|
|
382
|
+
timestampMs: nowMs,
|
|
383
|
+
resetAt: extractResetAtIso(details),
|
|
384
|
+
authIssue: authIssue || null
|
|
385
|
+
};
|
|
386
|
+
const next = applyErrorEvent(state, mapped, nowMs);
|
|
387
|
+
this.states.set(providerKey, tickQuotaStateTime(next, nowMs));
|
|
388
|
+
this.markDirty();
|
|
389
|
+
}
|
|
390
|
+
onProviderSuccess(ev) {
|
|
391
|
+
const providerKey = safeTrim(ev.runtime?.providerKey);
|
|
392
|
+
if (!providerKey)
|
|
393
|
+
return;
|
|
394
|
+
const nowMs = typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : Date.now();
|
|
395
|
+
const state = this.ensureProvider(providerKey);
|
|
396
|
+
const mapped = { providerKey, timestampMs: nowMs };
|
|
397
|
+
const next = applySuccessEvent(state, mapped, nowMs);
|
|
398
|
+
this.states.set(providerKey, next);
|
|
399
|
+
this.markDirty();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* External quota snapshot ingestion hook (host-driven).
|
|
403
|
+
* This API is intentionally small: the host adapter translates provider-specific
|
|
404
|
+
* quota responses into a normalized per-providerKey inPool/cooldown/blacklist update.
|
|
405
|
+
*/
|
|
406
|
+
updateProviderPoolState(options) {
|
|
407
|
+
const key = safeTrim(options.providerKey);
|
|
408
|
+
if (!key)
|
|
409
|
+
return;
|
|
410
|
+
const nowMs = Date.now();
|
|
411
|
+
const state = this.ensureProvider(key);
|
|
412
|
+
const next = {
|
|
413
|
+
...state,
|
|
414
|
+
inPool: Boolean(options.inPool),
|
|
415
|
+
reason: options.inPool ? 'ok' : state.reason,
|
|
416
|
+
cooldownUntil: typeof options.cooldownUntil === 'number' ? options.cooldownUntil : state.cooldownUntil,
|
|
417
|
+
blacklistUntil: typeof options.blacklistUntil === 'number' ? options.blacklistUntil : state.blacklistUntil,
|
|
418
|
+
...(typeof options.reason === 'string' && options.reason.trim()
|
|
419
|
+
? { reason: options.reason.trim() }
|
|
420
|
+
: {})
|
|
421
|
+
};
|
|
422
|
+
this.states.set(key, tickQuotaStateTime(next, nowMs));
|
|
423
|
+
this.markDirty();
|
|
424
|
+
}
|
|
425
|
+
getQuotaView() {
|
|
426
|
+
return (providerKey) => {
|
|
427
|
+
const key = safeTrim(providerKey);
|
|
428
|
+
if (!key)
|
|
429
|
+
return null;
|
|
430
|
+
const nowMs = Date.now();
|
|
431
|
+
const state = this.states.get(key);
|
|
432
|
+
if (!state) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const normalized = tickQuotaStateTime(state, nowMs);
|
|
436
|
+
if (normalized !== state) {
|
|
437
|
+
this.states.set(key, normalized);
|
|
438
|
+
}
|
|
439
|
+
const withinBlacklist = typeof normalized.blacklistUntil === 'number' && normalized.blacklistUntil > nowMs;
|
|
440
|
+
const withinCooldown = typeof normalized.cooldownUntil === 'number' && normalized.cooldownUntil > nowMs;
|
|
441
|
+
const inPool = normalized.inPool && !withinBlacklist && !withinCooldown;
|
|
442
|
+
const hasRecentError = typeof normalized.lastErrorAtMs === 'number' &&
|
|
443
|
+
Number.isFinite(normalized.lastErrorAtMs) &&
|
|
444
|
+
nowMs - normalized.lastErrorAtMs >= 0 &&
|
|
445
|
+
nowMs - normalized.lastErrorAtMs <= 30_000;
|
|
446
|
+
const selectionPenalty = hasRecentError && typeof normalized.consecutiveErrorCount === 'number' && normalized.consecutiveErrorCount > 0
|
|
447
|
+
? Math.max(0, Math.floor(normalized.consecutiveErrorCount))
|
|
448
|
+
: 0;
|
|
449
|
+
return {
|
|
450
|
+
providerKey: key,
|
|
451
|
+
inPool,
|
|
452
|
+
reason: normalized.reason,
|
|
453
|
+
priorityTier: normalized.priorityTier,
|
|
454
|
+
selectionPenalty,
|
|
455
|
+
lastErrorAtMs: normalized.lastErrorAtMs,
|
|
456
|
+
consecutiveErrorCount: normalized.consecutiveErrorCount,
|
|
457
|
+
cooldownUntil: normalized.cooldownUntil,
|
|
458
|
+
blacklistUntil: normalized.blacklistUntil
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
getSnapshot() {
|
|
463
|
+
return {
|
|
464
|
+
updatedAtMs: Date.now(),
|
|
465
|
+
providers: Object.fromEntries(this.states.entries())
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
markDirty() {
|
|
469
|
+
this.dirty = true;
|
|
470
|
+
if (this.persistTimer) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// best-effort debounce
|
|
474
|
+
this.persistTimer = setTimeout(() => {
|
|
475
|
+
this.persistTimer = null;
|
|
476
|
+
if (!this.dirty)
|
|
477
|
+
return;
|
|
478
|
+
this.dirty = false;
|
|
479
|
+
void this.persistNow();
|
|
480
|
+
}, 800);
|
|
481
|
+
}
|
|
482
|
+
async persistNow() {
|
|
483
|
+
if (!this.store)
|
|
484
|
+
return;
|
|
485
|
+
const snapshot = this.getSnapshot();
|
|
486
|
+
const payload = { savedAtMs: snapshot.updatedAtMs, providers: snapshot.providers };
|
|
487
|
+
await this.store.save(payload).catch(() => {
|
|
488
|
+
// persistence must never block routing
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ErrorEventForQuota, ErrorSeries, QuotaState, StaticQuotaConfig, SuccessEventForQuota } from './types.js';
|
|
2
|
+
export declare function createInitialQuotaState(providerKey: string, staticConfig?: StaticQuotaConfig, nowMs?: number): QuotaState;
|
|
3
|
+
export declare function normalizeErrorSeries(event: ErrorEventForQuota): ErrorSeries;
|
|
4
|
+
export declare function tickQuotaStateTime(state: QuotaState, nowMs: number): QuotaState;
|
|
5
|
+
export declare function applyErrorEvent(state: QuotaState, event: ErrorEventForQuota, nowMs?: number): QuotaState;
|
|
6
|
+
export declare function applySuccessEvent(state: QuotaState, _event: SuccessEventForQuota, nowMs?: number): QuotaState;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const COOLDOWN_SCHEDULE_429_MS = [5_000, 30_000, 60_000, 300_000, 1_800_000, 3_600_000, 10_000_000];
|
|
2
|
+
const COOLDOWN_SCHEDULE_FATAL_MS = [5 * 60_000, 15 * 60_000, 30 * 60_000, 60 * 60_000, 3 * 60 * 60_000];
|
|
3
|
+
const COOLDOWN_SCHEDULE_DEFAULT_MS = [5_000, 30_000, 60_000, 300_000, 1_800_000, 3_600_000, 10_000_000];
|
|
4
|
+
const ERROR_CHAIN_WINDOW_MS = 10 * 60_000;
|
|
5
|
+
const NETWORK_ERROR_CODES = [
|
|
6
|
+
'ECONNRESET',
|
|
7
|
+
'ECONNREFUSED',
|
|
8
|
+
'ETIMEDOUT',
|
|
9
|
+
'EAI_AGAIN',
|
|
10
|
+
'UPSTREAM_HEADERS_TIMEOUT',
|
|
11
|
+
'UPSTREAM_STREAM_TIMEOUT',
|
|
12
|
+
'UPSTREAM_STREAM_IDLE_TIMEOUT',
|
|
13
|
+
'UPSTREAM_STREAM_ABORTED'
|
|
14
|
+
];
|
|
15
|
+
export function createInitialQuotaState(providerKey, staticConfig, nowMs = Date.now()) {
|
|
16
|
+
const priorityTier = staticConfig && typeof staticConfig.priorityTier === 'number'
|
|
17
|
+
? staticConfig.priorityTier
|
|
18
|
+
: 100;
|
|
19
|
+
const authType = staticConfig && typeof staticConfig.authType === 'string' && staticConfig.authType.trim()
|
|
20
|
+
? staticConfig.authType.trim()
|
|
21
|
+
: 'unknown';
|
|
22
|
+
return {
|
|
23
|
+
providerKey,
|
|
24
|
+
inPool: true,
|
|
25
|
+
reason: 'ok',
|
|
26
|
+
authType,
|
|
27
|
+
authIssue: null,
|
|
28
|
+
priorityTier,
|
|
29
|
+
cooldownUntil: null,
|
|
30
|
+
blacklistUntil: null,
|
|
31
|
+
lastErrorSeries: null,
|
|
32
|
+
lastErrorCode: null,
|
|
33
|
+
lastErrorAtMs: null,
|
|
34
|
+
consecutiveErrorCount: 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function normalizeErrorSeries(event) {
|
|
38
|
+
if (event.fatal) {
|
|
39
|
+
return 'EFATAL';
|
|
40
|
+
}
|
|
41
|
+
const status = typeof event.httpStatus === 'number' ? event.httpStatus : null;
|
|
42
|
+
const rawCode = String(event.code || '').toUpperCase();
|
|
43
|
+
if (status === 429 || rawCode.includes('429') || rawCode.includes('RATE') || rawCode.includes('QUOTA')) {
|
|
44
|
+
return 'E429';
|
|
45
|
+
}
|
|
46
|
+
if (status && status >= 500) {
|
|
47
|
+
return 'E5XX';
|
|
48
|
+
}
|
|
49
|
+
if (rawCode.includes('TIMEOUT') || NETWORK_ERROR_CODES.some((code) => rawCode.includes(code))) {
|
|
50
|
+
return 'ENET';
|
|
51
|
+
}
|
|
52
|
+
if (rawCode.includes('AUTH') || rawCode.includes('UNAUTHORIZED') || rawCode.includes('CONFIG') || rawCode.includes('FATAL')) {
|
|
53
|
+
return 'EFATAL';
|
|
54
|
+
}
|
|
55
|
+
return 'EOTHER';
|
|
56
|
+
}
|
|
57
|
+
function normalizeErrorKey(event) {
|
|
58
|
+
const rawCode = String(event.code || '').trim().toUpperCase();
|
|
59
|
+
if (rawCode) {
|
|
60
|
+
return rawCode;
|
|
61
|
+
}
|
|
62
|
+
const status = typeof event.httpStatus === 'number' ? event.httpStatus : null;
|
|
63
|
+
if (status && Number.isFinite(status)) {
|
|
64
|
+
return `HTTP_${Math.floor(status)}`;
|
|
65
|
+
}
|
|
66
|
+
return 'ERR_UNKNOWN';
|
|
67
|
+
}
|
|
68
|
+
function computeCooldownMsBySeries(series, consecutive) {
|
|
69
|
+
if (consecutive <= 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const schedule = series === 'E429'
|
|
73
|
+
? COOLDOWN_SCHEDULE_429_MS
|
|
74
|
+
: series === 'EFATAL'
|
|
75
|
+
? COOLDOWN_SCHEDULE_FATAL_MS
|
|
76
|
+
: COOLDOWN_SCHEDULE_DEFAULT_MS;
|
|
77
|
+
const idx = Math.min(consecutive - 1, schedule.length - 1);
|
|
78
|
+
return schedule[idx] ?? null;
|
|
79
|
+
}
|
|
80
|
+
export function tickQuotaStateTime(state, nowMs) {
|
|
81
|
+
let next = state;
|
|
82
|
+
if (typeof next.cooldownUntil === 'number' && next.cooldownUntil <= nowMs) {
|
|
83
|
+
next = { ...next, cooldownUntil: null };
|
|
84
|
+
}
|
|
85
|
+
if (typeof next.blacklistUntil === 'number' && next.blacklistUntil <= nowMs) {
|
|
86
|
+
next = { ...next, blacklistUntil: null };
|
|
87
|
+
}
|
|
88
|
+
if (next.authIssue) {
|
|
89
|
+
if (next.inPool !== false || next.reason !== 'authVerify') {
|
|
90
|
+
next = { ...next, inPool: false, reason: 'authVerify' };
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
const inCooldown = typeof next.cooldownUntil === 'number' && next.cooldownUntil > nowMs;
|
|
95
|
+
const inBlacklist = typeof next.blacklistUntil === 'number' && next.blacklistUntil > nowMs;
|
|
96
|
+
if (inBlacklist) {
|
|
97
|
+
if (next.inPool !== false || next.reason !== 'blacklist') {
|
|
98
|
+
next = { ...next, inPool: false, reason: 'blacklist' };
|
|
99
|
+
}
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
if (inCooldown) {
|
|
103
|
+
if (next.inPool !== false || next.reason !== 'cooldown') {
|
|
104
|
+
next = { ...next, inPool: false, reason: 'cooldown' };
|
|
105
|
+
}
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
108
|
+
// TTLs expired: only auto-reset "cooldown/blacklist" back to ok.
|
|
109
|
+
if (next.reason === 'cooldown' || next.reason === 'blacklist') {
|
|
110
|
+
next = { ...next, inPool: true, reason: 'ok' };
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
export function applyErrorEvent(state, event, nowMs = event.timestampMs ?? Date.now()) {
|
|
115
|
+
// Manual/operator blacklist is rigid: automated error events must not override it.
|
|
116
|
+
if (state.blacklistUntil !== null && nowMs < state.blacklistUntil) {
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
const series = normalizeErrorSeries(event);
|
|
120
|
+
const errorKey = normalizeErrorKey(event);
|
|
121
|
+
const lastAt = typeof state.lastErrorAtMs === 'number' && Number.isFinite(state.lastErrorAtMs)
|
|
122
|
+
? state.lastErrorAtMs
|
|
123
|
+
: null;
|
|
124
|
+
const withinChainWindow = typeof lastAt === 'number' &&
|
|
125
|
+
nowMs - lastAt >= 0 &&
|
|
126
|
+
nowMs - lastAt <= ERROR_CHAIN_WINDOW_MS;
|
|
127
|
+
const sameErrorKey = withinChainWindow && state.lastErrorCode === errorKey;
|
|
128
|
+
const rawNextCount = sameErrorKey ? state.consecutiveErrorCount + 1 : 1;
|
|
129
|
+
const schedule = series === 'E429'
|
|
130
|
+
? COOLDOWN_SCHEDULE_429_MS
|
|
131
|
+
: series === 'EFATAL'
|
|
132
|
+
? COOLDOWN_SCHEDULE_FATAL_MS
|
|
133
|
+
: COOLDOWN_SCHEDULE_DEFAULT_MS;
|
|
134
|
+
const nextCount = rawNextCount > schedule.length ? 1 : rawNextCount;
|
|
135
|
+
const cooldownMs = computeCooldownMsBySeries(series, nextCount);
|
|
136
|
+
const nextUntil = cooldownMs ? nowMs + cooldownMs : null;
|
|
137
|
+
const existingUntil = typeof state.cooldownUntil === 'number' ? state.cooldownUntil : null;
|
|
138
|
+
const cooldownUntil = typeof nextUntil === 'number' && Number.isFinite(nextUntil)
|
|
139
|
+
? typeof existingUntil === 'number' && existingUntil > nextUntil
|
|
140
|
+
? existingUntil
|
|
141
|
+
: nextUntil
|
|
142
|
+
: existingUntil;
|
|
143
|
+
const inCooldown = typeof cooldownUntil === 'number' && cooldownUntil > nowMs;
|
|
144
|
+
const inBlacklist = typeof state.blacklistUntil === 'number' && state.blacklistUntil > nowMs;
|
|
145
|
+
const inPool = !inCooldown && !inBlacklist;
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
inPool,
|
|
149
|
+
reason: inBlacklist ? 'blacklist' : inCooldown ? 'cooldown' : 'ok',
|
|
150
|
+
cooldownUntil,
|
|
151
|
+
lastErrorSeries: series,
|
|
152
|
+
lastErrorCode: errorKey,
|
|
153
|
+
lastErrorAtMs: nowMs,
|
|
154
|
+
consecutiveErrorCount: nextCount,
|
|
155
|
+
...(event.authIssue ? { authIssue: event.authIssue, reason: 'authVerify', inPool: false } : {})
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function applySuccessEvent(state, _event, nowMs = _event.timestampMs ?? Date.now()) {
|
|
159
|
+
const next = {
|
|
160
|
+
...state,
|
|
161
|
+
lastErrorSeries: null,
|
|
162
|
+
lastErrorCode: null,
|
|
163
|
+
lastErrorAtMs: null,
|
|
164
|
+
consecutiveErrorCount: 0
|
|
165
|
+
};
|
|
166
|
+
return tickQuotaStateTime(next, nowMs);
|
|
167
|
+
}
|