@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
|
@@ -2,6 +2,26 @@ import { jsonClone } from '../hub/types/json.js';
|
|
|
2
2
|
function isPlainRecord(value) {
|
|
3
3
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
|
+
function normalizeSchemaMapEntries(value) {
|
|
6
|
+
if (!Array.isArray(value)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const entry of value) {
|
|
11
|
+
if (!isPlainRecord(entry)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const key = typeof entry.key === 'string' ? entry.key.trim() : '';
|
|
15
|
+
if (!key) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'value')) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
out[key] = entry.value;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
5
25
|
function pickBestSchemaVariant(variants) {
|
|
6
26
|
if (!Array.isArray(variants) || variants.length === 0) {
|
|
7
27
|
return { type: 'object', properties: {} };
|
|
@@ -45,7 +65,63 @@ function normalizeSchemaTypes(value) {
|
|
|
45
65
|
const out = {};
|
|
46
66
|
for (const [key, val] of Object.entries(record)) {
|
|
47
67
|
if (key === 'type' && typeof val === 'string') {
|
|
48
|
-
|
|
68
|
+
// Antigravity-Manager alignment: keep JSON-Schema-like lowercase types.
|
|
69
|
+
out[key] = val.trim().toLowerCase();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (key === 'type' && Array.isArray(val)) {
|
|
73
|
+
// Antigravity-Manager alignment: Gemini protobuf Schema.type is not repeating.
|
|
74
|
+
// JSON Schema may use union types like ["string","null"], which triggers:
|
|
75
|
+
// "Proto field is not repeating, cannot start list."
|
|
76
|
+
const candidates = val
|
|
77
|
+
.filter((entry) => typeof entry === 'string')
|
|
78
|
+
.map((entry) => String(entry).trim().toLowerCase())
|
|
79
|
+
.filter((entry) => entry && entry !== 'null');
|
|
80
|
+
const preferred = candidates.includes('string')
|
|
81
|
+
? 'string'
|
|
82
|
+
: candidates.includes('object')
|
|
83
|
+
? 'object'
|
|
84
|
+
: candidates.includes('array')
|
|
85
|
+
? 'array'
|
|
86
|
+
: candidates.includes('integer')
|
|
87
|
+
? 'integer'
|
|
88
|
+
: candidates.includes('number')
|
|
89
|
+
? 'number'
|
|
90
|
+
: candidates.includes('boolean')
|
|
91
|
+
? 'boolean'
|
|
92
|
+
: candidates[0];
|
|
93
|
+
if (preferred) {
|
|
94
|
+
out[key] = preferred;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (key === 'properties') {
|
|
99
|
+
const normalizedMap = normalizeSchemaMapEntries(val);
|
|
100
|
+
if (normalizedMap) {
|
|
101
|
+
out[key] = normalizeSchemaTypes(normalizedMap);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (val && typeof val === 'object') {
|
|
105
|
+
out[key] = normalizeSchemaTypes(val);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (key === 'enum' && Array.isArray(val)) {
|
|
110
|
+
// Antigravity-Manager alignment: Gemini rejects non-string enum values in tool schemas.
|
|
111
|
+
out[key] = val.map((entry) => {
|
|
112
|
+
if (typeof entry === 'string')
|
|
113
|
+
return entry;
|
|
114
|
+
if (entry === null)
|
|
115
|
+
return 'null';
|
|
116
|
+
if (typeof entry === 'number' || typeof entry === 'boolean')
|
|
117
|
+
return String(entry);
|
|
118
|
+
try {
|
|
119
|
+
return JSON.stringify(entry);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return String(entry);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
49
125
|
continue;
|
|
50
126
|
}
|
|
51
127
|
if ((key === 'properties' || key === 'items') && val && typeof val === 'object') {
|
|
@@ -105,6 +181,40 @@ function cloneParameters(value) {
|
|
|
105
181
|
}
|
|
106
182
|
const cloned = {};
|
|
107
183
|
for (const [key, entry] of Object.entries(value)) {
|
|
184
|
+
// Antigravity-Manager alignment: some tool pipelines serialize schema maps as [{ key, value }, ...].
|
|
185
|
+
// Gemini expects `properties` to be an object/map, not a list.
|
|
186
|
+
if (key === 'properties') {
|
|
187
|
+
const normalizedMap = normalizeSchemaMapEntries(entry);
|
|
188
|
+
if (normalizedMap) {
|
|
189
|
+
cloned.properties = cloneParameters(normalizedMap);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Antigravity-Manager alignment: Gemini protobuf Schema.type is not repeating.
|
|
194
|
+
// Drop union array types like ["string","null"] to a single stable type.
|
|
195
|
+
if (key === 'type' && Array.isArray(entry)) {
|
|
196
|
+
const candidates = entry
|
|
197
|
+
.filter((v) => typeof v === 'string')
|
|
198
|
+
.map((v) => String(v).trim().toLowerCase())
|
|
199
|
+
.filter((v) => v && v !== 'null');
|
|
200
|
+
if (candidates.length) {
|
|
201
|
+
// Prefer STRING to maximize compatibility.
|
|
202
|
+
cloned.type = candidates.includes('string')
|
|
203
|
+
? 'string'
|
|
204
|
+
: candidates.includes('object')
|
|
205
|
+
? 'object'
|
|
206
|
+
: candidates.includes('array')
|
|
207
|
+
? 'array'
|
|
208
|
+
: candidates.includes('integer')
|
|
209
|
+
? 'integer'
|
|
210
|
+
: candidates.includes('number')
|
|
211
|
+
? 'number'
|
|
212
|
+
: candidates.includes('boolean')
|
|
213
|
+
? 'boolean'
|
|
214
|
+
: candidates[0];
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
108
218
|
// Gemini function_declarations.parameters only support a subset of JSON Schema.
|
|
109
219
|
// Drop meta/unsupported fields that cause INVALID_ARGUMENT, such as $schema/exclusiveMinimum/propertyNames.
|
|
110
220
|
if (typeof key === 'string') {
|
|
@@ -132,12 +242,27 @@ function cloneParameters(value) {
|
|
|
132
242
|
// Preserve `required` for Gemini validation (align with gcli2api), but still drop additionalProperties.
|
|
133
243
|
if (key === 'additionalProperties')
|
|
134
244
|
continue;
|
|
245
|
+
if (key === 'external_web_access')
|
|
246
|
+
continue;
|
|
135
247
|
// Combinators are handled at the node level above.
|
|
136
248
|
if (key === 'oneOf' || key === 'anyOf' || key === 'allOf')
|
|
137
249
|
continue;
|
|
138
250
|
}
|
|
139
251
|
cloned[key] = cloneParameters(entry);
|
|
140
252
|
}
|
|
253
|
+
// Antigravity-Manager alignment: required must only reference keys present in properties.
|
|
254
|
+
const propNode = cloned.properties;
|
|
255
|
+
const reqNode = cloned.required;
|
|
256
|
+
if (isPlainRecord(propNode) && Array.isArray(reqNode)) {
|
|
257
|
+
const valid = new Set(Object.keys(propNode));
|
|
258
|
+
const filtered = reqNode.filter((v) => typeof v === 'string' && valid.has(v));
|
|
259
|
+
if (filtered.length) {
|
|
260
|
+
cloned.required = filtered;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
delete cloned.required;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
141
266
|
return cloned;
|
|
142
267
|
}
|
|
143
268
|
return { type: 'object', properties: {} };
|
|
@@ -255,11 +380,11 @@ export function buildGeminiToolsFromBridge(defs, options) {
|
|
|
255
380
|
// ignore
|
|
256
381
|
}
|
|
257
382
|
props.command = {
|
|
258
|
-
type: '
|
|
383
|
+
type: 'string',
|
|
259
384
|
description: 'Shell command to execute.'
|
|
260
385
|
};
|
|
261
386
|
// Keep workdir simple (avoid anyOf string|null patterns).
|
|
262
|
-
props.workdir = { type: '
|
|
387
|
+
props.workdir = { type: 'string', description: 'Working directory.' };
|
|
263
388
|
}
|
|
264
389
|
else {
|
|
265
390
|
if (!Object.prototype.hasOwnProperty.call(props, 'cmd') && Object.prototype.hasOwnProperty.call(props, 'command')) {
|
|
@@ -298,10 +423,10 @@ export function buildGeminiToolsFromBridge(defs, options) {
|
|
|
298
423
|
props.text = props.chars;
|
|
299
424
|
}
|
|
300
425
|
if (!Object.prototype.hasOwnProperty.call(props, 'session_id')) {
|
|
301
|
-
props.session_id = { type:
|
|
426
|
+
props.session_id = { type: 'number' };
|
|
302
427
|
}
|
|
303
428
|
if (!Object.prototype.hasOwnProperty.call(props, 'chars')) {
|
|
304
|
-
props.chars = { type:
|
|
429
|
+
props.chars = { type: 'string' };
|
|
305
430
|
}
|
|
306
431
|
params.properties = props;
|
|
307
432
|
try {
|
|
@@ -319,25 +444,25 @@ export function buildGeminiToolsFromBridge(defs, options) {
|
|
|
319
444
|
// - input/instructions/text: historical aliases containing patch text
|
|
320
445
|
if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
|
|
321
446
|
props.patch = {
|
|
322
|
-
type:
|
|
447
|
+
type: 'string',
|
|
323
448
|
description: 'Patch text (*** Begin Patch / *** End Patch or GNU unified diff).'
|
|
324
449
|
};
|
|
325
450
|
}
|
|
326
451
|
if (!Object.prototype.hasOwnProperty.call(props, 'input')) {
|
|
327
452
|
props.input = {
|
|
328
|
-
type:
|
|
453
|
+
type: 'string',
|
|
329
454
|
description: 'Alias of patch (patch text). Prefer patch.'
|
|
330
455
|
};
|
|
331
456
|
}
|
|
332
457
|
if (!Object.prototype.hasOwnProperty.call(props, 'instructions')) {
|
|
333
458
|
props.instructions = {
|
|
334
|
-
type:
|
|
459
|
+
type: 'string',
|
|
335
460
|
description: 'Alias of patch (patch text). Prefer patch.'
|
|
336
461
|
};
|
|
337
462
|
}
|
|
338
463
|
if (!Object.prototype.hasOwnProperty.call(props, 'text')) {
|
|
339
464
|
props.text = {
|
|
340
|
-
type:
|
|
465
|
+
type: 'string',
|
|
341
466
|
description: 'Alias of patch (patch text). Prefer patch.'
|
|
342
467
|
};
|
|
343
468
|
}
|
|
@@ -129,8 +129,12 @@ export function extractToolNamespaceXmlBlocksFromText(text) {
|
|
|
129
129
|
try {
|
|
130
130
|
if (typeof text !== 'string' || !text)
|
|
131
131
|
return null;
|
|
132
|
-
|
|
132
|
+
const hasOpenTag = text.includes('<tool:');
|
|
133
|
+
const hasCloseTag = /<\/\s*tool:\s*[A-Za-z0-9_]+\s*>/i.test(text);
|
|
134
|
+
const hasToolPrefix = /(?:^|\n)\s*(?:[•*+-]\s*)?tool:[A-Za-z0-9_]+/i.test(text);
|
|
135
|
+
if (!hasOpenTag && !(hasCloseTag && hasToolPrefix)) {
|
|
133
136
|
return null;
|
|
137
|
+
}
|
|
134
138
|
const out = [];
|
|
135
139
|
const blockRe = /<\s*tool:([A-Za-z0-9_]+)\s*>([\s\S]*?)<\/\s*tool:\s*\1\s*>/gi;
|
|
136
140
|
let bm;
|
|
@@ -211,6 +215,91 @@ export function extractToolNamespaceXmlBlocksFromText(text) {
|
|
|
211
215
|
}
|
|
212
216
|
out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
|
|
213
217
|
}
|
|
218
|
+
// Broken variant (seen in the wild):
|
|
219
|
+
// tool:exec_command (tool:exec_command)
|
|
220
|
+
// <command>...</command>
|
|
221
|
+
// </tool:exec_command>
|
|
222
|
+
//
|
|
223
|
+
// Best-effort: treat it as an implicit <tool:NAME> open tag.
|
|
224
|
+
const prefixBlockRe = /(?:^|\n)\s*(?:[•*+-]\s*)?tool:([A-Za-z0-9_]+)[^\n]*\n([\s\S]*?)<\/\s*tool:\s*\1\s*>/gi;
|
|
225
|
+
let pm;
|
|
226
|
+
while ((pm = prefixBlockRe.exec(text)) !== null) {
|
|
227
|
+
const rawName = String(pm[1] || '').trim();
|
|
228
|
+
const lname = rawName.toLowerCase();
|
|
229
|
+
if (!lname || !KNOWN_TOOLS.has(lname))
|
|
230
|
+
continue;
|
|
231
|
+
const inner = String(pm[2] || '');
|
|
232
|
+
const argsObj = {};
|
|
233
|
+
const kvRe = /<\s*([A-Za-z_][A-Za-z0-9_]*)\s*>([\s\S]*?)<\/\s*\1\s*>/g;
|
|
234
|
+
let km;
|
|
235
|
+
while ((km = kvRe.exec(inner)) !== null) {
|
|
236
|
+
const rawKey = String(km[1] || '').trim();
|
|
237
|
+
const key = normalizeKey(rawKey).toLowerCase();
|
|
238
|
+
if (!key)
|
|
239
|
+
continue;
|
|
240
|
+
if (key === 'requires_approval')
|
|
241
|
+
continue;
|
|
242
|
+
const rawVal = String(km[2] ?? '');
|
|
243
|
+
const value = tryParsePrimitiveValue(rawVal);
|
|
244
|
+
if (lname === 'exec_command' && (key === 'command' || key === 'cmd')) {
|
|
245
|
+
const cmd = coerceCommandValueToString(value);
|
|
246
|
+
if (cmd.trim().length) {
|
|
247
|
+
argsObj.cmd = cmd;
|
|
248
|
+
argsObj.command = cmd;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (lname === 'exec_command' && (key === 'cwd' || key === 'workdir')) {
|
|
253
|
+
const wd = String(value ?? rawVal).trim();
|
|
254
|
+
if (wd)
|
|
255
|
+
argsObj.workdir = wd;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (lname === 'write_stdin') {
|
|
259
|
+
if (key === 'input' || key === 'data' || key === 'chars' || key === 'text') {
|
|
260
|
+
argsObj.chars = rawVal;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (key === 'session_id') {
|
|
264
|
+
const n = Number.parseInt(String(value), 10);
|
|
265
|
+
argsObj.session_id = Number.isFinite(n) ? n : value;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (lname === 'apply_patch' && (key === 'patch' || key === 'input' || key === 'text' || key === 'instructions')) {
|
|
270
|
+
const patchText = typeof value === 'string' ? value : rawVal;
|
|
271
|
+
if (typeof patchText === 'string' && patchText.trim().length) {
|
|
272
|
+
argsObj.patch = patchText;
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
argsObj[key] = value;
|
|
277
|
+
}
|
|
278
|
+
const filtered = filterArgsForTool(lname, argsObj);
|
|
279
|
+
if (lname === 'exec_command') {
|
|
280
|
+
const cmd = typeof filtered?.cmd === 'string' ? String(filtered.cmd).trim() : '';
|
|
281
|
+
if (!cmd)
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (lname === 'write_stdin') {
|
|
285
|
+
const sid = filtered?.session_id;
|
|
286
|
+
if (sid === undefined || sid === null || String(sid).trim().length === 0)
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (lname === 'apply_patch') {
|
|
290
|
+
const patchText = typeof filtered?.patch === 'string' ? String(filtered.patch).trim() : '';
|
|
291
|
+
if (!patchText)
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
let argsStr = '{}';
|
|
295
|
+
try {
|
|
296
|
+
argsStr = JSON.stringify(filtered);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
argsStr = '{}';
|
|
300
|
+
}
|
|
301
|
+
out.push({ id: `call_${Math.random().toString(36).slice(2, 10)}`, name: lname, args: argsStr });
|
|
302
|
+
}
|
|
214
303
|
return out.length ? out : null;
|
|
215
304
|
}
|
|
216
305
|
catch {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type DailyResetParseResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
kind: 'local' | 'utc';
|
|
4
|
+
hour: number;
|
|
5
|
+
minute: number;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseDailyResetTime(input: string | null | undefined): DailyResetParseResult;
|
|
11
|
+
export declare function computeNextDailyResetAtMs(args: {
|
|
12
|
+
nowMs: number;
|
|
13
|
+
resetTime?: string | null;
|
|
14
|
+
}): {
|
|
15
|
+
resetAtMs: number;
|
|
16
|
+
resetTimeNormalized: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function pad2(n) {
|
|
2
|
+
return String(n).padStart(2, '0');
|
|
3
|
+
}
|
|
4
|
+
export function parseDailyResetTime(input) {
|
|
5
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
6
|
+
const value = raw || '12:00';
|
|
7
|
+
const isUtc = value.toUpperCase().endsWith('Z');
|
|
8
|
+
const base = isUtc ? value.slice(0, -1) : value;
|
|
9
|
+
const m = base.match(/^(\d{1,2}):(\d{2})$/);
|
|
10
|
+
if (!m) {
|
|
11
|
+
return { ok: false, error: `invalid reset time format: ${value}` };
|
|
12
|
+
}
|
|
13
|
+
const hour = Number(m[1]);
|
|
14
|
+
const minute = Number(m[2]);
|
|
15
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
16
|
+
return { ok: false, error: `invalid reset time value: ${value}` };
|
|
17
|
+
}
|
|
18
|
+
return { ok: true, kind: isUtc ? 'utc' : 'local', hour, minute };
|
|
19
|
+
}
|
|
20
|
+
export function computeNextDailyResetAtMs(args) {
|
|
21
|
+
const parsed = parseDailyResetTime(args.resetTime);
|
|
22
|
+
const fallback = { ok: true, kind: 'local', hour: 12, minute: 0 };
|
|
23
|
+
const resolved = parsed.ok ? parsed : fallback;
|
|
24
|
+
const kind = resolved.kind;
|
|
25
|
+
const hour = resolved.hour;
|
|
26
|
+
const minute = resolved.minute;
|
|
27
|
+
const resetTimeNormalized = `${pad2(hour)}:${pad2(minute)}${kind === 'utc' ? 'Z' : ''}`;
|
|
28
|
+
const now = new Date(args.nowMs);
|
|
29
|
+
let candidate;
|
|
30
|
+
if (kind === 'utc') {
|
|
31
|
+
candidate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hour, minute, 0, 0));
|
|
32
|
+
if (candidate.getTime() <= args.nowMs) {
|
|
33
|
+
candidate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, hour, minute, 0, 0));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, 0, 0);
|
|
38
|
+
if (candidate.getTime() <= args.nowMs) {
|
|
39
|
+
candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, hour, minute, 0, 0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { resetAtMs: candidate.getTime(), resetTimeNormalized };
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { QuotaManager } from './quota-manager.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ProviderErrorEvent, ProviderSuccessEvent, ProviderQuotaView } from '../router/virtual-router/types.js';
|
|
2
|
+
import type { QuotaState, QuotaStore, StaticQuotaConfig } from './types.js';
|
|
3
|
+
export declare class QuotaManager {
|
|
4
|
+
private readonly staticConfigs;
|
|
5
|
+
private readonly states;
|
|
6
|
+
private readonly store;
|
|
7
|
+
private persistTimer;
|
|
8
|
+
private dirty;
|
|
9
|
+
constructor(options?: {
|
|
10
|
+
store?: QuotaStore | null;
|
|
11
|
+
});
|
|
12
|
+
hydrateFromStore(): Promise<void>;
|
|
13
|
+
registerProviderStaticConfig(providerKey: string, cfg: StaticQuotaConfig): void;
|
|
14
|
+
ensureProvider(providerKey: string): QuotaState;
|
|
15
|
+
disableProvider(options: {
|
|
16
|
+
providerKey: string;
|
|
17
|
+
mode: 'cooldown' | 'blacklist';
|
|
18
|
+
durationMs: number;
|
|
19
|
+
reason?: string;
|
|
20
|
+
}): void;
|
|
21
|
+
recoverProvider(providerKey: string): void;
|
|
22
|
+
resetProvider(providerKey: string): void;
|
|
23
|
+
onProviderError(ev: ProviderErrorEvent): void;
|
|
24
|
+
onProviderSuccess(ev: ProviderSuccessEvent): void;
|
|
25
|
+
/**
|
|
26
|
+
* External quota snapshot ingestion hook (host-driven).
|
|
27
|
+
* This API is intentionally small: the host adapter translates provider-specific
|
|
28
|
+
* quota responses into a normalized per-providerKey inPool/cooldown/blacklist update.
|
|
29
|
+
*/
|
|
30
|
+
updateProviderPoolState(options: {
|
|
31
|
+
providerKey: string;
|
|
32
|
+
inPool: boolean;
|
|
33
|
+
reason?: string | null;
|
|
34
|
+
cooldownUntil?: number | null;
|
|
35
|
+
blacklistUntil?: number | null;
|
|
36
|
+
}): void;
|
|
37
|
+
getQuotaView(): ProviderQuotaView;
|
|
38
|
+
getSnapshot(): {
|
|
39
|
+
updatedAtMs: number;
|
|
40
|
+
providers: Record<string, QuotaState>;
|
|
41
|
+
};
|
|
42
|
+
private markDirty;
|
|
43
|
+
persistNow(): Promise<void>;
|
|
44
|
+
}
|