@rikcodes/teamclaude 1.1.22-rik.3 → 1.1.22-rik.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rikcodes/teamclaude",
3
- "version": "1.1.22-rik.3",
3
+ "version": "1.1.22-rik.4",
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/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
- * @param {unknown} id @param {number} chars @param {number} at @param {string|null} [model]
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 flight, or
414
- * tokens still in the window. The TUI keeps its fast tick for exactly as long.
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
- return this.streams.size > 0 || this.rate(at) > 0;
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
 
@@ -1099,7 +1099,48 @@ export class TUI {
1099
1099
 
1100
1100
  onRequestRouted(id, info) {
1101
1101
  const r = this.active.get(id);
1102
- if (r) r.account = info.account == null ? info.account : safeLine(info.account, 64);
1102
+ if (!r) return;
1103
+ r.account = info.account == null ? info.account : safeLine(info.account, 64);
1104
+ if (this._throughputOn()) this._markLeg(r, info.account);
1105
+ }
1106
+
1107
+ /**
1108
+ * Whether this request is the second leg of a generation the fleet meter
1109
+ * already counts, decided once, when the request is first routed.
1110
+ *
1111
+ * A translating sidecar (the Codex one in docs/openai.md) is a local-upstream
1112
+ * account that calls back through this proxy: Claude Code's request goes to
1113
+ * the sidecar, and the sidecar's translation of it comes back in and is served
1114
+ * by a real account. Both legs stream the same generation and both report its
1115
+ * tokens, so counting both doubles it. The outer leg is the one counted: it is
1116
+ * what the client asked for, and the sidecar hands it the translated count.
1117
+ *
1118
+ * The inner leg is recognised by what the sidecar forwards and nothing else
1119
+ * does: the SAME session as a request the proxy is currently serving on a
1120
+ * local upstream, in a DIFFERENT dialect from it (the path says which), since
1121
+ * the leg is that request translated. The session alone is not enough, because
1122
+ * one session runs unrelated requests side by side; those are in the client's
1123
+ * own dialect. A request that is itself on a local upstream is an outer leg,
1124
+ * never an inner one. With no session on either leg nothing can be paired, and
1125
+ * a local backend that does not call back never sends an inner leg, so in both
1126
+ * cases everything is counted, as before.
1127
+ *
1128
+ * O(active) once per routing, never per delta. A nested request still keeps
1129
+ * its own estimate and its own finished rate; it is only left out of the sum.
1130
+ *
1131
+ * @param {Record<string, any>} r the active entry
1132
+ * @param {string|null|undefined} name the account it was routed to
1133
+ */
1134
+ _markLeg(r, name) {
1135
+ const acct = name == null ? null : this.am.accounts.find((/** @type {any} */ a) => a.name === name);
1136
+ r.local = isLocalUpstream(acct);
1137
+ r.dialect ??= providerForPath(r.path || '');
1138
+ if (r.nested !== undefined) return;
1139
+ r.nested = false;
1140
+ if (r.local || !r.sessionId) return;
1141
+ for (const o of this.active.values()) {
1142
+ if (o !== r && o.local && o.sessionId === r.sessionId && o.dialect !== r.dialect) { r.nested = true; return; }
1143
+ }
1103
1144
  }
1104
1145
 
1105
1146
  /**
@@ -1113,7 +1154,7 @@ export class TUI {
1113
1154
  */
1114
1155
  onRequestProgress(id, { chars, at }) {
1115
1156
  const r = this.active.get(id);
1116
- if (r) this.throughput.progress(id, chars, at, r.model || null);
1157
+ if (r) this.throughput.progress(id, chars, at, r.model || null, !r.nested);
1117
1158
  }
1118
1159
 
1119
1160
  onRequestEnd(id, info) {
@@ -1134,7 +1175,7 @@ export class TUI {
1134
1175
  outputTokens: info.outputTokens, firstAt: info.firstTokenAt, lastAt: info.lastTokenAt,
1135
1176
  dispatchedAt: info.dispatchedAt ?? r?.started ?? null, endedAt: now, model: info.model || r?.model || null,
1136
1177
  };
1137
- this.throughput.finish(id, timing);
1178
+ this.throughput.finish(id, { ...timing, counted: !r?.nested });
1138
1179
  const tps = r ? requestRate({ ...timing, startedAt: timing.dispatchedAt }) : null;
1139
1180
  const tag = this._sessionTag(sid);
1140
1181
  // The line as it has always read. The rate is kept beside it rather than in