@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 +1 -0
- package/package.json +1 -1
- package/src/account-manager.js +78 -0
- package/src/codex-reset-credits.js +780 -0
- package/src/codex-usage.js +30 -0
- package/src/config.js +4 -0
- package/src/dashboard.js +8 -0
- package/src/index.js +26 -1
- package/src/server.js +272 -19
- package/src/status-renderer.js +23 -0
- package/src/sync-accounts.js +12 -0
- package/src/tui.js +73 -2
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.
|
|
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",
|
package/src/account-manager.js
CHANGED
|
@@ -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
|
}
|