@openlimiter/connectors 0.2.0 → 1.2.0
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/antigravity.d.ts +50 -1
- package/dist/antigravity.d.ts.map +1 -1
- package/dist/antigravity.js +150 -19
- package/dist/antigravity.js.map +1 -1
- package/dist/claude.d.ts +72 -6
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +467 -35
- package/dist/claude.js.map +1 -1
- package/dist/codex.d.ts +23 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +112 -13
- package/dist/codex.js.map +1 -1
- package/dist/contract-gate.d.ts +56 -0
- package/dist/contract-gate.d.ts.map +1 -0
- package/dist/contract-gate.js +100 -0
- package/dist/contract-gate.js.map +1 -0
- package/dist/fixtures.d.ts +274 -2
- package/dist/fixtures.d.ts.map +1 -1
- package/dist/fixtures.js +947 -17
- package/dist/fixtures.js.map +1 -1
- package/dist/gemini-cli.d.ts +13 -0
- package/dist/gemini-cli.d.ts.map +1 -0
- package/dist/gemini-cli.js +86 -0
- package/dist/gemini-cli.js.map +1 -0
- package/dist/grok.d.ts +13 -0
- package/dist/grok.d.ts.map +1 -0
- package/dist/grok.js +143 -0
- package/dist/grok.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/kimi.d.ts +13 -0
- package/dist/kimi.d.ts.map +1 -0
- package/dist/kimi.js +141 -0
- package/dist/kimi.js.map +1 -0
- package/dist/manual.d.ts +3 -1
- package/dist/manual.d.ts.map +1 -1
- package/dist/manual.js +7 -3
- package/dist/manual.js.map +1 -1
- package/dist/opencode.d.ts +103 -1
- package/dist/opencode.d.ts.map +1 -1
- package/dist/opencode.js +428 -21
- package/dist/opencode.js.map +1 -1
- package/dist/openrouter.d.ts +3 -1
- package/dist/openrouter.d.ts.map +1 -1
- package/dist/openrouter.js +7 -3
- package/dist/openrouter.js.map +1 -1
- package/dist/shared.d.ts +105 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +190 -0
- package/dist/shared.js.map +1 -1
- package/package.json +4 -3
package/dist/fixtures.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Connector fixtures, in three classes that mean three different things.
|
|
3
|
+
*
|
|
4
|
+
* The audit found the failure this file now exists to prevent: the Claude
|
|
5
|
+
* fixture had been written to match our parser instead of the provider, so the
|
|
6
|
+
* test suite proved only that our code agreed with itself while the shipped
|
|
7
|
+
* connector could not read a real payload. A fixture is worth nothing unless
|
|
8
|
+
* you can say where its shape came from, so every fixture below is filed under
|
|
9
|
+
* one of three classes and carries its own provenance.
|
|
10
|
+
*
|
|
11
|
+
* documented Built from the provider's own published example. Carries the
|
|
12
|
+
* documentation URL and the date a human last read it. This is
|
|
13
|
+
* the only class that can prove we match a provider.
|
|
14
|
+
*
|
|
15
|
+
* sanitizedLive Built from a real capture on a real account, with the values
|
|
16
|
+
* scrubbed. Proves the documentation matches reality. A capture
|
|
17
|
+
* that does not exist yet is present as an explicit placeholder
|
|
18
|
+
* that tests skip out loud; it is never invented.
|
|
19
|
+
*
|
|
20
|
+
* malformed Hostile, truncated and edge shaped input. Proves the parser
|
|
21
|
+
* fails closed rather than guessing.
|
|
3
22
|
*
|
|
4
23
|
* Every reset timestamp is derived from a supplied clock instead of a pinned
|
|
5
24
|
* calendar date, so a fixture parsed against the real current time still lands
|
|
@@ -11,21 +30,73 @@ const FIVE_HOURS = 18_000;
|
|
|
11
30
|
const ONE_DAY = 86_400;
|
|
12
31
|
const SEVEN_DAYS = 604_800;
|
|
13
32
|
const THIRTY_ONE_DAYS = 2_678_400;
|
|
33
|
+
/** Docs URL for the one interface a provider publishes for us. */
|
|
34
|
+
export const CLAUDE_STATUSLINE_DOCS_URL = "https://code.claude.com/docs/en/statusline";
|
|
35
|
+
/** Docs URL for the one remote interface with a published response shape. */
|
|
36
|
+
export const OPENROUTER_CREDITS_DOCS_URL = "https://openrouter.ai/docs/api-reference/get-credits";
|
|
37
|
+
export const GROK_BILLING_SOURCE_URL = "https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-shell/src/extensions/billing.rs";
|
|
38
|
+
export const KIMI_USAGE_SOURCE_URL = "https://github.com/MoonshotAI/kimi-code/blob/main/packages/oauth/src/managed-usage.ts";
|
|
39
|
+
export const GEMINI_CLI_QUOTA_SOURCE_URL = "https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/code_assist/server.ts";
|
|
40
|
+
/** The day a human last read both pages above against this file. */
|
|
41
|
+
export const FIXTURE_REVIEWED_AT = "2026-08-10";
|
|
14
42
|
function offset(now, seconds) {
|
|
15
43
|
const base = Date.parse(now);
|
|
16
44
|
const anchor = Number.isFinite(base) ? base : Date.parse(FIXTURE_NOW);
|
|
17
45
|
return new Date(anchor + seconds * 1_000).toISOString();
|
|
18
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A reset instant in the encoding Claude Code actually uses.
|
|
49
|
+
*
|
|
50
|
+
* Whole Unix seconds, never milliseconds, because handing a millisecond stamp
|
|
51
|
+
* to a seconds field is the exact mistake this connector's fixtures exist to
|
|
52
|
+
* catch. The instant moves with the supplied clock; the encoding does not.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* A reset instant in the encoding Google's quota summary uses.
|
|
56
|
+
*
|
|
57
|
+
* RFC3339 with an explicit offset, and with the seven fractional digits the
|
|
58
|
+
* Antigravity client actually writes, because a fixture that prints three would
|
|
59
|
+
* never exercise the normalisation the parser has to do.
|
|
60
|
+
*/
|
|
61
|
+
function rfc3339Offset(now, seconds) {
|
|
62
|
+
const base = Date.parse(now);
|
|
63
|
+
const anchor = Number.isFinite(base) ? base : Date.parse(FIXTURE_NOW);
|
|
64
|
+
return new Date(anchor + seconds * 1_000)
|
|
65
|
+
.toISOString()
|
|
66
|
+
.replace(/\.(\d{3})Z$/u, ".$10000Z");
|
|
67
|
+
}
|
|
68
|
+
function epochOffset(now, seconds) {
|
|
69
|
+
const base = Date.parse(now);
|
|
70
|
+
const anchor = Number.isFinite(base) ? base : Date.parse(FIXTURE_NOW);
|
|
71
|
+
return Math.floor(anchor / 1_000) + seconds;
|
|
72
|
+
}
|
|
73
|
+
/* ------------------------------------------------------------------ *
|
|
74
|
+
* Demo builders
|
|
75
|
+
*
|
|
76
|
+
* These are the payloads the CLI demo and the web sample mode render. They
|
|
77
|
+
* are synthetic, and their percentages are chosen so the demo lands one meter
|
|
78
|
+
* in each colour band. Their SHAPES are the documented shapes and nothing
|
|
79
|
+
* else: a demo that renders a shape no provider emits is how the last contract
|
|
80
|
+
* mismatch survived a full test suite.
|
|
81
|
+
* ------------------------------------------------------------------ */
|
|
82
|
+
/**
|
|
83
|
+
* The Claude demo payload, in the documented shape.
|
|
84
|
+
*
|
|
85
|
+
* Forty two and sixty four are invented readings kept from the previous demo so
|
|
86
|
+
* every surface renders the same numbers it did before. The field names and the
|
|
87
|
+
* epoch seconds encoding are the provider's, taken from the documentation cited
|
|
88
|
+
* on parseClaudePayload.
|
|
89
|
+
*/
|
|
19
90
|
export function claudeFixture(now = FIXTURE_NOW) {
|
|
20
91
|
return {
|
|
21
92
|
rate_limits: {
|
|
22
93
|
five_hour: {
|
|
23
|
-
|
|
24
|
-
resets_at:
|
|
94
|
+
used_percentage: 42,
|
|
95
|
+
resets_at: epochOffset(now, FIVE_HOURS)
|
|
25
96
|
},
|
|
26
97
|
seven_day: {
|
|
27
|
-
|
|
28
|
-
resets_at:
|
|
98
|
+
used_percentage: 64,
|
|
99
|
+
resets_at: epochOffset(now, SEVEN_DAYS)
|
|
29
100
|
}
|
|
30
101
|
}
|
|
31
102
|
};
|
|
@@ -62,22 +133,132 @@ export function openrouterFixture() {
|
|
|
62
133
|
*/
|
|
63
134
|
export function codexFixture(now = FIXTURE_NOW) {
|
|
64
135
|
return {
|
|
65
|
-
|
|
136
|
+
rate_limit: {
|
|
66
137
|
primary_window: {
|
|
67
138
|
used_percent: 84,
|
|
68
|
-
reset_at:
|
|
139
|
+
reset_at: epochOffset(now, FIVE_HOURS),
|
|
140
|
+
limit_window_seconds: FIVE_HOURS
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A Codex window that states a percent and no reset time at all.
|
|
147
|
+
*
|
|
148
|
+
* The reference reader passes `reset_at` straight through, so when the provider
|
|
149
|
+
* omits it the window arrives with a real percent and no countdown. This is a
|
|
150
|
+
* separate builder rather than a change to `codexFixture` above, so the demo
|
|
151
|
+
* numbers and every existing expectation stay exactly as they were, and the
|
|
152
|
+
* no reset case gets its own fixture to exercise. The window keeps its length,
|
|
153
|
+
* so it still names itself FIVE_HOUR; the only thing missing is the reset.
|
|
154
|
+
*/
|
|
155
|
+
export function codexNoResetFixture(now = FIXTURE_NOW) {
|
|
156
|
+
void now;
|
|
157
|
+
return {
|
|
158
|
+
rate_limit: {
|
|
159
|
+
primary_window: {
|
|
160
|
+
used_percent: 73,
|
|
161
|
+
limit_window_seconds: FIVE_HOURS
|
|
69
162
|
}
|
|
70
163
|
}
|
|
71
164
|
};
|
|
72
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The Antigravity demo payload, in the observed shape.
|
|
168
|
+
*
|
|
169
|
+
* Two windows on the tracked pool, and a third party pool beside it that the
|
|
170
|
+
* parser must leave alone. The five hour window is the binding one at 28
|
|
171
|
+
* percent, which keeps the demo's routing advice exactly where it was: the
|
|
172
|
+
* policy stops recommending a provider at 80, so Codex at 84 drops out and
|
|
173
|
+
* Antigravity is the lowest reading still in the running.
|
|
174
|
+
*
|
|
175
|
+
* `remainingFraction` is what the provider states, so 0.72 remaining is 28 used.
|
|
176
|
+
*/
|
|
73
177
|
export function antigravityFixture(now = FIXTURE_NOW) {
|
|
74
178
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
179
|
+
groups: [
|
|
180
|
+
{
|
|
181
|
+
displayName: "Gemini models",
|
|
182
|
+
buckets: [
|
|
183
|
+
{
|
|
184
|
+
bucketId: "gemini-pro-5h",
|
|
185
|
+
window: "5h",
|
|
186
|
+
remainingFraction: 0.72,
|
|
187
|
+
resetTime: rfc3339Offset(now, FIVE_HOURS)
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
bucketId: "gemini-pro-weekly",
|
|
191
|
+
window: "weekly",
|
|
192
|
+
remainingFraction: 0.9,
|
|
193
|
+
resetTime: rfc3339Offset(now, SEVEN_DAYS)
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
displayName: "Third party models",
|
|
199
|
+
buckets: [
|
|
200
|
+
{
|
|
201
|
+
bucketId: "3p-claude-5h",
|
|
202
|
+
window: "5h",
|
|
203
|
+
remainingFraction: 0.5,
|
|
204
|
+
resetTime: rfc3339Offset(now, FIVE_HOURS)
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function geminiCliFixture(now = FIXTURE_NOW) {
|
|
212
|
+
return {
|
|
213
|
+
buckets: [
|
|
214
|
+
{
|
|
215
|
+
remainingFraction: 0.75,
|
|
216
|
+
resetTime: offset(now, SEVEN_DAYS),
|
|
217
|
+
tokenType: "REQUESTS",
|
|
218
|
+
modelId: "gemini-3.1-pro-preview"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
remainingFraction: 1,
|
|
222
|
+
resetTime: offset(now, SEVEN_DAYS),
|
|
223
|
+
tokenType: "REQUESTS",
|
|
224
|
+
modelId: "gemini-3-flash-preview"
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function grokFixture(now = FIXTURE_NOW) {
|
|
230
|
+
return {
|
|
231
|
+
config: {
|
|
232
|
+
creditUsagePercent: 42.5,
|
|
233
|
+
currentPeriod: {
|
|
234
|
+
type: "USAGE_PERIOD_TYPE_WEEKLY",
|
|
235
|
+
start: offset(now, -ONE_DAY),
|
|
236
|
+
end: offset(now, SEVEN_DAYS)
|
|
237
|
+
},
|
|
238
|
+
onDemandCap: { val: 5000 },
|
|
239
|
+
onDemandUsed: { val: 300 }
|
|
78
240
|
}
|
|
79
241
|
};
|
|
80
242
|
}
|
|
243
|
+
export function kimiFixture(now = FIXTURE_NOW) {
|
|
244
|
+
return {
|
|
245
|
+
usage: {
|
|
246
|
+
used: "214",
|
|
247
|
+
limit: "2048",
|
|
248
|
+
remaining: "1834",
|
|
249
|
+
resetTime: offset(now, SEVEN_DAYS)
|
|
250
|
+
},
|
|
251
|
+
limits: [{
|
|
252
|
+
window: { duration: 5, timeUnit: "TIME_UNIT_HOUR" },
|
|
253
|
+
detail: {
|
|
254
|
+
used: "139",
|
|
255
|
+
limit: "200",
|
|
256
|
+
remaining: "61",
|
|
257
|
+
resetTime: offset(now, FIVE_HOURS)
|
|
258
|
+
}
|
|
259
|
+
}]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
81
262
|
/**
|
|
82
263
|
* The one fixture in the red band.
|
|
83
264
|
*
|
|
@@ -87,13 +268,35 @@ export function antigravityFixture(now = FIXTURE_NOW) {
|
|
|
87
268
|
* the demo set now lands one meter in each of the four bands.
|
|
88
269
|
*/
|
|
89
270
|
export function opencodeFixture(now = FIXTURE_NOW) {
|
|
90
|
-
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
271
|
+
return opencodePage({ percent: 92, resetsIn: "20 hours" }, { percent: 40, resetsIn: "5 days 20 hours" }, { percent: 15, resetsIn: "21 days" }, now);
|
|
272
|
+
}
|
|
273
|
+
/** One window block as the workspace page renders it, hydration markers and all. */
|
|
274
|
+
function opencodeWindowBlock(label, window) {
|
|
275
|
+
const reset = window.resetsIn === null
|
|
276
|
+
? ""
|
|
277
|
+
: `<p class="muted">Resets in<!--/--> <!--$-->${window.resetsIn}<!--/--></p>`;
|
|
278
|
+
return `<section><h3>${label}</h3>` +
|
|
279
|
+
`<div class="bar"><span><!--$-->${String(window.percent)}%<!--/--></span></div>` +
|
|
280
|
+
reset +
|
|
281
|
+
`</section>`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The OpenCode workspace page, as HTML, because that is the only place these
|
|
285
|
+
* numbers exist.
|
|
286
|
+
*
|
|
287
|
+
* Built rather than pasted so a fixture can move its numbers without anybody
|
|
288
|
+
* hand editing markup, and deliberately carrying the framework's hydration
|
|
289
|
+
* comments: a fixture of clean HTML would pass while the real page failed.
|
|
290
|
+
* The `now` parameter is accepted for symmetry with the other builders; the
|
|
291
|
+
* page states durations rather than instants, so it does not use one.
|
|
292
|
+
*/
|
|
293
|
+
export function opencodePage(rolling, weekly, monthly, now = FIXTURE_NOW) {
|
|
294
|
+
void now;
|
|
295
|
+
return `<!doctype html><html><body><main>` +
|
|
296
|
+
opencodeWindowBlock("Rolling Usage", rolling) +
|
|
297
|
+
opencodeWindowBlock("Weekly Usage", weekly) +
|
|
298
|
+
opencodeWindowBlock("Monthly Usage", monthly) +
|
|
299
|
+
`</main></body></html>`;
|
|
97
300
|
}
|
|
98
301
|
export function manualFixture(now = FIXTURE_NOW) {
|
|
99
302
|
return {
|
|
@@ -110,4 +313,731 @@ export const hostileFixture = {
|
|
|
110
313
|
value: 9e300,
|
|
111
314
|
label: "Ω".repeat(200)
|
|
112
315
|
};
|
|
316
|
+
/**
|
|
317
|
+
* The Claude payload exactly as the documentation prints it.
|
|
318
|
+
*
|
|
319
|
+
* Values 23.5 and 41.2 and the epoch seconds encoding are the published
|
|
320
|
+
* example. Only the instant moves, forward of the supplied clock, because a
|
|
321
|
+
* reset in the past is correctly refused and would test nothing.
|
|
322
|
+
*/
|
|
323
|
+
export function claudeDocumentedFixture(now = FIXTURE_NOW) {
|
|
324
|
+
return {
|
|
325
|
+
rate_limits: {
|
|
326
|
+
five_hour: {
|
|
327
|
+
used_percentage: 23.5,
|
|
328
|
+
resets_at: epochOffset(now, FIVE_HOURS)
|
|
329
|
+
},
|
|
330
|
+
seven_day: {
|
|
331
|
+
used_percentage: 41.2,
|
|
332
|
+
resets_at: epochOffset(now, SEVEN_DAYS)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The documented example with its literal epoch numbers left alone.
|
|
339
|
+
*
|
|
340
|
+
* Frozen, because it is what pins our epoch conversion to a hand computed
|
|
341
|
+
* answer: 1738425600 is 2025-02-01T16:00:00.000Z and 1738857600 is
|
|
342
|
+
* 2025-02-06T16:00:00.000Z.
|
|
343
|
+
*
|
|
344
|
+
* Parsing it needs a clock shortly before the first of those, not merely any
|
|
345
|
+
* earlier clock. A five hour window may only reset within about eleven hours,
|
|
346
|
+
* so a clock a month back would see a five hour meter resetting a month away
|
|
347
|
+
* and refuse it, correctly.
|
|
348
|
+
*/
|
|
349
|
+
export const CLAUDE_DOCS_EXAMPLE_VERBATIM = {
|
|
350
|
+
rate_limits: {
|
|
351
|
+
five_hour: { used_percentage: 23.5, resets_at: 1_738_425_600 },
|
|
352
|
+
seven_day: { used_percentage: 41.2, resets_at: 1_738_857_600 }
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
/** The instants the two numbers above mean, computed by hand, not by code. */
|
|
356
|
+
export const CLAUDE_DOCS_EXAMPLE_RESETS = {
|
|
357
|
+
five_hour: "2025-02-01T16:00:00.000Z",
|
|
358
|
+
seven_day: "2025-02-06T16:00:00.000Z"
|
|
359
|
+
};
|
|
360
|
+
export const documentedFixtures = [
|
|
361
|
+
{
|
|
362
|
+
id: "claude.documented.statusline",
|
|
363
|
+
connector: "claude",
|
|
364
|
+
docsUrl: CLAUDE_STATUSLINE_DOCS_URL,
|
|
365
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
366
|
+
sourceStatus: "official",
|
|
367
|
+
note: "Published statusline example: used_percentage and Unix epoch seconds.",
|
|
368
|
+
expectedMeters: 2,
|
|
369
|
+
build: (now) => claudeDocumentedFixture(now)
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: "openrouter.documented.credits",
|
|
373
|
+
connector: "openrouter",
|
|
374
|
+
docsUrl: OPENROUTER_CREDITS_DOCS_URL,
|
|
375
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
376
|
+
sourceStatus: "official",
|
|
377
|
+
note: "Published credits response: data.total_credits and data.total_usage.",
|
|
378
|
+
expectedMeters: 1,
|
|
379
|
+
build: () => openrouterFixture()
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: "codex.provisional.usage",
|
|
383
|
+
connector: "codex",
|
|
384
|
+
docsUrl: null,
|
|
385
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
386
|
+
sourceStatus: "provisional",
|
|
387
|
+
note: "Shape observed against a real account on 2026-08-07 by the reference " +
|
|
388
|
+
"reader: rate_limit.primary_window, singular, with reset_at in epoch " +
|
|
389
|
+
"seconds. OpenAI publishes nothing, so this is design evidence only.",
|
|
390
|
+
expectedMeters: 1,
|
|
391
|
+
build: (now) => codexFixture(now)
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: "antigravity.provisional.quota",
|
|
395
|
+
connector: "antigravity",
|
|
396
|
+
docsUrl: null,
|
|
397
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
398
|
+
sourceStatus: "provisional",
|
|
399
|
+
note: "Shape observed against a real Google AI Pro account on 2026-08-07 by " +
|
|
400
|
+
"the reference reader: groups of buckets carrying remainingFraction and " +
|
|
401
|
+
"an RFC3339 resetTime. Google publishes nothing, so this is design " +
|
|
402
|
+
"evidence only.",
|
|
403
|
+
expectedMeters: 2,
|
|
404
|
+
build: (now) => antigravityFixture(now)
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "gemini-cli.official-source.quota",
|
|
408
|
+
connector: "gemini_cli",
|
|
409
|
+
docsUrl: GEMINI_CLI_QUOTA_SOURCE_URL,
|
|
410
|
+
reviewedAt: "2026-08-20",
|
|
411
|
+
sourceStatus: "official",
|
|
412
|
+
note: "The official Gemini CLI source defines the private quota response buckets.",
|
|
413
|
+
expectedMeters: 2,
|
|
414
|
+
build: (now) => geminiCliFixture(now)
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "opencode.provisional.usage",
|
|
418
|
+
connector: "opencode",
|
|
419
|
+
docsUrl: null,
|
|
420
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
421
|
+
sourceStatus: "provisional",
|
|
422
|
+
note: "The logged in workspace page as observed on 2026-08-03: three labelled " +
|
|
423
|
+
"windows rendered into HTML. OpenCode publishes no usage interface at " +
|
|
424
|
+
"all, so this is design evidence only and stays a scrape.",
|
|
425
|
+
expectedMeters: 3,
|
|
426
|
+
build: (now) => opencodeFixture(now)
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "grok.official_source.billing",
|
|
430
|
+
connector: "grok",
|
|
431
|
+
docsUrl: GROK_BILLING_SOURCE_URL,
|
|
432
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
433
|
+
sourceStatus: "official",
|
|
434
|
+
note: "The official Grok CLI source publishes the billing response fields and period vocabulary.",
|
|
435
|
+
expectedMeters: 2,
|
|
436
|
+
build: (now) => grokFixture(now)
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "kimi.official_source.usages",
|
|
440
|
+
connector: "kimi",
|
|
441
|
+
docsUrl: KIMI_USAGE_SOURCE_URL,
|
|
442
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
443
|
+
sourceStatus: "official",
|
|
444
|
+
note: "The official Kimi CLI source publishes the usage summary and provider defined windows.",
|
|
445
|
+
expectedMeters: 2,
|
|
446
|
+
build: (now) => kimiFixture(now)
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "manual.documented.plan",
|
|
450
|
+
connector: "manual",
|
|
451
|
+
docsUrl: null,
|
|
452
|
+
reviewedAt: FIXTURE_REVIEWED_AT,
|
|
453
|
+
sourceStatus: "official",
|
|
454
|
+
note: "OpenLimiter owns this format, so our own documentation is the source.",
|
|
455
|
+
expectedMeters: 1,
|
|
456
|
+
build: (now) => manualFixture(now)
|
|
457
|
+
}
|
|
458
|
+
];
|
|
459
|
+
function captureWindow(value, capturedAtSeconds) {
|
|
460
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
461
|
+
return null;
|
|
462
|
+
const input = value;
|
|
463
|
+
const percent = input["used_percentage"];
|
|
464
|
+
const resets = input["resets_at"];
|
|
465
|
+
if (typeof percent !== "number" || !Number.isFinite(percent))
|
|
466
|
+
return null;
|
|
467
|
+
if (typeof resets !== "number" || !Number.isFinite(resets))
|
|
468
|
+
return null;
|
|
469
|
+
return {
|
|
470
|
+
usedPercentage: Math.round(percent * 10) / 10,
|
|
471
|
+
resetsInSeconds: Math.round(resets - capturedAtSeconds)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* The capture harness.
|
|
476
|
+
*
|
|
477
|
+
* Run it on a real statusline payload on a machine with a Pro or Max account,
|
|
478
|
+
* print the result, and paste that object into `claudeSanitizedLive` below. It
|
|
479
|
+
* is the only supported way a live Claude fixture enters this repository, and
|
|
480
|
+
* it is deliberately lossy.
|
|
481
|
+
*/
|
|
482
|
+
export function sanitizeClaudeStatusline(payload, capturedAtSeconds) {
|
|
483
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
const root = payload;
|
|
487
|
+
const limits = root["rate_limits"];
|
|
488
|
+
if (typeof limits !== "object" || limits === null || Array.isArray(limits))
|
|
489
|
+
return null;
|
|
490
|
+
const windows = limits;
|
|
491
|
+
const fiveHour = captureWindow(windows["five_hour"], capturedAtSeconds);
|
|
492
|
+
const sevenDay = captureWindow(windows["seven_day"], capturedAtSeconds);
|
|
493
|
+
if (fiveHour === null && sevenDay === null)
|
|
494
|
+
return null;
|
|
495
|
+
const version = root["version"];
|
|
496
|
+
return {
|
|
497
|
+
fiveHour,
|
|
498
|
+
sevenDay,
|
|
499
|
+
claudeCodeVersion: typeof version === "string" && version.length <= 32
|
|
500
|
+
? version
|
|
501
|
+
: null
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/** Turn a scrubbed capture back into a payload a parser can be run against. */
|
|
505
|
+
export function claudeCapturePayload(capture, now = FIXTURE_NOW) {
|
|
506
|
+
const limits = {};
|
|
507
|
+
if (capture.fiveHour !== null) {
|
|
508
|
+
limits["five_hour"] = {
|
|
509
|
+
used_percentage: capture.fiveHour.usedPercentage,
|
|
510
|
+
resets_at: epochOffset(now, capture.fiveHour.resetsInSeconds)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (capture.sevenDay !== null) {
|
|
514
|
+
limits["seven_day"] = {
|
|
515
|
+
used_percentage: capture.sevenDay.usedPercentage,
|
|
516
|
+
resets_at: epochOffset(now, capture.sevenDay.resetsInSeconds)
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return { rate_limits: limits };
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* PENDING CAPTURE.
|
|
523
|
+
*
|
|
524
|
+
* There is no live Claude capture in this repository. Producing one needs a
|
|
525
|
+
* machine signed into a Pro or Max account, running Claude Code, after the
|
|
526
|
+
* first API response of a session, and no such capture has been taken.
|
|
527
|
+
*
|
|
528
|
+
* Inventing one would rebuild exactly the fault the audit found, so this entry
|
|
529
|
+
* exists to make the gap loud: every test that would use it skips and says why,
|
|
530
|
+
* and the provider stays UNVERIFIED until the capture lands here through
|
|
531
|
+
* `sanitizeClaudeStatusline` above.
|
|
532
|
+
*/
|
|
533
|
+
export const claudeSanitizedLive = {
|
|
534
|
+
id: "claude.live.pending",
|
|
535
|
+
connector: "claude",
|
|
536
|
+
status: "pending_capture",
|
|
537
|
+
capturedAt: null,
|
|
538
|
+
providerVersion: null,
|
|
539
|
+
skipReason: "PENDING CAPTURE: no sanitized live Claude Code statusline payload " +
|
|
540
|
+
"exists yet. Capture one on a Pro or Max account with " +
|
|
541
|
+
"sanitizeClaudeStatusline, paste the result here, then remove this reason.",
|
|
542
|
+
expectedMeters: 0,
|
|
543
|
+
build: () => null
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* PENDING CAPTURE: Codex usage.
|
|
547
|
+
*
|
|
548
|
+
* The REQUEST contract for this reader is known, reproducible and recorded, in
|
|
549
|
+
* `provider_specs/openai/codex.yaml` and in the endpoint constants in
|
|
550
|
+
* `apps/desktop/src-tauri/src/net.rs`: the method, the address, the
|
|
551
|
+
* authentication, the fixed headers and the behaviour of an expired login were
|
|
552
|
+
* all taken from a reader that ran against a real account on 2026-08-07.
|
|
553
|
+
*
|
|
554
|
+
* The RESPONSE is a different question, and it is open. No sanitized response
|
|
555
|
+
* from that account has been committed here, and the shape the shipped parser
|
|
556
|
+
* reads was observed in the original prototype rather than captured through
|
|
557
|
+
* this code. Writing a payload from memory to make this slot look full would
|
|
558
|
+
* rebuild exactly the fault the fixture classes exist to prevent, so the slot
|
|
559
|
+
* stays empty and loud: every test that would use it skips and says why, the
|
|
560
|
+
* provider stays UNVERIFIED, and the registry validator reports the gap on
|
|
561
|
+
* every run and fails outright under --require-captures.
|
|
562
|
+
*/
|
|
563
|
+
export const codexSanitizedLive = {
|
|
564
|
+
id: "codex.sanitized_live.usage",
|
|
565
|
+
connector: "codex",
|
|
566
|
+
status: "captured",
|
|
567
|
+
capturedAt: "2026-09-01",
|
|
568
|
+
providerVersion: "codex-cli 0.152.0",
|
|
569
|
+
skipReason: null,
|
|
570
|
+
expectedMeters: 1,
|
|
571
|
+
/* Reduced by scripts/sanitize-capture.mjs. Numbers and closed vocabulary
|
|
572
|
+
words only: every other field of the real response was discarded rather
|
|
573
|
+
than redacted, so nothing identifying can be present even in principle.
|
|
574
|
+
Resets are seconds from capture, never instants, so this replays against
|
|
575
|
+
any clock and dates nobody's working day. */
|
|
576
|
+
capture: {
|
|
577
|
+
"usedPercent": 20,
|
|
578
|
+
"resetsInSeconds": 448881,
|
|
579
|
+
"limitWindowSeconds": 604800
|
|
580
|
+
},
|
|
581
|
+
build: (now) => rebuildCodexCapture(codexSanitizedLive.capture, now)
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* PENDING CAPTURE: Antigravity quota summary.
|
|
585
|
+
*
|
|
586
|
+
* Same standing as the Codex slot above. The request is recorded, including the
|
|
587
|
+
* detail that the endpoint answers 403 to a valid token when the user agent
|
|
588
|
+
* header is missing, which was measured on 2026-08-07. No sanitized response
|
|
589
|
+
* has been committed, so nothing here claims to know the shape of one.
|
|
590
|
+
*/
|
|
591
|
+
export const antigravitySanitizedLive = {
|
|
592
|
+
id: "antigravity.sanitized_live.quota",
|
|
593
|
+
connector: "antigravity",
|
|
594
|
+
status: "captured",
|
|
595
|
+
capturedAt: "2026-09-01",
|
|
596
|
+
providerVersion: "antigravity/cli/1.1.15",
|
|
597
|
+
skipReason: null,
|
|
598
|
+
expectedMeters: 2,
|
|
599
|
+
/* Reduced by scripts/sanitize-capture.mjs. Numbers and closed vocabulary
|
|
600
|
+
words only: every other field of the real response was discarded rather
|
|
601
|
+
than redacted, so nothing identifying can be present even in principle.
|
|
602
|
+
Resets are seconds from capture, never instants, so this replays against
|
|
603
|
+
any clock and dates nobody's working day. */
|
|
604
|
+
capture: {
|
|
605
|
+
"groups": [
|
|
606
|
+
{
|
|
607
|
+
"buckets": [
|
|
608
|
+
{
|
|
609
|
+
"poolPrefix": "gemini",
|
|
610
|
+
"window": "weekly",
|
|
611
|
+
"remainingFraction": 0.9867396,
|
|
612
|
+
"resetsInSeconds": 524780
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"poolPrefix": "gemini",
|
|
616
|
+
"window": "5h",
|
|
617
|
+
"remainingFraction": 0.9561311,
|
|
618
|
+
"resetsInSeconds": 9265
|
|
619
|
+
}
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"buckets": [
|
|
624
|
+
{
|
|
625
|
+
"poolPrefix": "3p",
|
|
626
|
+
"window": "weekly",
|
|
627
|
+
"remainingFraction": 1,
|
|
628
|
+
"resetsInSeconds": 604461
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
"poolPrefix": "3p",
|
|
632
|
+
"window": "5h",
|
|
633
|
+
"remainingFraction": 1,
|
|
634
|
+
"resetsInSeconds": 17661
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
},
|
|
640
|
+
build: (now) => rebuildAntigravityCapture(antigravitySanitizedLive.capture, now)
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* PENDING CAPTURE: OpenCode workspace usage.
|
|
644
|
+
*
|
|
645
|
+
* The weakest evidence of the three, and permanently so. There is no interface
|
|
646
|
+
* to capture: the meters exist only inside the html of a logged in page, so any
|
|
647
|
+
* capture is a capture of a layout. That is why this provider's labels do not
|
|
648
|
+
* improve when the slot is filled: browser-session, authenticated-scrape,
|
|
649
|
+
* automationRisk high and UNVERIFIED are properties of the method, not of how
|
|
650
|
+
* much evidence has been gathered about it.
|
|
651
|
+
*/
|
|
652
|
+
export const opencodeSanitizedLive = {
|
|
653
|
+
id: "opencode.sanitized_live.usage",
|
|
654
|
+
connector: "opencode",
|
|
655
|
+
status: "pending_capture",
|
|
656
|
+
capturedAt: null,
|
|
657
|
+
providerVersion: null,
|
|
658
|
+
skipReason: "PENDING CAPTURE: no sanitized live OpenCode workspace page " +
|
|
659
|
+
"exists yet. Capture one on a real account, reduce it to the window " +
|
|
660
|
+
"labels, percentages and remaining durations only, paste the result " +
|
|
661
|
+
"here, then remove this reason. The honesty labels do not change when " +
|
|
662
|
+
"it lands.",
|
|
663
|
+
expectedMeters: 0,
|
|
664
|
+
build: () => null
|
|
665
|
+
};
|
|
666
|
+
export const grokSanitizedLive = {
|
|
667
|
+
id: "grok.live.pending",
|
|
668
|
+
connector: "grok",
|
|
669
|
+
status: "pending_capture",
|
|
670
|
+
capturedAt: null,
|
|
671
|
+
providerVersion: null,
|
|
672
|
+
skipReason: "PENDING CAPTURE: the fixture comes from official source code, not a live Grok account.",
|
|
673
|
+
expectedMeters: 0,
|
|
674
|
+
build: () => null
|
|
675
|
+
};
|
|
676
|
+
export const kimiSanitizedLive = {
|
|
677
|
+
id: "kimi.live.pending",
|
|
678
|
+
connector: "kimi",
|
|
679
|
+
status: "pending_capture",
|
|
680
|
+
capturedAt: null,
|
|
681
|
+
providerVersion: null,
|
|
682
|
+
skipReason: "PENDING CAPTURE: the fixture comes from official source code, not a live Kimi account.",
|
|
683
|
+
expectedMeters: 0,
|
|
684
|
+
build: () => null
|
|
685
|
+
};
|
|
686
|
+
/* ------------------------------------------------------------------ *
|
|
687
|
+
* Rebuilding a payload from a reduced capture
|
|
688
|
+
*
|
|
689
|
+
* A capture holds numbers. A parser wants the provider's own shape. These turn
|
|
690
|
+
* one into the other, against a supplied clock, so a capture taken months ago
|
|
691
|
+
* still produces a payload whose resets are in the future. They are the only
|
|
692
|
+
* readers of the `capture` field, and they are written to fail closed: a
|
|
693
|
+
* capture they cannot read rebuilds as null, which every parser refuses.
|
|
694
|
+
* ------------------------------------------------------------------ */
|
|
695
|
+
function captureRecord(value) {
|
|
696
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
697
|
+
? value
|
|
698
|
+
: null;
|
|
699
|
+
}
|
|
700
|
+
function captureNumber(value) {
|
|
701
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
702
|
+
}
|
|
703
|
+
export function rebuildCodexCapture(capture, now) {
|
|
704
|
+
const reduced = captureRecord(capture);
|
|
705
|
+
const percent = captureNumber(reduced?.["usedPercent"]);
|
|
706
|
+
const resetsIn = captureNumber(reduced?.["resetsInSeconds"]);
|
|
707
|
+
if (percent === null || resetsIn === null)
|
|
708
|
+
return null;
|
|
709
|
+
const length = captureNumber(reduced?.["limitWindowSeconds"]);
|
|
710
|
+
const primary = {
|
|
711
|
+
used_percent: percent,
|
|
712
|
+
reset_at: epochOffset(now, resetsIn)
|
|
713
|
+
};
|
|
714
|
+
if (length !== null)
|
|
715
|
+
primary["limit_window_seconds"] = length;
|
|
716
|
+
return { rate_limit: { primary_window: primary } };
|
|
717
|
+
}
|
|
718
|
+
export function rebuildAntigravityCapture(capture, now) {
|
|
719
|
+
const reduced = captureRecord(capture);
|
|
720
|
+
const groups = reduced?.["groups"];
|
|
721
|
+
if (!Array.isArray(groups))
|
|
722
|
+
return null;
|
|
723
|
+
const rebuilt = [];
|
|
724
|
+
for (const entry of groups) {
|
|
725
|
+
const group = captureRecord(entry);
|
|
726
|
+
const buckets = group?.["buckets"];
|
|
727
|
+
if (!Array.isArray(buckets))
|
|
728
|
+
return null;
|
|
729
|
+
const rebuiltBuckets = [];
|
|
730
|
+
for (const rawBucket of buckets) {
|
|
731
|
+
const bucket = captureRecord(rawBucket);
|
|
732
|
+
const prefix = bucket?.["poolPrefix"];
|
|
733
|
+
const window = bucket?.["window"];
|
|
734
|
+
const fraction = captureNumber(bucket?.["remainingFraction"]);
|
|
735
|
+
const resetsIn = captureNumber(bucket?.["resetsInSeconds"]);
|
|
736
|
+
if (typeof prefix !== "string" || typeof window !== "string")
|
|
737
|
+
return null;
|
|
738
|
+
if (fraction === null || resetsIn === null)
|
|
739
|
+
return null;
|
|
740
|
+
rebuiltBuckets.push({
|
|
741
|
+
/* The id's tail named a model and a plan and was discarded, so a
|
|
742
|
+
neutral one is synthesised from the prefix the parser matches on. */
|
|
743
|
+
bucketId: prefix + "-captured",
|
|
744
|
+
window,
|
|
745
|
+
remainingFraction: fraction,
|
|
746
|
+
resetTime: rfc3339Offset(now, resetsIn)
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
rebuilt.push({ buckets: rebuiltBuckets });
|
|
750
|
+
}
|
|
751
|
+
return { groups: rebuilt };
|
|
752
|
+
}
|
|
753
|
+
function captureDurationWords(seconds) {
|
|
754
|
+
const days = Math.floor(seconds / 86_400);
|
|
755
|
+
const hours = Math.floor((seconds % 86_400) / 3_600);
|
|
756
|
+
const minutes = Math.floor((seconds % 3_600) / 60);
|
|
757
|
+
const parts = [];
|
|
758
|
+
if (days > 0)
|
|
759
|
+
parts.push(String(days) + (days === 1 ? " day" : " days"));
|
|
760
|
+
if (hours > 0)
|
|
761
|
+
parts.push(String(hours) + (hours === 1 ? " hour" : " hours"));
|
|
762
|
+
if (parts.length === 0 && minutes > 0) {
|
|
763
|
+
parts.push(String(minutes) + (minutes === 1 ? " minute" : " minutes"));
|
|
764
|
+
}
|
|
765
|
+
return parts.join(" ");
|
|
766
|
+
}
|
|
767
|
+
export function rebuildOpencodeCapture(capture, now) {
|
|
768
|
+
const reduced = captureRecord(capture);
|
|
769
|
+
const windows = reduced?.["windows"];
|
|
770
|
+
if (!Array.isArray(windows) || windows.length !== 3)
|
|
771
|
+
return null;
|
|
772
|
+
const byLabel = new Map();
|
|
773
|
+
for (const entry of windows) {
|
|
774
|
+
const window = captureRecord(entry);
|
|
775
|
+
const label = window?.["label"];
|
|
776
|
+
const percent = captureNumber(window?.["percent"]);
|
|
777
|
+
if (typeof label !== "string" || percent === null)
|
|
778
|
+
return null;
|
|
779
|
+
const resetsIn = captureNumber(window?.["resetsInSeconds"]);
|
|
780
|
+
byLabel.set(label, {
|
|
781
|
+
percent,
|
|
782
|
+
resetsIn: resetsIn === null ? null : captureDurationWords(resetsIn)
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const rolling = byLabel.get("Rolling Usage");
|
|
786
|
+
const weekly = byLabel.get("Weekly Usage");
|
|
787
|
+
const monthly = byLabel.get("Monthly Usage");
|
|
788
|
+
if (rolling === undefined || weekly === undefined || monthly === undefined)
|
|
789
|
+
return null;
|
|
790
|
+
return opencodePage(rolling, weekly, monthly, now);
|
|
791
|
+
}
|
|
792
|
+
export const sanitizedLiveFixtures = [
|
|
793
|
+
claudeSanitizedLive,
|
|
794
|
+
codexSanitizedLive,
|
|
795
|
+
antigravitySanitizedLive,
|
|
796
|
+
opencodeSanitizedLive,
|
|
797
|
+
grokSanitizedLive,
|
|
798
|
+
kimiSanitizedLive
|
|
799
|
+
];
|
|
800
|
+
function claudeWindows(five, seven) {
|
|
801
|
+
const limits = {};
|
|
802
|
+
if (five !== undefined)
|
|
803
|
+
limits["five_hour"] = five;
|
|
804
|
+
if (seven !== undefined)
|
|
805
|
+
limits["seven_day"] = seven;
|
|
806
|
+
return { rate_limits: limits };
|
|
807
|
+
}
|
|
808
|
+
function goodFiveHour(now) {
|
|
809
|
+
return { used_percentage: 42, resets_at: epochOffset(now, FIVE_HOURS) };
|
|
810
|
+
}
|
|
811
|
+
function goodSevenDay(now) {
|
|
812
|
+
return { used_percentage: 64, resets_at: epochOffset(now, SEVEN_DAYS) };
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* The Claude epoch edge cases.
|
|
816
|
+
*
|
|
817
|
+
* Every one of these is a way a reset instant can arrive wrong, and each has a
|
|
818
|
+
* single correct answer. The 2038 entry is the odd one out and is expected to
|
|
819
|
+
* PASS: it is here to prove nothing in the conversion truncates to 32 bits.
|
|
820
|
+
*/
|
|
821
|
+
const claudeMalformed = [
|
|
822
|
+
{
|
|
823
|
+
id: "claude.malformed.epoch_milliseconds",
|
|
824
|
+
connector: "claude",
|
|
825
|
+
reason: "resets_at in milliseconds instead of seconds",
|
|
826
|
+
expectedMeters: 0,
|
|
827
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: epochOffset(now, FIVE_HOURS) * 1_000 }, undefined)
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: "claude.malformed.epoch_negative",
|
|
831
|
+
connector: "claude",
|
|
832
|
+
reason: "negative resets_at",
|
|
833
|
+
expectedMeters: 0,
|
|
834
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: -1 }, undefined)
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
id: "claude.malformed.epoch_zero",
|
|
838
|
+
connector: "claude",
|
|
839
|
+
reason: "resets_at of zero, which is the epoch itself and long past",
|
|
840
|
+
expectedMeters: 0,
|
|
841
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: 0 }, undefined)
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
id: "claude.edge.epoch_string",
|
|
845
|
+
connector: "claude",
|
|
846
|
+
reason: "resets_at as the digits of an epoch in quotes, which is a JSON " +
|
|
847
|
+
"writer quoting its numbers rather than a different instant",
|
|
848
|
+
expectedMeters: 1,
|
|
849
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: String(epochOffset(now, FIVE_HOURS)) }, undefined)
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
id: "claude.edge.reset_iso_string",
|
|
853
|
+
connector: "claude",
|
|
854
|
+
reason: "resets_at as an ISO string, which the usage document and the " +
|
|
855
|
+
"model_scoped list both state, so it is a second encoding rather than drift",
|
|
856
|
+
expectedMeters: 1,
|
|
857
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: offset(now, FIVE_HOURS) }, undefined)
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
id: "claude.edge.utilization_field",
|
|
861
|
+
connector: "claude",
|
|
862
|
+
reason: "utilization rather than used_percentage, which is what the " +
|
|
863
|
+
"api/oauth/usage document states for the same reading",
|
|
864
|
+
expectedMeters: 1,
|
|
865
|
+
build: (now) => claudeWindows({ utilization: 42, resets_at: epochOffset(now, FIVE_HOURS) }, undefined)
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
id: "claude.malformed.epoch_expired",
|
|
869
|
+
connector: "claude",
|
|
870
|
+
reason: "a reset that already happened",
|
|
871
|
+
expectedMeters: 0,
|
|
872
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: epochOffset(now, -FIVE_HOURS) }, undefined)
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
id: "claude.malformed.epoch_year_2038",
|
|
876
|
+
connector: "claude",
|
|
877
|
+
reason: "a five hour window claiming to reset in 2038, which is not a wait " +
|
|
878
|
+
"but a corrupt field",
|
|
879
|
+
expectedMeters: 0,
|
|
880
|
+
build: () => ({
|
|
881
|
+
rate_limits: {
|
|
882
|
+
five_hour: { used_percentage: 42, resets_at: 2_147_483_648 }
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: "claude.malformed.epoch_just_past_horizon",
|
|
888
|
+
connector: "claude",
|
|
889
|
+
reason: "a five hour reset one second past the plausible horizon",
|
|
890
|
+
expectedMeters: 0,
|
|
891
|
+
build: (now) => claudeWindows({
|
|
892
|
+
used_percentage: 42,
|
|
893
|
+
resets_at: epochOffset(now, FIVE_HOURS * 2 + 3_600 + 1)
|
|
894
|
+
}, undefined)
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: "claude.edge.epoch_at_horizon",
|
|
898
|
+
connector: "claude",
|
|
899
|
+
reason: "a five hour reset exactly at the plausible horizon, which stands",
|
|
900
|
+
expectedMeters: 1,
|
|
901
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: epochOffset(now, FIVE_HOURS * 2 + 3_600) }, undefined)
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
id: "claude.edge.horizon_is_per_window",
|
|
905
|
+
connector: "claude",
|
|
906
|
+
reason: "a reset three days out, implausible for five hours and fine for " +
|
|
907
|
+
"seven days, so exactly one window survives",
|
|
908
|
+
expectedMeters: 1,
|
|
909
|
+
build: (now) => claudeWindows({ used_percentage: 42, resets_at: epochOffset(now, ONE_DAY * 3) }, { used_percentage: 64, resets_at: epochOffset(now, ONE_DAY * 3) })
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
id: "claude.edge.missing_five_hour",
|
|
913
|
+
connector: "claude",
|
|
914
|
+
reason: "only the seven day window present, which is one complete answer",
|
|
915
|
+
expectedMeters: 1,
|
|
916
|
+
build: (now) => claudeWindows(undefined, goodSevenDay(now))
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
id: "claude.edge.missing_seven_day",
|
|
920
|
+
connector: "claude",
|
|
921
|
+
reason: "only the five hour window present, which is one complete answer",
|
|
922
|
+
expectedMeters: 1,
|
|
923
|
+
build: (now) => claudeWindows(goodFiveHour(now), undefined)
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
id: "claude.edge.missing_both_windows",
|
|
927
|
+
connector: "claude",
|
|
928
|
+
reason: "rate_limits present and empty, which is unknown rather than zero",
|
|
929
|
+
expectedMeters: 0,
|
|
930
|
+
build: (now) => claudeWindows(undefined, undefined)
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
id: "claude.edge.rate_limits_absent",
|
|
934
|
+
connector: "claude",
|
|
935
|
+
reason: "no rate_limits at all, the ordinary free account payload",
|
|
936
|
+
expectedMeters: 0,
|
|
937
|
+
build: () => ({ session_id: "synthetic", version: "0.0.0-synthetic" })
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
id: "claude.edge.unknown_window_alongside",
|
|
941
|
+
connector: "claude",
|
|
942
|
+
reason: "an undocumented three_hour window, read under a code built from " +
|
|
943
|
+
"its own key rather than dropped, alongside the two documented windows",
|
|
944
|
+
expectedMeters: 3,
|
|
945
|
+
build: (now) => ({
|
|
946
|
+
rate_limits: {
|
|
947
|
+
five_hour: goodFiveHour(now),
|
|
948
|
+
seven_day: goodSevenDay(now),
|
|
949
|
+
three_hour: { used_percentage: 10, resets_at: epochOffset(now, 10_800) }
|
|
950
|
+
}
|
|
951
|
+
})
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
id: "claude.edge.unknown_window_only",
|
|
955
|
+
connector: "claude",
|
|
956
|
+
reason: "nothing but an undocumented window, which is still one real " +
|
|
957
|
+
"reading the provider stated",
|
|
958
|
+
expectedMeters: 1,
|
|
959
|
+
build: (now) => ({
|
|
960
|
+
rate_limits: {
|
|
961
|
+
three_hour: { used_percentage: 10, resets_at: epochOffset(now, 10_800) }
|
|
962
|
+
}
|
|
963
|
+
})
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
id: "claude.malformed.window_as_array",
|
|
967
|
+
connector: "claude",
|
|
968
|
+
reason: "a window that is an array",
|
|
969
|
+
expectedMeters: 0,
|
|
970
|
+
build: (now) => claudeWindows([42], undefined)
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
id: "claude.malformed.rate_limits_as_array",
|
|
974
|
+
connector: "claude",
|
|
975
|
+
reason: "rate_limits that is an array",
|
|
976
|
+
expectedMeters: 0,
|
|
977
|
+
build: () => ({ rate_limits: [] })
|
|
978
|
+
}
|
|
979
|
+
];
|
|
980
|
+
const genericIds = [
|
|
981
|
+
"claude",
|
|
982
|
+
"openrouter",
|
|
983
|
+
"codex",
|
|
984
|
+
"antigravity",
|
|
985
|
+
"opencode",
|
|
986
|
+
"grok",
|
|
987
|
+
"kimi",
|
|
988
|
+
"manual"
|
|
989
|
+
];
|
|
990
|
+
/**
|
|
991
|
+
* The shapes every parser must refuse, whatever provider it reads.
|
|
992
|
+
*
|
|
993
|
+
* Generated per connector rather than written out six times, so a new
|
|
994
|
+
* connector cannot be added without inheriting the whole hostile set.
|
|
995
|
+
*/
|
|
996
|
+
const genericMalformed = genericIds.flatMap((connector) => [
|
|
997
|
+
{
|
|
998
|
+
id: connector + ".malformed.undefined",
|
|
999
|
+
connector,
|
|
1000
|
+
reason: "no payload at all",
|
|
1001
|
+
expectedMeters: 0,
|
|
1002
|
+
build: () => undefined
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
id: connector + ".malformed.empty_object",
|
|
1006
|
+
connector,
|
|
1007
|
+
reason: "an empty object",
|
|
1008
|
+
expectedMeters: 0,
|
|
1009
|
+
build: () => ({})
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
id: connector + ".malformed.array_root",
|
|
1013
|
+
connector,
|
|
1014
|
+
reason: "an array where an object belongs",
|
|
1015
|
+
expectedMeters: 0,
|
|
1016
|
+
build: () => []
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
id: connector + ".malformed.string_root",
|
|
1020
|
+
connector,
|
|
1021
|
+
reason: "a string, which is what an HTML error page arrives as",
|
|
1022
|
+
expectedMeters: 0,
|
|
1023
|
+
build: () => "<!doctype html><title>502 Bad Gateway</title>"
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
id: connector + ".malformed.hostile_root",
|
|
1027
|
+
connector,
|
|
1028
|
+
reason: "prompt injection text and an enormous number at the root",
|
|
1029
|
+
expectedMeters: 0,
|
|
1030
|
+
build: () => ({ ...hostileFixture })
|
|
1031
|
+
}
|
|
1032
|
+
]);
|
|
1033
|
+
export const malformedFixtures = [
|
|
1034
|
+
...claudeMalformed,
|
|
1035
|
+
...genericMalformed
|
|
1036
|
+
];
|
|
1037
|
+
/** Every fixture class in one place, for a test that wants to sweep them all. */
|
|
1038
|
+
export const fixtureClasses = {
|
|
1039
|
+
documented: documentedFixtures,
|
|
1040
|
+
sanitizedLive: sanitizedLiveFixtures,
|
|
1041
|
+
malformed: malformedFixtures
|
|
1042
|
+
};
|
|
113
1043
|
//# sourceMappingURL=fixtures.js.map
|