@rikcodes/teamclaude 1.1.22-rik.3 → 1.1.22-rik.5
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/package.json +1 -1
- package/src/config.js +1 -0
- package/src/index.js +2 -0
- package/src/throughput.js +24 -10
- package/src/tui.js +123 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rikcodes/teamclaude",
|
|
3
|
-
"version": "1.1.22-rik.
|
|
3
|
+
"version": "1.1.22-rik.5",
|
|
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/config.js
CHANGED
|
@@ -111,6 +111,7 @@ export function createDefaultConfig() {
|
|
|
111
111
|
// the proxy does no throughput work at all (see OutputTracker).
|
|
112
112
|
throughputMeter: false,
|
|
113
113
|
projection: { enabled: true, windowMinutes: 90, wasteFloor: 0.1 },
|
|
114
|
+
accountSort: 'arranged',
|
|
114
115
|
eventLogging: 'hide',
|
|
115
116
|
defaultClientMode: 'mitm',
|
|
116
117
|
// Written out rather than left absent, so a fresh config states the one
|
package/src/index.js
CHANGED
|
@@ -535,6 +535,7 @@ async function serverCommand() {
|
|
|
535
535
|
// Read by the server per request and by the TUI per frame, so the reload
|
|
536
536
|
// is the whole application: the next request is timed, or is not.
|
|
537
537
|
config.throughputMeter = diskConfig.throughputMeter === true;
|
|
538
|
+
config.accountSort = diskConfig.accountSort === 'weekly-reset' ? 'weekly-reset' : 'arranged';
|
|
538
539
|
// Read by `run`/`env` from disk, but the TUI settings screen shows it live.
|
|
539
540
|
config.defaultClientMode = diskConfig.defaultClientMode === 'base-url' ? 'base-url' : 'mitm';
|
|
540
541
|
// The fleet switch for spending Codex reset credits. The redeemer reads it
|
|
@@ -636,6 +637,7 @@ async function serverCommand() {
|
|
|
636
637
|
if (config.eventLogging != null) diskConfig.eventLogging = config.eventLogging;
|
|
637
638
|
if (config.quotaBarPercent != null) diskConfig.quotaBarPercent = config.quotaBarPercent;
|
|
638
639
|
if (config.throughputMeter != null) diskConfig.throughputMeter = config.throughputMeter;
|
|
640
|
+
if (config.accountSort != null) diskConfig.accountSort = config.accountSort;
|
|
639
641
|
if (config.defaultClientMode != null) diskConfig.defaultClientMode = config.defaultClientMode;
|
|
640
642
|
if (config.autoRedeemResets != null) diskConfig.autoRedeemResets = config.autoRedeemResets;
|
|
641
643
|
if (config.blockedModels != null) diskConfig.blockedModels = config.blockedModels;
|
package/src/throughput.js
CHANGED
|
@@ -259,7 +259,7 @@ export function niceCeil(v) {
|
|
|
259
259
|
|
|
260
260
|
const mod = (/** @type {number} */ x, /** @type {number} */ n) => ((x % n) + n) % n;
|
|
261
261
|
|
|
262
|
-
/** @typedef {{ chars: number, firstAt: number, lastAt: number, model: string|null }} Stream */
|
|
262
|
+
/** @typedef {{ chars: number, firstAt: number, lastAt: number, model: string|null, counted: boolean }} Stream */
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
265
|
* The fleet's output rate: tokens generated across every request in the last
|
|
@@ -282,6 +282,11 @@ const mod = (/** @type {number} */ x, /** @type {number} */ n) => ((x % n) + n)
|
|
|
282
282
|
*
|
|
283
283
|
* Generating time runs from the stream's first output event to now, not to its
|
|
284
284
|
* latest one: hidden thinking sends nothing while it thinks.
|
|
285
|
+
*
|
|
286
|
+
* A stream can be followed without being counted (`counted` false): its own
|
|
287
|
+
* live estimate is kept, but it adds nothing to the fleet's reading, books
|
|
288
|
+
* nothing when it ends and teaches its model nothing. The TUI uses that for
|
|
289
|
+
* the second leg of one generation (see TUI._markLeg).
|
|
285
290
|
*/
|
|
286
291
|
export class ThroughputMeter {
|
|
287
292
|
/** @param {{ now?: () => number, windowSec?: number, ringSec?: number }} [opts] */
|
|
@@ -305,22 +310,24 @@ export class ThroughputMeter {
|
|
|
305
310
|
|
|
306
311
|
/**
|
|
307
312
|
* Output streamed for request `id`: `chars` of text at `at`, from `model`.
|
|
308
|
-
* The first call starts the stream's generating time.
|
|
309
|
-
*
|
|
313
|
+
* The first call starts the stream's generating time. `counted` false follows
|
|
314
|
+
* the stream for its own estimate only (see the class comment).
|
|
315
|
+
* @param {unknown} id @param {number} chars @param {number} at @param {string|null} [model] @param {boolean} [counted]
|
|
310
316
|
*/
|
|
311
|
-
progress(id, chars, at, model = null) {
|
|
317
|
+
progress(id, chars, at, model = null, counted = true) {
|
|
312
318
|
const t = time(at);
|
|
313
319
|
if (t === null) return;
|
|
314
320
|
const n = typeof chars === 'number' && Number.isFinite(chars) && chars > 0 ? chars : 0;
|
|
315
321
|
let s = this.streams.get(id);
|
|
316
322
|
if (!s) {
|
|
317
323
|
if (this.streams.size >= MAX_STREAMS) this.streams.delete(this.streams.keys().next().value);
|
|
318
|
-
s = { chars: 0, firstAt: t, lastAt: t, model: null };
|
|
324
|
+
s = { chars: 0, firstAt: t, lastAt: t, model: null, counted: true };
|
|
319
325
|
this.streams.set(id, s);
|
|
320
326
|
}
|
|
321
327
|
s.chars += n;
|
|
322
328
|
if (t > s.lastAt) s.lastAt = t;
|
|
323
329
|
if (model) s.model = model;
|
|
330
|
+
s.counted = counted !== false;
|
|
324
331
|
}
|
|
325
332
|
|
|
326
333
|
/**
|
|
@@ -333,12 +340,16 @@ export class ThroughputMeter {
|
|
|
333
340
|
* part of its estimate it can vouch for, its visible text, and not the part
|
|
334
341
|
* that was a guess from its model's pace.
|
|
335
342
|
*
|
|
343
|
+
* A request that is not counted (`counted` false, here or on its stream)
|
|
344
|
+
* books nothing and teaches nothing: it is only forgotten.
|
|
345
|
+
*
|
|
336
346
|
* @param {unknown} id
|
|
337
|
-
* @param {{ outputTokens?: number|null, firstAt?: number|null, lastAt?: number|null, dispatchedAt?: number|null, endedAt?: number|null, model?: string|null }} [r]
|
|
347
|
+
* @param {{ outputTokens?: number|null, firstAt?: number|null, lastAt?: number|null, dispatchedAt?: number|null, endedAt?: number|null, model?: string|null, counted?: boolean }} [r]
|
|
338
348
|
*/
|
|
339
|
-
finish(id, { outputTokens = null, firstAt = null, lastAt = null, dispatchedAt = null, endedAt = null, model = null } = {}) {
|
|
349
|
+
finish(id, { outputTokens = null, firstAt = null, lastAt = null, dispatchedAt = null, endedAt = null, model = null, counted = true } = {}) {
|
|
340
350
|
const s = this.streams.get(id);
|
|
341
351
|
this.streams.delete(id);
|
|
352
|
+
if (counted === false || s?.counted === false) return;
|
|
342
353
|
const end = time(endedAt);
|
|
343
354
|
if (end !== null) this._observe(end);
|
|
344
355
|
const n = tokens(outputTokens);
|
|
@@ -402,6 +413,7 @@ export class ThroughputMeter {
|
|
|
402
413
|
sum += this.ring[mod(sec - w, n)] * (1 - into);
|
|
403
414
|
const from = t - w * 1000;
|
|
404
415
|
for (const s of this.streams.values()) {
|
|
416
|
+
if (!s.counted) continue;
|
|
405
417
|
const { est, genMs } = this._estimate(s, t);
|
|
406
418
|
if (!est) continue;
|
|
407
419
|
sum += genMs > 0 ? est * (Math.min(genMs, t - from) / genMs) : est;
|
|
@@ -410,11 +422,13 @@ export class ThroughputMeter {
|
|
|
410
422
|
return Number.isFinite(r) && r > 0 ? r : 0;
|
|
411
423
|
}
|
|
412
424
|
|
|
413
|
-
/** Whether the reading is anything but a settled zero: a stream in
|
|
414
|
-
* tokens still in the window. The TUI keeps its fast tick for
|
|
425
|
+
/** Whether the reading is anything but a settled zero: a counted stream in
|
|
426
|
+
* flight, or tokens still in the window. The TUI keeps its fast tick for
|
|
427
|
+
* exactly as long.
|
|
415
428
|
* @param {number} [at] */
|
|
416
429
|
recent(at = this.now()) {
|
|
417
|
-
|
|
430
|
+
for (const s of this.streams.values()) if (s.counted) return true;
|
|
431
|
+
return this.rate(at) > 0;
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
/**
|
package/src/tui.js
CHANGED
|
@@ -22,7 +22,7 @@ import { sanitizeText, safeLine } from './safe-text.js';
|
|
|
22
22
|
// The setting rules live in one module; the CLI, the MCP tools and this screen
|
|
23
23
|
// all read them from there, so they cannot drift apart (#426).
|
|
24
24
|
import { MAX_PROBE_SECONDS, ROUTE_COLORS } from './config-ops.js';
|
|
25
|
-
import { isLocalUpstream } from './provider.js';
|
|
25
|
+
import { isLocalUpstream, providerForPath } from './provider.js';
|
|
26
26
|
import { ThroughputMeter, requestRate, formatRate } from './throughput.js';
|
|
27
27
|
import { renderSpeedo, speedoWidth, SPEEDO_MIN_H, SPEEDO_MAX_H } from './speedo.js';
|
|
28
28
|
|
|
@@ -286,6 +286,23 @@ const PANE_MIN = 62;
|
|
|
286
286
|
// the feature, and there is nothing to migrate.
|
|
287
287
|
const listRank = (/** @type {any} */ a) => (Number.isFinite(a?.displayOrder) ? a.displayOrder : Infinity);
|
|
288
288
|
|
|
289
|
+
// How the account list is ordered inside each provider group (`accountSort`).
|
|
290
|
+
// `arranged` is the operator's own order (listRank); `weekly-reset` puts the
|
|
291
|
+
// account whose weekly window ends soonest first, so quota that is about to
|
|
292
|
+
// expire unspent is at the top of the list.
|
|
293
|
+
const ACCOUNT_SORTS = ['arranged', 'weekly-reset'];
|
|
294
|
+
/** @type {Record<string, string>} */
|
|
295
|
+
const ACCOUNT_SORT_LABELS = { arranged: 'arranged', 'weekly-reset': 'weekly reset' };
|
|
296
|
+
|
|
297
|
+
// When the account's weekly window resets — the sort key behind `weekly-reset`.
|
|
298
|
+
// No reading (an API-key account, or one that has not reported) and a reset
|
|
299
|
+
// that has already passed both sort last: the second is a window that has just
|
|
300
|
+
// started over, so its next reset is the one furthest away.
|
|
301
|
+
const weeklyResetRank = (/** @type {any} */ a, /** @type {number} */ now) => {
|
|
302
|
+
const t = a?.quota?.unified7dReset;
|
|
303
|
+
return Number.isFinite(t) && t > now ? t : Infinity;
|
|
304
|
+
};
|
|
305
|
+
|
|
289
306
|
// How long a reorder waits after the last move before it is written. Longer
|
|
290
307
|
// than a terminal's key-repeat interval, so a held arrow is one write; short
|
|
291
308
|
// enough that the file is current by the time anyone looks at it.
|
|
@@ -1099,7 +1116,48 @@ export class TUI {
|
|
|
1099
1116
|
|
|
1100
1117
|
onRequestRouted(id, info) {
|
|
1101
1118
|
const r = this.active.get(id);
|
|
1102
|
-
if (r)
|
|
1119
|
+
if (!r) return;
|
|
1120
|
+
r.account = info.account == null ? info.account : safeLine(info.account, 64);
|
|
1121
|
+
if (this._throughputOn()) this._markLeg(r, info.account);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Whether this request is the second leg of a generation the fleet meter
|
|
1126
|
+
* already counts, decided once, when the request is first routed.
|
|
1127
|
+
*
|
|
1128
|
+
* A translating sidecar (the Codex one in docs/openai.md) is a local-upstream
|
|
1129
|
+
* account that calls back through this proxy: Claude Code's request goes to
|
|
1130
|
+
* the sidecar, and the sidecar's translation of it comes back in and is served
|
|
1131
|
+
* by a real account. Both legs stream the same generation and both report its
|
|
1132
|
+
* tokens, so counting both doubles it. The outer leg is the one counted: it is
|
|
1133
|
+
* what the client asked for, and the sidecar hands it the translated count.
|
|
1134
|
+
*
|
|
1135
|
+
* The inner leg is recognised by what the sidecar forwards and nothing else
|
|
1136
|
+
* does: the SAME session as a request the proxy is currently serving on a
|
|
1137
|
+
* local upstream, in a DIFFERENT dialect from it (the path says which), since
|
|
1138
|
+
* the leg is that request translated. The session alone is not enough, because
|
|
1139
|
+
* one session runs unrelated requests side by side; those are in the client's
|
|
1140
|
+
* own dialect. A request that is itself on a local upstream is an outer leg,
|
|
1141
|
+
* never an inner one. With no session on either leg nothing can be paired, and
|
|
1142
|
+
* a local backend that does not call back never sends an inner leg, so in both
|
|
1143
|
+
* cases everything is counted, as before.
|
|
1144
|
+
*
|
|
1145
|
+
* O(active) once per routing, never per delta. A nested request still keeps
|
|
1146
|
+
* its own estimate and its own finished rate; it is only left out of the sum.
|
|
1147
|
+
*
|
|
1148
|
+
* @param {Record<string, any>} r the active entry
|
|
1149
|
+
* @param {string|null|undefined} name the account it was routed to
|
|
1150
|
+
*/
|
|
1151
|
+
_markLeg(r, name) {
|
|
1152
|
+
const acct = name == null ? null : this.am.accounts.find((/** @type {any} */ a) => a.name === name);
|
|
1153
|
+
r.local = isLocalUpstream(acct);
|
|
1154
|
+
r.dialect ??= providerForPath(r.path || '');
|
|
1155
|
+
if (r.nested !== undefined) return;
|
|
1156
|
+
r.nested = false;
|
|
1157
|
+
if (r.local || !r.sessionId) return;
|
|
1158
|
+
for (const o of this.active.values()) {
|
|
1159
|
+
if (o !== r && o.local && o.sessionId === r.sessionId && o.dialect !== r.dialect) { r.nested = true; return; }
|
|
1160
|
+
}
|
|
1103
1161
|
}
|
|
1104
1162
|
|
|
1105
1163
|
/**
|
|
@@ -1113,7 +1171,7 @@ export class TUI {
|
|
|
1113
1171
|
*/
|
|
1114
1172
|
onRequestProgress(id, { chars, at }) {
|
|
1115
1173
|
const r = this.active.get(id);
|
|
1116
|
-
if (r) this.throughput.progress(id, chars, at, r.model || null);
|
|
1174
|
+
if (r) this.throughput.progress(id, chars, at, r.model || null, !r.nested);
|
|
1117
1175
|
}
|
|
1118
1176
|
|
|
1119
1177
|
onRequestEnd(id, info) {
|
|
@@ -1134,7 +1192,7 @@ export class TUI {
|
|
|
1134
1192
|
outputTokens: info.outputTokens, firstAt: info.firstTokenAt, lastAt: info.lastTokenAt,
|
|
1135
1193
|
dispatchedAt: info.dispatchedAt ?? r?.started ?? null, endedAt: now, model: info.model || r?.model || null,
|
|
1136
1194
|
};
|
|
1137
|
-
this.throughput.finish(id, timing);
|
|
1195
|
+
this.throughput.finish(id, { ...timing, counted: !r?.nested });
|
|
1138
1196
|
const tps = r ? requestRate({ ...timing, startedAt: timing.dispatchedAt }) : null;
|
|
1139
1197
|
const tag = this._sessionTag(sid);
|
|
1140
1198
|
// The line as it has always read. The rate is kept beside it rather than in
|
|
@@ -1427,6 +1485,21 @@ export class TUI {
|
|
|
1427
1485
|
});
|
|
1428
1486
|
}
|
|
1429
1487
|
|
|
1488
|
+
if (this.am.accounts.length > 1) {
|
|
1489
|
+
fields.push({
|
|
1490
|
+
id: 'accountSort',
|
|
1491
|
+
label: 'Sort accounts',
|
|
1492
|
+
hint: '←→ cycle',
|
|
1493
|
+
value: () => {
|
|
1494
|
+
const s = this._accountSort();
|
|
1495
|
+
return s === 'arranged' ? gray(ACCOUNT_SORT_LABELS[s]) : green(ACCOUNT_SORT_LABELS[s]);
|
|
1496
|
+
},
|
|
1497
|
+
left: () => this._cycleAccountSort(-1),
|
|
1498
|
+
right: () => this._cycleAccountSort(+1),
|
|
1499
|
+
enter: () => this._cycleAccountSort(+1),
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1430
1503
|
fields.push({
|
|
1431
1504
|
id: 'upstreamProxy',
|
|
1432
1505
|
label: 'Upstream proxy',
|
|
@@ -2052,6 +2125,24 @@ export class TUI {
|
|
|
2052
2125
|
if (this.running) this.render();
|
|
2053
2126
|
}
|
|
2054
2127
|
|
|
2128
|
+
/** The configured account sort; anything unknown reads as `arranged`. */
|
|
2129
|
+
_accountSort() {
|
|
2130
|
+
const s = this.config?.accountSort;
|
|
2131
|
+
return ACCOUNT_SORTS.includes(s) ? s : 'arranged';
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
async _cycleAccountSort(dir = 1) {
|
|
2135
|
+
// Read by _displayOrder on every frame, so the assignment is the whole
|
|
2136
|
+
// application and the save is only what survives a restart.
|
|
2137
|
+
const prev = this.config.accountSort;
|
|
2138
|
+
const cur = this._accountSort();
|
|
2139
|
+
const next = ACCOUNT_SORTS[(ACCOUNT_SORTS.indexOf(cur) + dir + ACCOUNT_SORTS.length) % ACCOUNT_SORTS.length];
|
|
2140
|
+
this.config.accountSort = next;
|
|
2141
|
+
if (!await this._saveSetting('account sort', () => { this.config.accountSort = prev; })) return;
|
|
2142
|
+
this._addLog(`Account sort: ${ACCOUNT_SORT_LABELS[next]}`);
|
|
2143
|
+
if (this.running) this.render();
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2055
2146
|
async _cycleEventLogging(dir = 1) {
|
|
2056
2147
|
// Claude Code telemetry display/handling: show → hide → block → show.
|
|
2057
2148
|
const order = ['show', 'hide', 'block'];
|
|
@@ -2942,17 +3033,32 @@ export class TUI {
|
|
|
2942
3033
|
* rows — see _keySelect, which walks this order but still stores an index.
|
|
2943
3034
|
* Which is also why the arrangement is a sort key rather than a permutation
|
|
2944
3035
|
* of `am.accounts`: see _doMoveAccount.
|
|
3036
|
+
*
|
|
3037
|
+
* With `accountSort: "weekly-reset"` the soonest weekly reset goes before
|
|
3038
|
+
* the arrangement, which then only breaks ties. Not on the reorder screen,
|
|
3039
|
+
* and not when `arranged` is asked for: the arrangement is what that screen
|
|
3040
|
+
* edits, so it must see that order.
|
|
3041
|
+
*
|
|
3042
|
+
* @param {{ arranged?: boolean }} [opts]
|
|
2945
3043
|
*/
|
|
2946
|
-
_displayOrder() {
|
|
3044
|
+
_displayOrder({ arranged = false } = {}) {
|
|
3045
|
+
const byReset = !arranged && this.config?.accountSort === 'weekly-reset'
|
|
3046
|
+
&& !(this.mode === 'select' && this.selAction === 'reorder');
|
|
3047
|
+
const now = Date.now();
|
|
2947
3048
|
return this.am.accounts
|
|
2948
3049
|
.map((/** @type {any} */ _, /** @type {number} */ i) => i)
|
|
2949
3050
|
.filter(i => !isLocalUpstream(this.am.accounts[i]))
|
|
2950
3051
|
.sort((/** @type {number} */ x, /** @type {number} */ y) => {
|
|
2951
3052
|
const px = PROVIDER_ORDER.indexOf(providerOf(this.am.accounts[x]));
|
|
2952
3053
|
const py = PROVIDER_ORDER.indexOf(providerOf(this.am.accounts[y]));
|
|
2953
|
-
// Provider, then the arrangement: the provider is what a
|
|
2954
|
-
// number the operator set
|
|
3054
|
+
// Provider, then the sort, then the arrangement: the provider is what a
|
|
3055
|
+
// row IS, so no sort and no number the operator set crosses it.
|
|
2955
3056
|
if (px !== py) return px - py;
|
|
3057
|
+
if (byReset) {
|
|
3058
|
+
const tx = weeklyResetRank(this.am.accounts[x], now);
|
|
3059
|
+
const ty = weeklyResetRank(this.am.accounts[y], now);
|
|
3060
|
+
if (tx !== ty) return tx < ty ? -1 : 1; // Infinity - Infinity is NaN, so compare
|
|
3061
|
+
}
|
|
2956
3062
|
const rx = listRank(this.am.accounts[x]);
|
|
2957
3063
|
const ry = listRank(this.am.accounts[y]);
|
|
2958
3064
|
// Infinity !== Infinity is false, so two unplaced accounts fall through
|
|
@@ -3240,9 +3346,13 @@ export class TUI {
|
|
|
3240
3346
|
* Every account except the locally-served ones. _displayOrder already leaves
|
|
3241
3347
|
* those out (they draw as conduit lines, not rows), so they hold no position
|
|
3242
3348
|
* and their array slots are simply stepped over.
|
|
3349
|
+
*
|
|
3350
|
+
* Always in the arranged order, whatever `accountSort` says: a move
|
|
3351
|
+
* renumbers every account from this list, so a sorted list here would
|
|
3352
|
+
* write the sort into `displayOrder`.
|
|
3243
3353
|
*/
|
|
3244
3354
|
_arrangeable() {
|
|
3245
|
-
return this._displayOrder();
|
|
3355
|
+
return this._displayOrder({ arranged: true });
|
|
3246
3356
|
}
|
|
3247
3357
|
|
|
3248
3358
|
/** The rows that carry ►: the cursor, or in a mixed pool each provider's current
|
|
@@ -3583,6 +3693,11 @@ export class TUI {
|
|
|
3583
3693
|
lines.push(row(byId('addAccount')));
|
|
3584
3694
|
if (byId('removeAccount')) lines.push(row(byId('removeAccount')));
|
|
3585
3695
|
if (byId('orderAccounts')) lines.push(row(byId('orderAccounts')));
|
|
3696
|
+
if (byId('accountSort')) {
|
|
3697
|
+
lines.push(row(byId('accountSort')));
|
|
3698
|
+
lines.push(dim(' Weekly reset lists the account whose week ends soonest first;'));
|
|
3699
|
+
lines.push(dim(' the arranged order breaks ties.'));
|
|
3700
|
+
}
|
|
3586
3701
|
lines.push('');
|
|
3587
3702
|
// ── Network
|
|
3588
3703
|
// Drawn before the sx.org block, which returns early when sx is unavailable:
|