@velum-labs/routekit-accounts 0.16.2 → 0.16.4

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.
@@ -49,6 +49,63 @@ function parsedRateLimitWindow(value, observedAt, source, migration) {
49
49
  source: windowSource
50
50
  };
51
51
  }
52
+ function parsedResetCredit(value) {
53
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0) {
54
+ return undefined;
55
+ }
56
+ return {
57
+ id: value.id,
58
+ ...(typeof value.resetType === "string" ? { resetType: value.resetType } : {}),
59
+ ...(typeof value.status === "string" ? { status: value.status } : {}),
60
+ ...(typeof value.grantedAt === "number" && Number.isFinite(value.grantedAt)
61
+ ? { grantedAt: value.grantedAt }
62
+ : {}),
63
+ ...(typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt)
64
+ ? { expiresAt: value.expiresAt }
65
+ : {}),
66
+ ...(typeof value.title === "string" ? { title: value.title } : {}),
67
+ ...(typeof value.description === "string" ? { description: value.description } : {})
68
+ };
69
+ }
70
+ function parsedResetCreditSnapshot(value, fallbackObservedAt, migration) {
71
+ if (!isRecord(value) ||
72
+ typeof value.availableCount !== "number" ||
73
+ !Number.isFinite(value.availableCount) ||
74
+ value.availableCount < 0) {
75
+ return undefined;
76
+ }
77
+ let observedAt;
78
+ if (typeof value.observedAt === "number" && Number.isFinite(value.observedAt)) {
79
+ observedAt = value.observedAt;
80
+ }
81
+ else if (fallbackObservedAt !== undefined) {
82
+ if (migration !== undefined)
83
+ migration.required = true;
84
+ observedAt = fallbackObservedAt;
85
+ }
86
+ else {
87
+ return undefined;
88
+ }
89
+ if (value.credits !== undefined && !Array.isArray(value.credits))
90
+ return undefined;
91
+ const credits = Array.isArray(value.credits)
92
+ ? value.credits.flatMap((entry) => {
93
+ const credit = parsedResetCredit(entry);
94
+ return credit === undefined ? [] : [credit];
95
+ })
96
+ : undefined;
97
+ if (Array.isArray(value.credits) &&
98
+ credits !== undefined &&
99
+ credits.length !== value.credits.length &&
100
+ migration !== undefined) {
101
+ migration.required = true;
102
+ }
103
+ return {
104
+ observedAt,
105
+ availableCount: Math.floor(value.availableCount),
106
+ ...(credits !== undefined ? { credits } : {})
107
+ };
108
+ }
52
109
  function parsedAccountLimits(value, mode, migration) {
53
110
  if (!isRecord(value) ||
54
111
  !isRecord(value.windows) ||
@@ -87,13 +144,15 @@ function parsedAccountLimits(value, mode, migration) {
87
144
  writable: true
88
145
  });
89
146
  }
147
+ const resetCredits = parsedResetCreditSnapshot(value.resetCredits, value.observedAt, migration);
90
148
  return {
91
149
  windows,
92
150
  observedAt: value.observedAt,
93
151
  source: value.source,
94
152
  completeness,
95
153
  ...(typeof value.planType === "string" ? { planType: value.planType } : {}),
96
- ...(isRecord(value.credits) ? { credits: value.credits } : {})
154
+ ...(isRecord(value.credits) ? { credits: value.credits } : {}),
155
+ ...(resetCredits !== undefined ? { resetCredits } : {})
97
156
  };
98
157
  }
99
158
  function parsedMemberState(value, mode, migration) {
@@ -183,7 +242,8 @@ function mergeLimits(previous, next, mode) {
183
242
  });
184
243
  }
185
244
  }
186
- return next.completeness === "snapshot"
245
+ const resetCredits = next.resetCredits ?? previous?.resetCredits;
246
+ const merged = next.completeness === "snapshot"
187
247
  ? { ...next, windows }
188
248
  : {
189
249
  ...previous,
@@ -192,6 +252,10 @@ function mergeLimits(previous, next, mode) {
192
252
  observedAt: next.observedAt,
193
253
  source: next.source
194
254
  };
255
+ return {
256
+ ...merged,
257
+ ...(resetCredits !== undefined ? { resetCredits } : {})
258
+ };
195
259
  }
196
260
  /**
197
261
  * Trackers for one state file share mutable state process-wide. A daemon
@@ -782,7 +846,18 @@ export class SubscriptionAccountSet {
782
846
  if (this.#provider.fetchResetCredits === undefined) {
783
847
  throw new Error(`${this.mode} does not support redeemable rate-limit resets`);
784
848
  }
785
- return await this.#provider.fetchResetCredits(member.credential, signal);
849
+ const resetCredits = await this.#provider.fetchResetCredits(member.credential, signal);
850
+ const previous = this.#tracker.limits(member.id);
851
+ this.#tracker.update(member.id, {
852
+ ...(previous ?? {
853
+ windows: {},
854
+ source: "usage",
855
+ completeness: "partial"
856
+ }),
857
+ resetCredits,
858
+ observedAt: previous?.observedAt ?? resetCredits.observedAt
859
+ });
860
+ return resetCredits;
786
861
  }
787
862
  async #attachResetCredits(member, limits, signal) {
788
863
  if (this.#provider.fetchResetCredits === undefined)
@@ -792,7 +867,10 @@ export class SubscriptionAccountSet {
792
867
  return { ...limits, resetCredits };
793
868
  }
794
869
  catch {
795
- return limits;
870
+ // Keep the last authoritative snapshot when the dedicated listing fails.
871
+ // Do not let a weaker embedded usage payload erase known credit rows.
872
+ const previous = this.#tracker.limits(member.id)?.resetCredits;
873
+ return previous === undefined ? limits : { ...limits, resetCredits: previous };
796
874
  }
797
875
  }
798
876
  async #acquire(model, excluded, signal) {
package/dist/provider.js CHANGED
@@ -285,7 +285,7 @@ function codexUsageLimits(payload, source = "usage", completeness = "snapshot")
285
285
  : {}),
286
286
  ...(typeof rawCredits.balance === "string" ? { balance: rawCredits.balance } : {})
287
287
  };
288
- const resetCredits = codexResetCreditsFromUsage(payload);
288
+ const resetCredits = codexResetCreditsFromUsage(payload, observedAt);
289
289
  return {
290
290
  windows,
291
291
  ...(typeof payload.plan_type === "string" ? { planType: payload.plan_type } : {}),
@@ -296,7 +296,7 @@ function codexUsageLimits(payload, source = "usage", completeness = "snapshot")
296
296
  completeness
297
297
  };
298
298
  }
299
- function codexResetCreditsFromUsage(payload) {
299
+ function codexResetCreditsFromUsage(payload, observedAt) {
300
300
  const raw = isRecord(payload.rate_limit_reset_credits)
301
301
  ? payload.rate_limit_reset_credits
302
302
  : isRecord(payload.rateLimitResetCredits)
@@ -315,6 +315,7 @@ function codexResetCreditsFromUsage(payload) {
315
315
  .filter((entry) => entry !== undefined)
316
316
  : undefined;
317
317
  return {
318
+ observedAt,
318
319
  availableCount: Math.max(0, Math.floor(count)),
319
320
  ...(credits !== undefined && credits.length > 0 ? { credits } : {})
320
321
  };
@@ -353,6 +354,7 @@ function parseResetCredit(value) {
353
354
  };
354
355
  }
355
356
  function parseResetCreditSnapshot(payload) {
357
+ const observedAt = Date.now() / 1000;
356
358
  if (!isRecord(payload))
357
359
  throw new Error("Codex reset-credits endpoint returned an invalid payload");
358
360
  const rows = Array.isArray(payload.credits)
@@ -373,6 +375,7 @@ function parseResetCreditSnapshot(payload) {
373
375
  numeric(payload.availableCount) ??
374
376
  available.length;
375
377
  return {
378
+ observedAt,
376
379
  availableCount: Math.max(0, Math.floor(count)),
377
380
  ...(credits.length > 0 ? { credits } : {})
378
381
  };
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
@@ -46,7 +46,9 @@ function fakeProvider(state, modelsByToken = {}, mode = "codex") {
46
46
  };
47
47
  },
48
48
  async fetchResetCredits() {
49
- return state.resetCredits ?? { availableCount: 0, credits: [] };
49
+ if (state.failResetCredits === true)
50
+ throw new Error("reset credits unavailable");
51
+ return state.resetCredits ?? { observedAt: Date.now() / 1000, availableCount: 0, credits: [] };
50
52
  },
51
53
  async consumeResetCredit(_credential, input) {
52
54
  state.consumeCalls = (state.consumeCalls ?? 0) + 1;
@@ -60,12 +62,12 @@ function fakeProvider(state, modelsByToken = {}, mode = "codex") {
60
62
  source: "usage"
61
63
  }
62
64
  },
63
- resetCredits: { availableCount: 0, credits: [] },
65
+ resetCredits: { observedAt: Date.now() / 1000, availableCount: 0, credits: [] },
64
66
  observedAt: Date.now() / 1000,
65
67
  source: "usage",
66
68
  completeness: "snapshot"
67
69
  };
68
- state.resetCredits = { availableCount: 0, credits: [] };
70
+ state.resetCredits = { observedAt: Date.now() / 1000, availableCount: 0, credits: [] };
69
71
  }
70
72
  return {
71
73
  ok: code === "reset",
@@ -1449,6 +1451,7 @@ test("redeeming a banked reset refreshes windows and clears cooling", async () =
1449
1451
  }
1450
1452
  },
1451
1453
  resetCredits: {
1454
+ observedAt,
1452
1455
  availableCount: 1,
1453
1456
  credits: [
1454
1457
  {
@@ -1463,6 +1466,7 @@ test("redeeming a banked reset refreshes windows and clears cooling", async () =
1463
1466
  completeness: "snapshot"
1464
1467
  },
1465
1468
  resetCredits: {
1469
+ observedAt,
1466
1470
  availableCount: 1,
1467
1471
  credits: [
1468
1472
  {
@@ -1501,3 +1505,114 @@ test("redeeming a banked reset refreshes windows and clears cooling", async () =
1501
1505
  rmSync(directory, { recursive: true, force: true });
1502
1506
  }
1503
1507
  });
1508
+ test("dedicated reset refresh preserves stale state on failure and clears on empty", async () => {
1509
+ const directory = mkdtempSync(join(tmpdir(), "routekit-pool-reset-refresh-"));
1510
+ writeMember(directory, "work", { accessToken: "token-work" });
1511
+ const observedAt = Date.now() / 1000;
1512
+ const detailed = {
1513
+ observedAt,
1514
+ availableCount: 1,
1515
+ credits: [{ id: "RateLimitResetCredit_stale", status: "available", title: "Stale reset" }]
1516
+ };
1517
+ const state = {
1518
+ refreshes: 0,
1519
+ resetCredits: detailed,
1520
+ usageLimits: {
1521
+ windows: {
1522
+ primary: {
1523
+ utilization: 0.5,
1524
+ observedAt,
1525
+ source: "usage"
1526
+ }
1527
+ },
1528
+ // Weaker embedded count-only payload must not win when dedicated listing fails.
1529
+ resetCredits: { observedAt, availableCount: 1 },
1530
+ observedAt,
1531
+ source: "usage",
1532
+ completeness: "snapshot"
1533
+ }
1534
+ };
1535
+ const pool = await SubscriptionAccountSet.open(fakeProvider(state), {
1536
+ mode: "codex",
1537
+ source: { kind: "directory", path: directory }
1538
+ });
1539
+ try {
1540
+ await pool.listResetCredits("work");
1541
+ assert.deepEqual(pool.snapshot().members[0]?.limits?.resetCredits, detailed);
1542
+ state.failResetCredits = true;
1543
+ await pool.probe();
1544
+ assert.deepEqual(pool.snapshot().members[0]?.limits?.resetCredits, detailed);
1545
+ state.failResetCredits = false;
1546
+ state.resetCredits = { observedAt: observedAt + 1, availableCount: 0, credits: [] };
1547
+ await pool.listResetCredits("work");
1548
+ assert.deepEqual(pool.snapshot().members[0]?.limits?.resetCredits, state.resetCredits);
1549
+ }
1550
+ finally {
1551
+ await pool.close();
1552
+ rmSync(directory, { recursive: true, force: true });
1553
+ }
1554
+ });
1555
+ test("tracker restores fully validated reset credit details", () => {
1556
+ const directory = mkdtempSync(join(tmpdir(), "routekit-tracker-reset-persist-"));
1557
+ const path = join(directory, ".state.json");
1558
+ const observedAt = 1775000000;
1559
+ writeFileSync(path, JSON.stringify({
1560
+ members: [{
1561
+ id: "work",
1562
+ limits: {
1563
+ windows: {}, observedAt, source: "usage", completeness: "snapshot",
1564
+ resetCredits: {
1565
+ observedAt: observedAt + 1,
1566
+ availableCount: 1,
1567
+ credits: [{
1568
+ id: "RateLimitResetCredit_saved", resetType: "codex_rate_limits",
1569
+ status: "available", grantedAt: observedAt - 10, expiresAt: observedAt + 100,
1570
+ title: "Saved reset", description: "Persisted details"
1571
+ }]
1572
+ }
1573
+ }
1574
+ }]
1575
+ }));
1576
+ try {
1577
+ assert.deepEqual(new RateLimitTracker(path, "codex").limits("work")?.resetCredits, {
1578
+ observedAt: observedAt + 1, availableCount: 1, credits: [{
1579
+ id: "RateLimitResetCredit_saved", resetType: "codex_rate_limits",
1580
+ status: "available", grantedAt: observedAt - 10, expiresAt: observedAt + 100,
1581
+ title: "Saved reset", description: "Persisted details"
1582
+ }]
1583
+ });
1584
+ }
1585
+ finally {
1586
+ rmSync(directory, { recursive: true, force: true });
1587
+ }
1588
+ });
1589
+ test("tracker migrates legacy reset credits missing observedAt", () => {
1590
+ const directory = mkdtempSync(join(tmpdir(), "routekit-tracker-reset-migrate-"));
1591
+ const path = join(directory, ".state.json");
1592
+ const observedAt = 1775000000;
1593
+ writeFileSync(path, JSON.stringify({
1594
+ members: [{
1595
+ id: "work",
1596
+ limits: {
1597
+ windows: {}, observedAt, source: "usage", completeness: "snapshot",
1598
+ resetCredits: {
1599
+ availableCount: 1,
1600
+ credits: [{ id: "RateLimitResetCredit_legacy", status: "available" }]
1601
+ }
1602
+ }
1603
+ }]
1604
+ }));
1605
+ try {
1606
+ const tracker = new RateLimitTracker(path, "codex");
1607
+ assert.deepEqual(tracker.limits("work")?.resetCredits, {
1608
+ observedAt,
1609
+ availableCount: 1,
1610
+ credits: [{ id: "RateLimitResetCredit_legacy", status: "available" }]
1611
+ });
1612
+ const persisted = JSON.parse(readFileSync(path, "utf8"));
1613
+ assert.equal(persisted.members[0]?.limits?.resetCredits?.observedAt, observedAt);
1614
+ }
1615
+ finally {
1616
+ rmSync(directory, { recursive: true, force: true });
1617
+ }
1618
+ });
@@ -109,6 +109,7 @@ test("Codex adapter parses banked reset credits from usage payloads", async () =
109
109
  sourcePath: "/tmp/codex.json"
110
110
  });
111
111
  assert.equal(limits.resetCredits?.availableCount, 2);
112
+ assert.equal(typeof limits.resetCredits?.observedAt, "number");
112
113
  assert.equal(limits.resetCredits?.credits?.[0]?.id, "RateLimitResetCredit_a");
113
114
  assert.equal(limits.windows.primary?.utilization, 0.9);
114
115
  }
@@ -161,6 +162,7 @@ test("Codex adapter lists and consumes banked rate-limit resets", async () => {
161
162
  sourcePath: "/tmp/codex.json"
162
163
  });
163
164
  assert.equal(listed.availableCount, 1);
165
+ assert.equal(typeof listed.observedAt, "number");
164
166
  assert.equal(listed.credits?.[0]?.id, "RateLimitResetCredit_b");
165
167
  const consumed = await provider.consumeResetCredit({
166
168
  mode: "codex",
@@ -114,6 +114,16 @@ test("the usage wire schema round-trips an account-set snapshot", () => {
114
114
  source: "headers"
115
115
  }
116
116
  },
117
+ resetCredits: {
118
+ observedAt: 1_776_000_100,
119
+ availableCount: 1,
120
+ credits: [{
121
+ id: "RateLimitResetCredit_wire",
122
+ status: "available",
123
+ title: "Wire reset",
124
+ expiresAt: 1_777_000_000
125
+ }]
126
+ },
117
127
  observedAt: 1_776_000_000,
118
128
  source: "headers",
119
129
  completeness: "partial"
@@ -128,6 +138,7 @@ test("the usage wire schema round-trips an account-set snapshot", () => {
128
138
  assert.equal(parsed.accountSets[0]?.members[0]?.lastSelected, true);
129
139
  assert.equal(parsed.accountSets[0]?.members[0]?.lastSelectedAt, 1_776_000_000_000);
130
140
  assert.equal(parsed.accountSets[0]?.members[0]?.active, true);
141
+ assert.equal(parsed.accountSets[0]?.members[0]?.limits?.resetCredits?.credits?.[0]?.id, "RateLimitResetCredit_wire");
131
142
  assert.deepEqual(parsed.accountSets[0]?.members[0]?.readinessReasons, snapshot.members[0]?.readinessReasons);
132
143
  const older = structuredClone(snapshot);
133
144
  delete older.members[0]?.readinessReasons;
package/dist/types.d.ts CHANGED
@@ -39,6 +39,7 @@ export type ResetCredit = {
39
39
  description?: string;
40
40
  };
41
41
  export type ResetCreditSnapshot = {
42
+ observedAt: number;
42
43
  availableCount: number;
43
44
  credits?: ResetCredit[];
44
45
  };
package/dist/wire.d.ts CHANGED
@@ -88,6 +88,7 @@ export declare const subscriptionUsageResponseSchema: z.ZodObject<{
88
88
  balance: z.ZodOptional<z.ZodString>;
89
89
  }, z.core.$strip>>;
90
90
  resetCredits: z.ZodOptional<z.ZodObject<{
91
+ observedAt: z.ZodNumber;
91
92
  availableCount: z.ZodNumber;
92
93
  credits: z.ZodOptional<z.ZodArray<z.ZodObject<{
93
94
  id: z.ZodString;
package/dist/wire.js CHANGED
@@ -30,6 +30,7 @@ const resetCreditSchema = z.object({
30
30
  description: z.string().optional()
31
31
  });
32
32
  const resetCreditSnapshotSchema = z.object({
33
+ observedAt: z.number(),
33
34
  availableCount: z.number(),
34
35
  credits: z.array(resetCreditSchema).optional()
35
36
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-accounts",
3
3
  "private": false,
4
- "version": "0.16.2",
4
+ "version": "0.16.4",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -28,10 +28,10 @@
28
28
  "dependencies": {
29
29
  "yaml": "2.9.0",
30
30
  "zod": "4.4.3",
31
- "@velum-labs/routekit-contracts": "0.16.2",
32
- "@velum-labs/routekit-gateway": "0.16.2",
33
- "@velum-labs/routekit-registry": "0.16.2",
34
- "@velum-labs/routekit-runtime": "0.16.2"
31
+ "@velum-labs/routekit-contracts": "0.16.4",
32
+ "@velum-labs/routekit-gateway": "0.16.4",
33
+ "@velum-labs/routekit-registry": "0.16.4",
34
+ "@velum-labs/routekit-runtime": "0.16.4"
35
35
  },
36
36
  "keywords": [
37
37
  "routekit",