mslxdff 0.1.87 → 0.1.89

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.
@@ -1,6 +1,7 @@
1
1
  import { joinUrl } from "../base.js";
2
2
  import { isAuthError, isInsufficientStatus } from "./auth.js";
3
3
  import { appendRotationLog as defaultAppend } from "./rotation-log.js";
4
+ import { createTransport } from "../../transport/index.js";
4
5
 
5
6
  function buildAuthHeaders(key, auth) {
6
7
  const h = {
@@ -13,71 +14,62 @@ function buildAuthHeaders(key, auth) {
13
14
  };
14
15
  if (key) h["Authorization"] = `Bearer ${key}`;
15
16
  if (auth?.uid) h["X-User-Id"] = auth.uid;
16
- if (auth?.domain) h["X-Domain"] = auth.domain;
17
- else h["X-Domain"] = "www.codebuddy.cn";
18
- if (auth?.enterpriseId) {
19
- h["X-Enterprise-Id"] = auth.enterpriseId;
20
- h["X-Tenant-Id"] = auth.enterpriseId;
21
- }
17
+ h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
18
+ if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
22
19
  return h;
23
20
  }
24
21
 
25
22
  function withUidHeader(res, uid) {
26
- try {
27
- res.headers.set("x-mslxdff-workbuddy-uid", uid);
28
- return res;
29
- } catch {
23
+ try { res.headers.set("x-mslxdff-workbuddy-uid", uid); return res; }
24
+ catch {
30
25
  try {
31
26
  const h = new Headers(res.headers);
32
27
  h.set("x-mslxdff-workbuddy-uid", uid);
33
- const clone = new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
34
- clone._t = res._t;
35
- return clone;
36
- } catch {
37
- return res;
38
- }
28
+ const c = new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
29
+ c._t = res._t;
30
+ return c;
31
+ } catch { return res; }
39
32
  }
40
33
  }
41
34
 
42
- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
35
+ function nowMs(clock) { return typeof performance !== "undefined" && performance.now ? performance.now() : clock(); }
36
+
37
+ function errRes(status, msg, reason, uid, t0, clock) {
38
+ const r = new Response(JSON.stringify({ error: msg }), { status, headers: { "Content-Type": "application/json", "x-mslxdff-workbuddy-reason": reason, "x-mslxdff-workbuddy-uid": uid } });
39
+ r._t = { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0) };
40
+ return r;
41
+ }
42
+
43
+ function failErr(msg, t0, clock, extra) {
44
+ const e = new Error(msg);
45
+ e._t = { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0), ...(extra || {}) };
46
+ return e;
47
+ }
43
48
 
44
49
  export function createChatService({
45
50
  id = "workbuddy",
46
51
  baseUrl,
47
52
  chatPath,
48
- keys, // mutable array ref
49
- authList, // mutable array ref
50
- ring, // keyRing instance for auto mode
53
+ keys,
54
+ authList,
55
+ ring,
51
56
  fetchImpl,
52
57
  dispatcher,
53
58
  connectTimeoutMs = 30_000,
54
- retry = {
55
- network: { attempts: 2, delayMs: 300 },
56
- 429: { attempts: 1, delayMs: 100 },
57
- 502: { attempts: 1, delayMs: 100 },
58
- 503: { attempts: 1, delayMs: 100 },
59
- 504: { attempts: 1, delayMs: 100 },
60
- },
61
- balanceCache, // { getCachedBalance, setCachedBalance }
62
- authService, // { refreshTokenFor, maybeProactiveRefresh }
63
- logger, // { append } or null
59
+ balanceCache,
60
+ authService,
61
+ logger,
64
62
  clock = Date.now,
65
63
  cooldownMs = 30_000,
66
64
  } = {}) {
67
65
  const getBalance = balanceCache?.getCachedBalance || (() => null);
68
66
  const setBalance = balanceCache?.setCachedBalance || (() => {});
69
67
  const doLog = (opts) => {
70
- if (logger && typeof logger.append === "function") {
71
- try { logger.append(opts); } catch {}
72
- return;
73
- }
74
- if (logger && typeof logger.log === "function") {
75
- try { logger.log(opts); } catch {}
76
- return;
77
- }
78
- // fallback to default append (may write to fs; test passes noop logger so not called)
68
+ if (logger && typeof logger.append === "function") { try { logger.append(opts); } catch {} return; }
69
+ if (logger && typeof logger.log === "function") { try { logger.log(opts); } catch {} return; }
79
70
  try { defaultAppend(opts); } catch {}
80
71
  };
72
+ const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
81
73
 
82
74
  function authForKey(key) {
83
75
  const idx = keys.indexOf(key);
@@ -86,113 +78,77 @@ export function createChatService({
86
78
  return { uid: "", domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" };
87
79
  }
88
80
 
89
- async function attemptOnce(url, body, key) {
90
- const controller = new AbortController();
91
- const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
92
- try {
93
- const auth = authForKey(key);
94
- const headers = buildAuthHeaders(key, auth);
95
- const finalBody = { ...body, stream: true };
96
- const opts = { method: "POST", headers, body: JSON.stringify(finalBody), signal: controller.signal };
97
- if (dispatcher) opts.dispatcher = dispatcher;
98
- const res = await fetchImpl(url, opts);
99
- return res;
100
- } catch (err) {
101
- return err;
102
- } finally {
103
- clearTimeout(timer);
104
- }
81
+ async function fetchOnce(url, body, key, auth) {
82
+ return transport.request({ url, headers: buildAuthHeaders(key, auth), body: { ...body, stream: true }, stream: true });
105
83
  }
106
84
 
85
+ async function withRefresh(url, body, key, auth) {
86
+ let res;
87
+ try { res = await fetchOnce(url, body, key, auth); }
88
+ catch (e) { throw e; }
89
+ if (res.status < 400) return res;
90
+ let txt = "";
91
+ try { txt = await res.text(); } catch {}
92
+ if (!isAuthError(res.status, txt)) return res;
93
+ const newKey = await authService?.refreshTokenFor?.(key, auth);
94
+ if (!newKey) throw failErr(`workbuddy refresh failed for ${auth?.uid || ""}: ${txt.slice(0, 120)}`, _t0(), clock);
95
+ const auth2 = authForKey(newKey);
96
+ let res2;
97
+ try { res2 = await fetchOnce(url, body, newKey, auth2); }
98
+ catch (e2) { throw e2; }
99
+ let stillTxt = "";
100
+ if (res2.status >= 400) { try { stillTxt = await res2.text(); } catch {} }
101
+ const stillAuth = isAuthError(res2.status, stillTxt);
102
+ const uid2 = auth2.uid || auth.uid;
103
+ res2 = withUidHeader(res2, uid2);
104
+ res2._t = res2._t || { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - _t0()) };
105
+ if (stillAuth || res2.status === 429 || res2.status >= 500) try { ring.onError(newKey); } catch {}
106
+ doLog({ uid: uid2, model: _modelForLog(), totalMs: Math.round(nowMs(clock) - _t0()), balanceHit: false, error: stillAuth ? `still auth ${res2.status}` : undefined });
107
+ if (stillAuth) throw failErr(`workbuddy auth still failing after refresh for ${uid2}: ${stillTxt.slice(0, 120)}`, _t0(), clock);
108
+ return res2;
109
+ }
110
+
111
+ let _t0Val = 0;
112
+ let _modelVal = "";
113
+ function _t0() { return _t0Val; }
114
+ function _modelForLog() { return _modelVal; }
115
+
107
116
  async function runChat(body, activeRing, opts = {}) {
108
117
  const url = joinUrl(baseUrl, chatPath);
109
- const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
118
+ const t0 = nowMs(clock);
119
+ _t0Val = t0;
120
+ _modelVal = body?.model || "";
110
121
  const preferredUid = opts?.workbuddyUid ? String(opts.workbuddyUid).trim() : "";
111
- const modelForLog = body?.model || "";
112
122
 
113
123
  if (preferredUid) {
114
- const idx = authList.findIndex(a => a.uid === preferredUid || a.uid.startsWith(preferredUid));
115
- if (idx < 0) {
116
- const errBody = JSON.stringify({ error: `workbuddy uid not found: ${preferredUid}` });
117
- const res = new Response(errBody, { status: 403, headers: { "Content-Type": "application/json", "x-mslxdff-workbuddy-reason": "uid-not-found", "x-mslxdff-workbuddy-uid": preferredUid } });
118
- res._t = { attempts: [], waitMs: 0, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
119
- return res;
120
- }
124
+ const idx = authList.findIndex((a) => a.uid === preferredUid || String(a.uid).startsWith(preferredUid));
125
+ if (idx < 0) return errRes(403, `workbuddy uid not found: ${preferredUid}`, "uid-not-found", preferredUid, t0, clock);
121
126
  const key = keys[idx];
122
127
  const auth = authList[idx];
123
128
  authService?.maybeProactiveRefresh?.(auth, key);
124
129
  const cached = getBalance(auth.uid);
125
- if (cached && Number(cached.total) === 0) {
126
- const errBody = JSON.stringify({ error: `workbuddy uid in cooldown (balance 0): ${auth.uid}` });
127
- const res = new Response(errBody, { status: 403, headers: { "Content-Type": "application/json", "x-mslxdff-workbuddy-reason": "uid-cooling", "x-mslxdff-workbuddy-uid": auth.uid } });
128
- res._t = { attempts: [], waitMs: 0, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
130
+ if (cached && Number(cached.total) === 0) return errRes(403, `workbuddy uid in cooldown (balance 0): ${auth.uid}`, "uid-cooling", auth.uid, t0, clock);
131
+ try {
132
+ let res = await withRefresh(url, body, key, auth);
133
+ res = withUidHeader(res, auth.uid);
134
+ res._t = res._t || { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0) };
135
+ if (res.status === 401 || res.status === 403 || res.status === 429 || res.status >= 500) try { activeRing.onError(key); } catch {}
136
+ doLog({ uid: auth.uid, model: _modelVal, totalMs: Math.round(nowMs(clock) - t0), balanceHit: false });
129
137
  return res;
130
- }
131
- const attempts = [];
132
- let waitMs = 0;
133
- for (let attempt = 0; ; attempt++) {
134
- const t = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
135
- let result = await attemptOnce(url, body, key);
136
- const type = result instanceof Error ? "network" : `http${result?.status}`;
137
- attempts.push({ attempt, type, ms: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t) });
138
- if (result instanceof Error) {
139
- const entry = retry?.network;
140
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
141
- activeRing.onError(key);
142
- result._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
143
- throw result;
144
- }
145
- if (attempt === 0) {
146
- let bodyText = "";
147
- try { bodyText = await result.clone().text(); } catch {}
148
- if (isAuthError(result.status, bodyText)) {
149
- const newKey = await authService?.refreshTokenFor?.(key, auth);
150
- if (newKey) {
151
- const auth2 = authForKey(newKey);
152
- const headers2 = buildAuthHeaders(newKey, auth2);
153
- const finalBody = { ...body, stream: true };
154
- const controller2 = new AbortController();
155
- const timer2 = setTimeout(() => controller2.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
156
- try {
157
- const opts2 = { method: "POST", headers: headers2, body: JSON.stringify(finalBody), signal: controller2.signal };
158
- if (dispatcher) opts2.dispatcher = dispatcher;
159
- let res2 = await fetchImpl(url, opts2);
160
- let res2Body = "";
161
- try { res2Body = await res2.clone().text(); } catch {}
162
- const stillAuth = isAuthError(res2.status, res2Body);
163
- res2._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), refreshed: true };
164
- res2 = withUidHeader(res2, auth2.uid || auth.uid);
165
- if (stillAuth || res2.status === 429 || res2.status >= 500) activeRing.onError(newKey);
166
- doLog({ uid: auth2.uid || auth.uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: false, error: stillAuth ? `still auth ${res2.status}` : undefined });
167
- return res2;
168
- } catch (e2) { e2._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) }; throw e2; } finally { clearTimeout(timer2); }
169
- }
170
- }
171
- }
172
- const entry = retry?.[result.status];
173
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
174
- if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
175
- result = withUidHeader(result, auth.uid);
176
- result._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
177
- doLog({ uid: auth.uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: false });
178
- return result;
138
+ } catch (e) {
139
+ e._t = e._t || { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0) };
140
+ if (!(e instanceof Error) || !String(e.message).includes("refresh failed")) try { activeRing.onError(key); } catch {}
141
+ throw e;
179
142
  }
180
143
  }
181
144
 
182
- // auto rotation
183
- const attempts = [];
184
- let waitMs = 0;
185
- let lastErr = null;
186
145
  const tried = new Set();
187
146
  const maxTries = Math.max(1, keys.length);
147
+ let lastErr = null;
188
148
  for (let triedCount = 0; triedCount < maxTries; triedCount++) {
189
149
  const key = activeRing.next();
190
150
  if (!key) {
191
- if (triedCount === 0) {
192
- const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
193
- err._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), cooldownMs };
194
- throw err;
195
- }
151
+ if (triedCount === 0) throw failErr(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`, t0, clock, { cooldownMs });
196
152
  break;
197
153
  }
198
154
  const auth = authForKey(key);
@@ -201,110 +157,42 @@ export function createChatService({
201
157
  tried.add(uid || key);
202
158
  const cached = uid ? getBalance(uid) : null;
203
159
  if (cached && Number(cached.total) === 0) {
204
- activeRing.onError(key);
160
+ try { activeRing.onError(key); } catch {}
205
161
  setBalance(uid, { ...cached, total: 0 });
206
- doLog({ uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: true });
207
- // 单号余额为 0 直接返回 403,避免抛“exhausted”让调用方误判为 500
208
- if (keys.length === 1 || activeRing.available() === 0) {
209
- const errBody = JSON.stringify({ error: `workbuddy uid in cooldown (balance 0): ${uid}` });
210
- const res = new Response(errBody, { status: 403, headers: { "Content-Type": "application/json", "x-mslxdff-workbuddy-reason": "uid-cooling", "x-mslxdff-workbuddy-uid": uid } });
211
- res._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
212
- return res;
213
- }
162
+ doLog({ uid, model: _modelVal, totalMs: Math.round(nowMs(clock) - t0), balanceHit: true });
163
+ if (keys.length === 1 || activeRing.available() === 0) return errRes(403, `workbuddy uid in cooldown (balance 0): ${uid}`, "uid-cooling", uid, t0, clock);
214
164
  continue;
215
165
  }
216
- if (!key && activeRing.size === 0) {
217
- const err = new Error(`${id}: missing MSLXDFF_WORKBUDDY_KEY (chat requires a real key) — run node workbuddy-token-auto.js`);
218
- err._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
219
- throw err;
220
- }
166
+ if (!key && activeRing.size === 0) throw failErr(`${id}: missing MSLXDFF_WORKBUDDY_KEY (chat requires a real key) — run node workbuddy-token-auto.js`, t0, clock);
221
167
  authService?.maybeProactiveRefresh?.(auth, key);
222
- let result = null;
223
- let attempt = 0;
224
- for (; ; attempt++) {
225
- const t = typeof performance !== "undefined" && performance.now ? performance.now() : clock();
226
- result = await attemptOnce(url, body, key);
227
- const type = result instanceof Error ? "network" : `http${result?.status}`;
228
- attempts.push({ attempt: triedCount * 10 + attempt, type, ms: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t), uid });
229
- if (result instanceof Error) {
230
- const entry = retry?.network;
231
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
232
- activeRing.onError(key);
233
- lastErr = result;
234
- result._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
235
- break;
236
- }
237
- if (attempt === 0) {
238
- let bodyText = "";
239
- try { bodyText = await result.clone().text(); } catch {}
240
- const needRefresh = isAuthError(result.status, bodyText);
241
- if (needRefresh) {
242
- const newKey = await authService?.refreshTokenFor?.(key, auth);
243
- if (newKey) {
244
- const auth2 = authForKey(newKey);
245
- const headers2 = buildAuthHeaders(newKey, auth2);
246
- const finalBody = { ...body, stream: true };
247
- const controller2 = new AbortController();
248
- const timer2 = setTimeout(() => controller2.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
249
- try {
250
- const opts2 = { method: "POST", headers: headers2, body: JSON.stringify(finalBody), signal: controller2.signal };
251
- if (dispatcher) opts2.dispatcher = dispatcher;
252
- let res2 = await fetchImpl(url, opts2);
253
- let res2Body = "";
254
- try { res2Body = await res2.clone().text(); } catch {}
255
- const stillAuth = isAuthError(res2.status, res2Body);
256
- res2._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), refreshed: true };
257
- res2 = withUidHeader(res2, auth2.uid || uid);
258
- if (stillAuth || res2.status === 429 || res2.status >= 500) activeRing.onError(newKey);
259
- doLog({ uid: auth2.uid || uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: false, error: stillAuth ? `still auth ${res2.status}` : undefined });
260
- if (stillAuth) {
261
- lastErr = new Error(`workbuddy auth still failing after refresh for ${uid}: ${res2Body.slice(0, 120)}`);
262
- lastErr._t = res2._t;
263
- activeRing.onError(newKey);
264
- break;
265
- }
266
- return res2;
267
- } catch (e2) { e2._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) }; lastErr = e2; break; } finally { clearTimeout(timer2); }
268
- } else {
269
- lastErr = new Error(`workbuddy refresh failed for ${uid}: ${bodyText.slice(0, 120)}`);
270
- lastErr._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
271
- activeRing.onError(key);
272
- break;
273
- }
274
- }
275
- }
276
- const entry = retry?.[result.status];
277
- if (entry && attempt < entry.attempts) { await sleep(entry.delayMs); waitMs += entry.delayMs; continue; }
278
- break;
279
- }
280
- if (result instanceof Error) {
168
+ let res;
169
+ try { res = await withRefresh(url, body, key, auth); }
170
+ catch (e) {
171
+ e._t = e._t || { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0) };
172
+ try { activeRing.onError(key); } catch {}
173
+ lastErr = e;
281
174
  continue;
282
175
  }
283
- let bodyText = "";
284
- let isInsuff = false;
285
- if (result.status === 402 || result.status === 403 || result.status === 429) {
286
- try { bodyText = await result.clone().text(); } catch { try { bodyText = await result.text(); } catch { bodyText = ""; } }
287
- isInsuff = isInsufficientStatus(result.status, bodyText, cached);
288
- }
289
- if (isInsuff) {
290
- activeRing.onError(key);
291
- if (uid) setBalance(uid, { total: 0, dailyPacks: 0, activeCount: 0, nextExpire: null, fetchedAt: clock() });
292
- doLog({ uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: true, error: bodyText.slice(0, 120) });
293
- lastErr = new Error(`workbuddy insufficient for ${uid}: ${bodyText.slice(0, 120)}`);
294
- lastErr._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
295
- continue;
176
+ if (res.status === 402 || res.status === 403 || res.status === 429) {
177
+ let txt = "";
178
+ try { txt = await res.text(); } catch {}
179
+ if (isInsufficientStatus(res.status, txt, cached)) {
180
+ try { activeRing.onError(key); } catch {}
181
+ if (uid) setBalance(uid, { total: 0, dailyPacks: 0, activeCount: 0, nextExpire: null, fetchedAt: clock() });
182
+ doLog({ uid, model: _modelVal, totalMs: Math.round(nowMs(clock) - t0), balanceHit: true, error: txt.slice(0, 120) });
183
+ lastErr = failErr(`workbuddy insufficient for ${uid}: ${txt.slice(0, 120)}`, t0, clock);
184
+ continue;
185
+ }
296
186
  }
297
- if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
298
- result = withUidHeader(result, uid);
299
- result._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
300
- doLog({ uid, model: modelForLog, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0), balanceHit: false });
301
- return result;
187
+ if (res.status === 401 || res.status === 403 || res.status === 429 || res.status >= 500) try { activeRing.onError(key); } catch {}
188
+ res = withUidHeader(res, uid);
189
+ res._t = res._t || { attempts: [], waitMs: 0, totalMs: Math.round(nowMs(clock) - t0) };
190
+ doLog({ uid, model: _modelVal, totalMs: Math.round(nowMs(clock) - t0), balanceHit: false });
191
+ return res;
302
192
  }
303
193
  if (lastErr) throw lastErr;
304
- const err = new Error(`${id}: all workbuddy accounts exhausted or unavailable`);
305
- err._t = { attempts, waitMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : clock()) - t0) };
306
- throw err;
194
+ throw failErr(`${id}: all workbuddy accounts exhausted or unavailable`, t0, clock);
307
195
  }
308
196
 
309
- return { runChat, authForKey, buildAuthHeaders, withUidHeader, attemptOnce };
197
+ return { runChat, authForKey, buildAuthHeaders, withUidHeader, attemptOnce: fetchOnce };
310
198
  }