@rikcodes/teamclaude 1.1.20-rik.15 → 1.1.20-rik.16

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/README.md CHANGED
@@ -242,6 +242,7 @@ This feature is on by default. Each account row shows which window binds first:
242
242
  | [OpenAI models](docs/openai.md) | Codex sidecar setup, custom model registration, GPT subagents, limitations |
243
243
  | [Configuration](docs/configuration.md) | Config format, every field, environment variables, network tuning |
244
244
  | [Proxy modes](docs/proxy-modes.md) | MITM forward proxy, sx.org residential egress |
245
+ | [Remote host](docs/remote.md) | Running the fleet on an always-on box: reaching it, moving the accounts, service supervision, pointing clients at it |
245
246
  | [Compliance](docs/compliance.md) | Terms of service notes |
246
247
 
247
248
  ## Releasing this fork
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.20-rik.15",
3
+ "version": "1.1.20-rik.16",
4
4
  "description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -100,6 +100,23 @@ const PERSISTED_QUOTA_FIELDS = [
100
100
  'unifiedStatus', 'unifiedStatusSeenAt',
101
101
  'tokensLimit', 'tokensRemaining', 'requestsLimit', 'requestsRemaining', 'resetsAt',
102
102
  'scopedWeekly',
103
+ // Codex free rate-limit reset credits, `{ available, applicable, seenAt }`.
104
+ // Worth persisting although it is not a quota: the usage probe is off by
105
+ // default, so without this a restart forgets that an account holds a credit
106
+ // until something next reads /wham/usage — and the row that says so is the
107
+ // only place an operator sees one at all.
108
+ 'resetCredits',
109
+ // The Codex subscription tier, a string from the response headers or the
110
+ // usage payload. Both sources are traffic, so without this a restarted server
111
+ // cannot name an account's plan until it next serves a request — and a plan
112
+ // an account is on does not change over a restart.
113
+ 'planType',
114
+ // Codex's model-scoped weekly buckets, `{ [slug]: { name, utilization,
115
+ // resetAt, seenAt } }`. Learned the same way and just as lossy on restart,
116
+ // and the per-entry `seenAt` is load-bearing beyond the reading itself: it is
117
+ // what orders the eviction that keeps the table under its cap, so dropping
118
+ // the table also drops the history that decides what makes room next.
119
+ 'codexModelBuckets',
103
120
  ];
104
121
 
105
122
  // The family (Fable/Sonnet) weekly buckets and the field holding when each was
@@ -127,6 +144,22 @@ function parseResetAt(value) {
127
144
  return Number.isNaN(parsed) ? null : parsed;
128
145
  }
129
146
 
147
+ /**
148
+ * The quota fields a Codex account only ever LEARNS — from a `/wham/usage`
149
+ * payload or from the state restored off disk — so `emptyQuota` below does not
150
+ * seed them. Absent is meaningful for each: it says nothing has been read yet,
151
+ * which a seeded null would spell the same way as "read, and empty".
152
+ *
153
+ * Declared here so the few places that write them can say so (`@type` on the
154
+ * local that holds the quota), rather than each one reading as a property that
155
+ * does not exist.
156
+ *
157
+ * @typedef {object} CodexLearnedQuota
158
+ * @property {string} [planType] the Codex subscription tier
159
+ * @property {{available: number, applicable: number|null, seenAt: number}} [resetCredits] free rate-limit reset credits held, and when that was last seen
160
+ * @property {Record<string, {name: string, utilization: number, resetAt: number|null, seenAt: number}>} [codexModelBuckets] model-scoped weekly buckets, keyed by slug
161
+ */
162
+
130
163
  function emptyQuota() {
131
164
  return {
132
165
  // Standard API rate limits (API key accounts)
@@ -223,6 +256,17 @@ function makeAccount(acct, index) {
223
256
  displayOrder: Number.isFinite(acct.displayOrder) ? acct.displayOrder : null,
224
257
  disabled: acct.disabled || false,
225
258
  maxUsage: acct.maxUsage ?? null,
259
+ // Whether this account is EXEMPT from spending one of its free Codex
260
+ // rate-limit reset credits (see codex-reset-credits.js). Negative-only, and
261
+ // the polarity is the opposite of what the name suggests: the switch that
262
+ // arms anything is the fleet-wide `autoRedeemResets`, because the policy it
263
+ // arms ("only when the whole Codex pool is dry") is a statement about the
264
+ // fleet. All this key can say is "never this one", so `true` and an absent
265
+ // key mean exactly the same thing here. Meaningless on an Anthropic
266
+ // account, which has no such credits — the redeemer checks the provider
267
+ // rather than making the field's default depend on it, so a config moved
268
+ // between providers keeps saying the same thing.
269
+ autoRedeemReset: acct.autoRedeemReset !== false,
226
270
  upstream: acct.upstream || null,
227
271
  modelMap: acct.modelMap || null,
228
272
  // Fields to drop from request bodies for this account (third-party upstreams
@@ -3602,6 +3646,19 @@ export class AccountManager {
3602
3646
  && this.accounts.some(a => providerOf(a) === 'codex');
3603
3647
  }
3604
3648
 
3649
+ /**
3650
+ * Whether this account merely relays to our own Codex pool, for a caller
3651
+ * outside this class that must not treat a relayed reading as the account's
3652
+ * own. The private form above is the definition; this is the same question
3653
+ * asked from the request path, where the distinction decides whether a quota
3654
+ * rejection describes the account in hand or one behind it.
3655
+ *
3656
+ * @param {any} account
3657
+ */
3658
+ isCodexConduit(account) {
3659
+ return this._isCodexConduit(account);
3660
+ }
3661
+
3605
3662
  /**
3606
3663
  * Update cumulative token usage from response body data.
3607
3664
  */
@@ -3796,6 +3853,9 @@ export class AccountManager {
3796
3853
  applyCodexUsageData(accountIndex, usage) {
3797
3854
  const account = this.accounts[accountIndex];
3798
3855
  if (!account || !usage || usage.error) return;
3856
+ // The three Codex-learned fields below are written here for the first time,
3857
+ // so the empty-quota shape does not carry them. See CodexLearnedQuota.
3858
+ /** @type {typeof account.quota & CodexLearnedQuota} */
3799
3859
  const q = account.quota;
3800
3860
  if (usage.fiveHour) {
3801
3861
  q.unified5h = usage.fiveHour.utilization;
@@ -3806,6 +3866,10 @@ export class AccountManager {
3806
3866
  q.unified7dReset = usage.sevenDay.resetAt ?? null;
3807
3867
  }
3808
3868
  if (usage.planType) q.planType = safeLine(usage.planType, 64);
3869
+ // Stamped, because nothing else refreshes it: a payload that mentions no
3870
+ // credits leaves the last reading alone rather than blanking it, so the
3871
+ // age is the only thing that says how much the number is worth.
3872
+ if (usage.resetCredits) q.resetCredits = { ...usage.resetCredits, seenAt: Date.now() };
3809
3873
  if (Array.isArray(usage.modelBuckets)) {
3810
3874
  q.codexModelBuckets = Object.fromEntries(usage.modelBuckets.slice(0, MAX_CODEX_MODEL_BUCKETS)
3811
3875
  .filter(bucket => bucket?.slug)
@@ -4132,6 +4196,20 @@ export class AccountManager {
4132
4196
  for (const f of PERSISTED_QUOTA_FIELDS) {
4133
4197
  if (match.quota[f] != null) account.quota[f] = match.quota[f];
4134
4198
  }
4199
+ // Both writers of the Codex bucket table cap it, because its keys are
4200
+ // upstream header names; restoring is the one way in that never passed a
4201
+ // cap. A file we wrote cannot be over the ceiling, but an edited or
4202
+ // half-written one can, and it would then stand until some slug this
4203
+ // server has never seen turns up to evict the surplus. Newest readings
4204
+ // kept, which is the same order the eviction there works in.
4205
+ /** @type {typeof account.quota & CodexLearnedQuota} */
4206
+ const quota = account.quota;
4207
+ const restoredBuckets = quota.codexModelBuckets;
4208
+ if (restoredBuckets && Object.keys(restoredBuckets).length > MAX_CODEX_MODEL_BUCKETS) {
4209
+ quota.codexModelBuckets = Object.fromEntries(Object.entries(restoredBuckets)
4210
+ .sort((a, b) => (b[1]?.seenAt || 0) - (a[1]?.seenAt || 0))
4211
+ .slice(0, MAX_CODEX_MODEL_BUCKETS));
4212
+ }
4135
4213
  for (const field of ['organizationType', 'rateLimitTier', 'seatTier', 'hasClaudeMax', 'hasClaudePro']) {
4136
4214
  if (match.profile?.[field] != null) account[field] = match.profile[field];
4137
4215
  }