@omnicross/subscriptions 0.1.2 → 0.1.3

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/dist/index.cjs CHANGED
@@ -1,29 +1,281 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9;
2
2
 
3
3
 
4
4
 
5
5
  var _chunkTPW5Q25Ycjs = require('./chunk-TPW5Q25Y.cjs');
6
6
 
7
+ // src/scheduler/SubscriptionAccountSelector.ts
8
+ var SESSION_AFFINITY_TTL_MS = 36e5;
9
+ var LAST_USED_PERSIST_THROTTLE_MS = 6e4;
10
+ var DEFAULT_ACCOUNT_PRIORITY = 50;
11
+ function scopedKey(providerId, tail) {
12
+ return `${providerId}\0${tail}`;
13
+ }
14
+ function parseIso(iso) {
15
+ if (!iso) return 0;
16
+ const ms = Date.parse(iso);
17
+ return Number.isNaN(ms) ? 0 : ms;
18
+ }
19
+ var SubscriptionAccountSelector = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this); }
20
+ /** `providerId\0accountId → live lastUsedAt ms` (authoritative tie-break). */
21
+ __init() {this.lastUsedOverlay = /* @__PURE__ */ new Map()}
22
+ /** `providerId\0sessionKey → { accountId, expiresAt }`. */
23
+ __init2() {this.affinity = /* @__PURE__ */ new Map()}
24
+ /** `providerId\0accountId → last durable-persist ms` (throttle state). */
25
+ __init3() {this.lastPersist = /* @__PURE__ */ new Map()}
26
+ /**
27
+ * Choose the account to serve this request, or `null` when there are ≤ 1
28
+ * schedulable accounts (the zero-regression signal — caller uses the active
29
+ * account). Updates the live `lastUsedAt` overlay for the chosen account and,
30
+ * when a `sessionKey` is given, records/extends the affinity mapping.
31
+ */
32
+ select(input) {
33
+ const now = _nullishCoalesce(input.now, () => ( Date.now()));
34
+ const schedulable = input.accounts.filter((a) => a.schedulable !== false);
35
+ if (schedulable.length <= 1) return null;
36
+ if (input.sessionKey) {
37
+ const aKey = scopedKey(input.providerId, input.sessionKey);
38
+ const hit = this.affinity.get(aKey);
39
+ if (hit && hit.expiresAt > now && schedulable.some((a) => a.id === hit.accountId)) {
40
+ hit.expiresAt = now + SESSION_AFFINITY_TTL_MS;
41
+ this.markUsed(input.providerId, hit.accountId, now);
42
+ return { accountId: hit.accountId, isActive: hit.accountId === input.activeAccountId };
43
+ }
44
+ }
45
+ const chosen = this.pickOrdered(input.providerId, schedulable);
46
+ this.markUsed(input.providerId, chosen.id, now);
47
+ if (input.sessionKey) {
48
+ this.affinity.set(scopedKey(input.providerId, input.sessionKey), {
49
+ accountId: chosen.id,
50
+ expiresAt: now + SESSION_AFFINITY_TTL_MS
51
+ });
52
+ }
53
+ return { accountId: chosen.id, isActive: chosen.id === input.activeAccountId };
54
+ }
55
+ /**
56
+ * Drop every session-affinity mapping bound to this account (subscription-account-
57
+ * health, task 4.1). Called when a selected account's by-id token turns out
58
+ * null/invalid or an affinity-bound account becomes unhealthy, so the next
59
+ * selection for those sessions picks a fresh account instead of re-sticking to
60
+ * the bad one. O(affinity entries) — the map is tiny (one entry per live
61
+ * conversation).
62
+ */
63
+ evictAffinity(providerId, accountId) {
64
+ const prefix = `${providerId} `;
65
+ for (const [key, entry] of this.affinity) {
66
+ if (entry.accountId === accountId && key.startsWith(prefix)) {
67
+ this.affinity.delete(key);
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Whether a best-effort `lastUsedAt` durable persist is DUE for this account
73
+ * (≥ `LAST_USED_PERSIST_THROTTLE_MS` since the last one). Records the persist
74
+ * time when it returns `true`, so the strategy calls `touchAccountLastUsed`
75
+ * sparingly and the request hot path does not rewrite the store every request.
76
+ */
77
+ duePersist(providerId, accountId, now = Date.now()) {
78
+ const key = scopedKey(providerId, accountId);
79
+ const last = _nullishCoalesce(this.lastPersist.get(key), () => ( 0));
80
+ if (now - last < LAST_USED_PERSIST_THROTTLE_MS) return false;
81
+ this.lastPersist.set(key, now);
82
+ return true;
83
+ }
84
+ /** Sort by `priority` asc → effective `lastUsedAt` asc → `createdAt` asc → `[0]`. */
85
+ pickOrdered(providerId, accounts) {
86
+ const ranked = accounts.map((account) => ({
87
+ account,
88
+ priority: _nullishCoalesce(account.priority, () => ( DEFAULT_ACCOUNT_PRIORITY)),
89
+ lastUsed: this.effectiveLastUsed(providerId, account),
90
+ created: parseIso(account.createdAt)
91
+ }));
92
+ ranked.sort(
93
+ (x, y) => x.priority - y.priority || x.lastUsed - y.lastUsed || x.created - y.created
94
+ );
95
+ return ranked[0].account;
96
+ }
97
+ /** The live tie-break value: the in-memory overlay when set, else the persisted
98
+ * `lastUsedAt` (0 when absent). */
99
+ effectiveLastUsed(providerId, account) {
100
+ const overlay = this.lastUsedOverlay.get(scopedKey(providerId, account.id));
101
+ return _nullishCoalesce(overlay, () => ( parseIso(account.lastUsedAt)));
102
+ }
103
+ markUsed(providerId, accountId, now) {
104
+ this.lastUsedOverlay.set(scopedKey(providerId, accountId), now);
105
+ }
106
+ }, _class);
107
+
108
+ // src/SubscriptionAccountService.ts
109
+ var _SubscriptionAccountHealth = require('@omnicross/core/pipeline/SubscriptionAccountHealth');
110
+
111
+ // src/scheduler/accountModelMap.ts
112
+ function canonicalModelId(value) {
113
+ const idx = value.indexOf(",");
114
+ const bare = idx >= 0 ? value.slice(idx + 1) : value;
115
+ return bare.trim().toLowerCase();
116
+ }
117
+ function accountSupportsModel(supportedModels, model) {
118
+ if (!supportedModels) return true;
119
+ const target = canonicalModelId(model);
120
+ if (Array.isArray(supportedModels)) {
121
+ return supportedModels.some((m) => canonicalModelId(m) === target);
122
+ }
123
+ return Object.keys(supportedModels).some((k) => canonicalModelId(k) === target);
124
+ }
125
+ function remapForAccount(supportedModels, model) {
126
+ if (!supportedModels || Array.isArray(supportedModels)) return model;
127
+ const target = canonicalModelId(model);
128
+ for (const [key, actual] of Object.entries(supportedModels)) {
129
+ if (canonicalModelId(key) === target) return actual;
130
+ }
131
+ return model;
132
+ }
133
+ function remapReportForAccount(supportedModels, model) {
134
+ if (!model) return void 0;
135
+ const remapped = remapForAccount(supportedModels, model);
136
+ return remapped === model ? void 0 : remapped;
137
+ }
138
+
139
+ // src/scheduler/accountSelection.ts
140
+ var ACCOUNTS_KEY = {
141
+ claude: "claudeAccounts",
142
+ codex: "codexAccounts",
143
+ gemini: "geminiAccounts",
144
+ opencodego: "opencodegoAccounts"
145
+ };
146
+ var ACTIVE_KEY = {
147
+ claude: "activeClaudeAccountId",
148
+ codex: "activeCodexAccountId",
149
+ gemini: "activeGeminiAccountId",
150
+ opencodego: "activeOpencodegoAccountId"
151
+ };
152
+ function gateSchedulable(accounts, providerId, health, now, resolvedModel, supportedModelsById) {
153
+ if (!health && !resolvedModel || accounts.length < 2) return accounts;
154
+ return accounts.map((a) => {
155
+ const healthOk = health ? health.isSchedulable(providerId, a.id, now) : true;
156
+ const modelOk = resolvedModel ? accountSupportsModel(supportedModelsById.get(a.id), resolvedModel) : true;
157
+ return { ...a, schedulable: healthOk && modelOk };
158
+ });
159
+ }
160
+ function isPoolGated(accounts, health, resolvedModel) {
161
+ return (health !== void 0 || resolvedModel !== void 0) && accounts.length >= 2;
162
+ }
163
+ function readSchedulableAccounts(config, providerId) {
164
+ const raw = _nullishCoalesce(config[ACCOUNTS_KEY[providerId]], () => ( []));
165
+ const accounts = raw.map((a) => ({
166
+ id: a.id,
167
+ priority: a.priority,
168
+ lastUsedAt: a.lastUsedAt,
169
+ createdAt: a.createdAt
170
+ }));
171
+ const supportedModelsById = new Map(
172
+ raw.map((a) => [a.id, a.supportedModels])
173
+ );
174
+ const activeAccountId = config[ACTIVE_KEY[providerId]];
175
+ return { accounts, activeAccountId, supportedModelsById };
176
+ }
177
+ function pickByIdTarget(selector, gated, providerId, activeAccountId, sessionKey, now, healthGated) {
178
+ const selection = selector.select({ providerId, accounts: gated, activeAccountId, sessionKey, now });
179
+ if (selection && !selection.isActive) return selection.accountId;
180
+ if (selection === null && healthGated) {
181
+ const schedulable = gated.filter((a) => a.schedulable !== false);
182
+ if (schedulable.length === 1 && schedulable[0].id !== activeAccountId) return schedulable[0].id;
183
+ }
184
+ return void 0;
185
+ }
186
+ async function resolveSelectedToken(selector, tokens, providerId, sessionKey, activeGetter, ctx) {
187
+ const health = _optionalChain([ctx, 'optionalAccess', _ => _.health]);
188
+ const report = _optionalChain([ctx, 'optionalAccess', _2 => _2.reportSelection]);
189
+ const now = _optionalChain([ctx, 'optionalAccess', _3 => _3.now]);
190
+ const resolvedModel = _optionalChain([ctx, 'optionalAccess', _4 => _4.resolvedModel]);
191
+ if (selector && tokens.getAccessTokenForAccount) {
192
+ const config = await tokens.getFullConfig();
193
+ const { accounts, activeAccountId, supportedModelsById } = readSchedulableAccounts(config, providerId);
194
+ const gated = gateSchedulable(accounts, providerId, health, now, resolvedModel, supportedModelsById);
195
+ const poolGated = isPoolGated(accounts, health, resolvedModel);
196
+ const remapFor = (id) => remapReportForAccount(supportedModelsById.get(id), resolvedModel);
197
+ const targetId = pickByIdTarget(selector, gated, providerId, activeAccountId, sessionKey, now, poolGated);
198
+ if (targetId !== void 0) {
199
+ const byId = await tokens.getAccessTokenForAccount(providerId, targetId);
200
+ if (byId) {
201
+ maybeTouchLastUsed(selector, tokens, providerId, targetId);
202
+ _optionalChain([report, 'optionalCall', _5 => _5(targetId, false, remapFor(targetId))]);
203
+ return byId;
204
+ }
205
+ selector.evictAffinity(providerId, targetId);
206
+ _optionalChain([health, 'optionalAccess', _6 => _6.recordUpstreamOutcome, 'call', _7 => _7(providerId, targetId, { status: 401, now })]);
207
+ const remaining = gated.filter((a) => a.id !== targetId);
208
+ const retryId = pickByIdTarget(selector, remaining, providerId, activeAccountId, sessionKey, now, poolGated);
209
+ if (retryId !== void 0) {
210
+ const retryToken = await tokens.getAccessTokenForAccount(providerId, retryId);
211
+ if (retryToken) {
212
+ maybeTouchLastUsed(selector, tokens, providerId, retryId);
213
+ _optionalChain([report, 'optionalCall', _8 => _8(retryId, false, remapFor(retryId))]);
214
+ return retryToken;
215
+ }
216
+ }
217
+ }
218
+ if (activeAccountId) _optionalChain([report, 'optionalCall', _9 => _9(activeAccountId, true, remapFor(activeAccountId))]);
219
+ return activeGetter();
220
+ }
221
+ return activeGetter();
222
+ }
223
+ function maybeTouchLastUsed(selector, tokens, providerId, accountId) {
224
+ if (!tokens.touchAccountLastUsed) return;
225
+ if (!selector.duePersist(providerId, accountId)) return;
226
+ void tokens.touchAccountLastUsed(providerId, accountId, (/* @__PURE__ */ new Date()).toISOString()).catch(() => {
227
+ });
228
+ }
229
+ async function refreshSelectedAccount(selector, tokens, mutex, providerId, sessionKey) {
230
+ if (!sessionKey || !selector || !tokens.refreshAccountToken) return null;
231
+ const config = await tokens.getFullConfig();
232
+ const { accounts, activeAccountId } = readSchedulableAccounts(config, providerId);
233
+ const selection = selector.select({ providerId, accounts, activeAccountId, sessionKey });
234
+ if (!selection || selection.isActive) return null;
235
+ const accountId = selection.accountId;
236
+ return mutex.run(`${providerId}:${accountId}`, async () => {
237
+ try {
238
+ return await _asyncNullishCoalesce(await tokens.refreshAccountToken(providerId, accountId), async () => ( false));
239
+ } catch (err) {
240
+ console.warn(`[accountSelection] ${providerId}:${accountId} by-id refresh failed:`, err);
241
+ return false;
242
+ }
243
+ });
244
+ }
245
+
7
246
  // src/auth/OAuthBearerAuthStrategy.ts
8
247
  var REFRESH_LEAD_MS = 5 * 6e4;
9
- var OAuthBearerAuthStrategy = (_class = class {
10
- constructor(providerId, tokens, mutex) {;_class.prototype.__init.call(this);
248
+ var OAuthBearerAuthStrategy = (_class2 = class {
249
+ constructor(providerId, tokens, mutex, selector, health) {;_class2.prototype.__init4.call(this);
11
250
  this.tokens = tokens;
12
251
  this.mutex = mutex;
252
+ this.selector = selector;
253
+ this.health = health;
13
254
  this.providerId = providerId;
14
255
  }
15
256
 
16
257
 
17
- __init() {this.kind = "oauth-bearer"}
18
258
 
19
- async applyHeaders(headers, _hints) {
20
- const token = await this.resolveAccessToken();
259
+
260
+ __init4() {this.kind = "oauth-bearer"}
261
+
262
+ async applyHeaders(headers, hints) {
263
+ const token = await resolveSelectedToken(
264
+ this.selector,
265
+ this.tokens,
266
+ this.providerId,
267
+ _optionalChain([hints, 'optionalAccess', _10 => _10.sessionKey]),
268
+ () => this.resolveAccessToken(),
269
+ { health: this.health, reportSelection: _optionalChain([hints, 'optionalAccess', _11 => _11.reportSelection]), resolvedModel: _optionalChain([hints, 'optionalAccess', _12 => _12.resolvedModel]) }
270
+ );
21
271
  if (!token) {
22
272
  return;
23
273
  }
24
274
  headers["Authorization"] = `Bearer ${token}`;
25
275
  }
26
- async onUnauthorized() {
276
+ async onUnauthorized(sessionKey) {
277
+ const byId = await refreshSelectedAccount(this.selector, this.tokens, this.mutex, this.providerId, sessionKey);
278
+ if (byId !== null) return byId;
27
279
  return this.mutex.run(`${this.providerId}:refresh`, async () => {
28
280
  try {
29
281
  return this.providerId === "codex" ? await this.tokens.refreshCodexToken() : await this.tokens.refreshGeminiToken();
@@ -36,7 +288,7 @@ var OAuthBearerAuthStrategy = (_class = class {
36
288
  async describeStatus() {
37
289
  const config = await this.tokens.getFullConfig();
38
290
  const entry = this.providerId === "codex" ? config.codex : config.gemini;
39
- if (!_optionalChain([entry, 'optionalAccess', _ => _.accessToken])) {
291
+ if (!_optionalChain([entry, 'optionalAccess', _13 => _13.accessToken])) {
40
292
  return { providerId: this.providerId, ok: false, reason: "missing-credential" };
41
293
  }
42
294
  if (entry.status === "expired") {
@@ -53,7 +305,7 @@ var OAuthBearerAuthStrategy = (_class = class {
53
305
  async resolveAccessToken() {
54
306
  const config = await this.tokens.getFullConfig();
55
307
  const entry = this.providerId === "codex" ? config.codex : config.gemini;
56
- if (!_optionalChain([entry, 'optionalAccess', _2 => _2.accessToken])) return null;
308
+ if (!_optionalChain([entry, 'optionalAccess', _14 => _14.accessToken])) return null;
57
309
  const expiresAtMs = entry.expiresAt ? new Date(entry.expiresAt).getTime() : 0;
58
310
  const expiringSoon = expiresAtMs > 0 && Date.now() >= expiresAtMs - REFRESH_LEAD_MS;
59
311
  if (expiringSoon && entry.refreshToken) {
@@ -63,29 +315,42 @@ var OAuthBearerAuthStrategy = (_class = class {
63
315
  if (!refreshed) return null;
64
316
  const fresh = await this.tokens.getFullConfig();
65
317
  const freshEntry = this.providerId === "codex" ? fresh.codex : fresh.gemini;
66
- return _nullishCoalesce(_optionalChain([freshEntry, 'optionalAccess', _3 => _3.accessToken]), () => ( null));
318
+ return _nullishCoalesce(_optionalChain([freshEntry, 'optionalAccess', _15 => _15.accessToken]), () => ( null));
67
319
  }
68
320
  if (entry.status === "expired") return null;
69
321
  return entry.accessToken;
70
322
  }
71
- }, _class);
323
+ }, _class2);
72
324
 
73
325
  // src/auth/PassThroughAuthStrategy.ts
74
- var PassThroughAuthStrategy = (_class2 = class {
75
- constructor(tokens, mutex) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);
326
+ var PassThroughAuthStrategy = (_class3 = class {
327
+ constructor(tokens, mutex, selector, health) {;_class3.prototype.__init5.call(this);_class3.prototype.__init6.call(this);
76
328
  this.tokens = tokens;
77
329
  this.mutex = mutex;
330
+ this.selector = selector;
331
+ this.health = health;
78
332
  }
79
333
 
80
334
 
81
- __init2() {this.kind = "pass-through"}
82
- __init3() {this.providerId = "claude"}
83
- async applyHeaders(headers, _hints) {
84
- const token = await this.tokens.getValidClaudeAccessToken();
335
+
336
+
337
+ __init5() {this.kind = "pass-through"}
338
+ __init6() {this.providerId = "claude"}
339
+ async applyHeaders(headers, hints) {
340
+ const token = await resolveSelectedToken(
341
+ this.selector,
342
+ this.tokens,
343
+ "claude",
344
+ _optionalChain([hints, 'optionalAccess', _16 => _16.sessionKey]),
345
+ () => this.tokens.getValidClaudeAccessToken(),
346
+ { health: this.health, reportSelection: _optionalChain([hints, 'optionalAccess', _17 => _17.reportSelection]), resolvedModel: _optionalChain([hints, 'optionalAccess', _18 => _18.resolvedModel]) }
347
+ );
85
348
  if (!token) return;
86
349
  headers["Authorization"] = `Bearer ${token}`;
87
350
  }
88
- async onUnauthorized() {
351
+ async onUnauthorized(sessionKey) {
352
+ const byId = await refreshSelectedAccount(this.selector, this.tokens, this.mutex, "claude", sessionKey);
353
+ if (byId !== null) return byId;
89
354
  return this.mutex.run("claude:refresh", async () => {
90
355
  try {
91
356
  return await this.tokens.refreshClaudeToken();
@@ -98,7 +363,7 @@ var PassThroughAuthStrategy = (_class2 = class {
98
363
  async describeStatus() {
99
364
  const config = await this.tokens.getFullConfig();
100
365
  const claude = config.claude;
101
- if (!_optionalChain([claude, 'optionalAccess', _4 => _4.accessToken])) {
366
+ if (!_optionalChain([claude, 'optionalAccess', _19 => _19.accessToken])) {
102
367
  return { providerId: "claude", ok: false, reason: "missing-credential" };
103
368
  }
104
369
  if (claude.status === "expired") {
@@ -111,11 +376,11 @@ var PassThroughAuthStrategy = (_class2 = class {
111
376
  }
112
377
  return { providerId: "claude", ok: true, expiresAt: claude.expiresAt };
113
378
  }
114
- }, _class2);
379
+ }, _class3);
115
380
 
116
381
  // src/auth/RefreshMutex.ts
117
- var RefreshMutex = (_class3 = class {constructor() { _class3.prototype.__init4.call(this); }
118
- __init4() {this.inflight = /* @__PURE__ */ new Map()}
382
+ var RefreshMutex = (_class4 = class {constructor() { _class4.prototype.__init7.call(this); }
383
+ __init7() {this.inflight = /* @__PURE__ */ new Map()}
119
384
  /**
120
385
  * Run `task()` exclusively for `key`. If another caller is already running
121
386
  * for the same key, this call awaits the existing promise instead of
@@ -134,34 +399,45 @@ var RefreshMutex = (_class3 = class {constructor() { _class3.prototype.__init4.c
134
399
  this.inflight.set(key, promise);
135
400
  return promise;
136
401
  }
137
- }, _class3);
402
+ }, _class4);
138
403
 
139
404
  // src/auth/StaticBearerAuthStrategy.ts
140
405
  var ANTHROPIC_SHAPE_PATH = "/v1/messages";
141
- var StaticBearerAuthStrategy = (_class4 = class {
142
- constructor(tokens) {;_class4.prototype.__init5.call(this);_class4.prototype.__init6.call(this);
406
+ var StaticBearerAuthStrategy = (_class5 = class {
407
+ constructor(tokens, selector, health) {;_class5.prototype.__init8.call(this);_class5.prototype.__init9.call(this);
143
408
  this.tokens = tokens;
409
+ this.selector = selector;
410
+ this.health = health;
144
411
  }
145
412
 
146
- __init5() {this.kind = "static-bearer"}
147
- __init6() {this.providerId = "opencodego"}
413
+
414
+
415
+ __init8() {this.kind = "static-bearer"}
416
+ __init9() {this.providerId = "opencodego"}
148
417
  async applyHeaders(headers, hints) {
149
- const key = await this.tokens.getValidOpenCodeGoApiKey();
418
+ const key = await resolveSelectedToken(
419
+ this.selector,
420
+ this.tokens,
421
+ "opencodego",
422
+ _optionalChain([hints, 'optionalAccess', _20 => _20.sessionKey]),
423
+ () => this.tokens.getValidOpenCodeGoApiKey(),
424
+ { health: this.health, reportSelection: _optionalChain([hints, 'optionalAccess', _21 => _21.reportSelection]), resolvedModel: _optionalChain([hints, 'optionalAccess', _22 => _22.resolvedModel]) }
425
+ );
150
426
  if (!key) {
151
427
  return;
152
428
  }
153
429
  headers["Authorization"] = `Bearer ${key}`;
154
- if (_optionalChain([hints, 'optionalAccess', _5 => _5.upstreamUrl, 'optionalAccess', _6 => _6.includes, 'call', _7 => _7(ANTHROPIC_SHAPE_PATH)])) {
430
+ if (_optionalChain([hints, 'optionalAccess', _23 => _23.upstreamUrl, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25(ANTHROPIC_SHAPE_PATH)])) {
155
431
  headers["x-api-key"] = key;
156
432
  }
157
433
  }
158
- async onUnauthorized() {
434
+ async onUnauthorized(_sessionKey) {
159
435
  return false;
160
436
  }
161
437
  async describeStatus() {
162
438
  const config = await this.tokens.getFullConfig();
163
439
  const oc = config.opencodego;
164
- if (!_optionalChain([oc, 'optionalAccess', _8 => _8.apiKey])) {
440
+ if (!_optionalChain([oc, 'optionalAccess', _26 => _26.apiKey])) {
165
441
  return { providerId: "opencodego", ok: false, reason: "missing-credential" };
166
442
  }
167
443
  if (oc.status === "error") {
@@ -169,7 +445,7 @@ var StaticBearerAuthStrategy = (_class4 = class {
169
445
  }
170
446
  return { providerId: "opencodego", ok: true };
171
447
  }
172
- }, _class4);
448
+ }, _class5);
173
449
 
174
450
  // src/SubscriptionAccountService.ts
175
451
  var DISPLAY_NAMES = {
@@ -178,15 +454,19 @@ var DISPLAY_NAMES = {
178
454
  gemini: "Gemini (Google OAuth)",
179
455
  opencodego: "OpenCodeGo (Bearer key)"
180
456
  };
181
- var SubscriptionAccountService = (_class5 = class {
182
- __init7() {this.mutex = new RefreshMutex()}
457
+ var SubscriptionAccountService = (_class6 = class {
458
+ __init10() {this.mutex = new RefreshMutex()}
459
+ /** ONE account-pool scheduler (subscription-account-scheduling) shared by all
460
+ * four strategies so they share the affinity map + the `lastUsedAt` overlay. */
461
+ __init11() {this.selector = new SubscriptionAccountSelector()}
183
462
 
184
- constructor(tokens) {;_class5.prototype.__init7.call(this);
463
+ constructor(tokens) {;_class6.prototype.__init10.call(this);_class6.prototype.__init11.call(this);
464
+ const health = _SubscriptionAccountHealth.getSharedAccountHealth.call(void 0, );
185
465
  this.strategies = /* @__PURE__ */ new Map([
186
- ["claude", new PassThroughAuthStrategy(tokens, this.mutex)],
187
- ["codex", new OAuthBearerAuthStrategy("codex", tokens, this.mutex)],
188
- ["gemini", new OAuthBearerAuthStrategy("gemini", tokens, this.mutex)],
189
- ["opencodego", new StaticBearerAuthStrategy(tokens)]
466
+ ["claude", new PassThroughAuthStrategy(tokens, this.mutex, this.selector, health)],
467
+ ["codex", new OAuthBearerAuthStrategy("codex", tokens, this.mutex, this.selector, health)],
468
+ ["gemini", new OAuthBearerAuthStrategy("gemini", tokens, this.mutex, this.selector, health)],
469
+ ["opencodego", new StaticBearerAuthStrategy(tokens, this.selector, health)]
190
470
  ]);
191
471
  }
192
472
  /** Returns the strategy bound to a subscription provider, or `null` for unknown ids. */
@@ -214,7 +494,7 @@ var SubscriptionAccountService = (_class5 = class {
214
494
  }
215
495
  return entries;
216
496
  }
217
- }, _class5);
497
+ }, _class6);
218
498
  var _moduleSingleton = null;
219
499
  function setSubscriptionAccountService(svc) {
220
500
  _moduleSingleton = svc;
@@ -230,21 +510,21 @@ var _subscriptionRegistryPort = require('@omnicross/core/outbound-api/subscripti
230
510
  var _GeminiCodeAssistTransformer = require('@omnicross/core/transformer/transformers/GeminiCodeAssistTransformer');
231
511
 
232
512
  // src/opencodego/CircuitBreaker.ts
233
- var CircuitBreaker = (_class6 = class {
234
- __init8() {this.state = "closed"}
513
+ var CircuitBreaker = (_class7 = class {
514
+ __init12() {this.state = "closed"}
235
515
  /** CONSECUTIVE failures while closed (reset by any closed success). */
236
- __init9() {this.failureCount = 0}
516
+ __init13() {this.failureCount = 0}
237
517
  /** Successes accumulated in the current half-open probe window. */
238
- __init10() {this.successCount = 0}
518
+ __init14() {this.successCount = 0}
239
519
  /** Test calls admitted in the current half-open window (cap = halfOpenMaxCalls). */
240
- __init11() {this.halfOpenCalls = 0}
520
+ __init15() {this.halfOpenCalls = 0}
241
521
  /** `now()` at the last recorded failure — drives the open→half-open elapsed check. */
242
- __init12() {this.lastFailureTime = 0}
522
+ __init16() {this.lastFailureTime = 0}
243
523
 
244
524
 
245
525
 
246
526
 
247
- constructor(opts = {}) {;_class6.prototype.__init8.call(this);_class6.prototype.__init9.call(this);_class6.prototype.__init10.call(this);_class6.prototype.__init11.call(this);_class6.prototype.__init12.call(this);
527
+ constructor(opts = {}) {;_class7.prototype.__init12.call(this);_class7.prototype.__init13.call(this);_class7.prototype.__init14.call(this);_class7.prototype.__init15.call(this);_class7.prototype.__init16.call(this);
248
528
  this.threshold = _nullishCoalesce(opts.threshold, () => ( 3));
249
529
  this.openMs = _nullishCoalesce(opts.openMs, () => ( 3e4));
250
530
  this.halfOpenMaxCalls = _nullishCoalesce(opts.halfOpenMaxCalls, () => ( 3));
@@ -327,13 +607,13 @@ var CircuitBreaker = (_class6 = class {
327
607
  this.state = "open";
328
608
  }
329
609
  }
330
- }, _class6);
331
- var CircuitBreakerRegistry = (_class7 = class {
332
- constructor(options = {}) {;_class7.prototype.__init13.call(this);
610
+ }, _class7);
611
+ var CircuitBreakerRegistry = (_class8 = class {
612
+ constructor(options = {}) {;_class8.prototype.__init17.call(this);
333
613
  this.options = options;
334
614
  }
335
615
 
336
- __init13() {this.breakers = /* @__PURE__ */ new Map()}
616
+ __init17() {this.breakers = /* @__PURE__ */ new Map()}
337
617
  /** Get (or lazily create) the breaker for a model id. */
338
618
  get(modelId) {
339
619
  let breaker = this.breakers.get(modelId);
@@ -355,7 +635,7 @@ var CircuitBreakerRegistry = (_class7 = class {
355
635
  recordFailure(modelId) {
356
636
  this.get(modelId).recordFailure();
357
637
  }
358
- }, _class7);
638
+ }, _class8);
359
639
 
360
640
  // src/opencodego/defaults.ts
361
641
  var DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD = 8e4;
@@ -501,11 +781,11 @@ function resolveOpenCodeGoShape(entry) {
501
781
  function resolveOpenCodeGoHalf(modelId, config) {
502
782
  if (!config) return "go";
503
783
  for (const entry of Object.values(_nullishCoalesce(config.modelMap, () => ( {})))) {
504
- if (_optionalChain([entry, 'optionalAccess', _9 => _9.modelId]) === modelId) return _nullishCoalesce(entry.provider, () => ( "go"));
784
+ if (_optionalChain([entry, 'optionalAccess', _27 => _27.modelId]) === modelId) return _nullishCoalesce(entry.provider, () => ( "go"));
505
785
  }
506
786
  for (const list of Object.values(_nullishCoalesce(config.fallbacks, () => ( {})))) {
507
787
  for (const entry of _nullishCoalesce(list, () => ( []))) {
508
- if (_optionalChain([entry, 'optionalAccess', _10 => _10.modelId]) === modelId) return _nullishCoalesce(entry.provider, () => ( "go"));
788
+ if (_optionalChain([entry, 'optionalAccess', _28 => _28.modelId]) === modelId) return _nullishCoalesce(entry.provider, () => ( "go"));
509
789
  }
510
790
  }
511
791
  return "go";
@@ -605,7 +885,7 @@ function hasBackgroundPattern(loweredSlices) {
605
885
  return containsAny(loweredSlices, BACKGROUND_KEYWORDS);
606
886
  }
607
887
  function resolveOpenCodeGoScenario(summary, config) {
608
- const longContextThreshold = _nullishCoalesce(_optionalChain([config, 'optionalAccess', _11 => _11.modelMap, 'optionalAccess', _12 => _12.long_context, 'optionalAccess', _13 => _13.contextThreshold]), () => ( DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD));
888
+ const longContextThreshold = _nullishCoalesce(_optionalChain([config, 'optionalAccess', _29 => _29.modelMap, 'optionalAccess', _30 => _30.long_context, 'optionalAccess', _31 => _31.contextThreshold]), () => ( DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD));
609
889
  if (summary.estimatedInputTokens >= longContextThreshold) {
610
890
  return "long_context";
611
891
  }
@@ -637,8 +917,8 @@ function resolveOpenCodeGoTarget(modelId, config) {
637
917
  const shape = resolveOpenCodeGoShape({ provider: half, modelId });
638
918
  return { half, shape };
639
919
  }
640
- var SubscriptionProviderRegistry = (_class8 = class {
641
- constructor(accounts, tokens) {;_class8.prototype.__init14.call(this);
920
+ var SubscriptionProviderRegistry = (_class9 = class {
921
+ constructor(accounts, tokens) {;_class9.prototype.__init18.call(this);
642
922
  this.accounts = accounts;
643
923
  this.tokens = tokens;
644
924
  const claude = this.accounts.getStrategy("claude");
@@ -735,7 +1015,7 @@ var SubscriptionProviderRegistry = (_class8 = class {
735
1015
  resolveUpstreamUrl: (model, config) => {
736
1016
  const oc = config;
737
1017
  const { half, shape } = resolveOpenCodeGoTarget(model, oc);
738
- const override = half === "zen" ? _optionalChain([oc, 'optionalAccess', _14 => _14.zenBaseUrl]) : _optionalChain([oc, 'optionalAccess', _15 => _15.baseUrl]);
1018
+ const override = half === "zen" ? _optionalChain([oc, 'optionalAccess', _32 => _32.zenBaseUrl]) : _optionalChain([oc, 'optionalAccess', _33 => _33.baseUrl]);
739
1019
  return buildOpenCodeGoUrl(half, shape, override);
740
1020
  },
741
1021
  // zen seam (Decision 3): vary the provider transformer chain by resolved
@@ -753,7 +1033,7 @@ var SubscriptionProviderRegistry = (_class8 = class {
753
1033
  modelTransformerNames: [],
754
1034
  modelMapper: (sdkModel, summary, config) => {
755
1035
  const scenario = resolveOpenCodeGoScenario(summary, config);
756
- const entry = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([config, 'optionalAccess', _16 => _16.modelMap, 'optionalAccess', _17 => _17[scenario]]), () => ( _optionalChain([config, 'optionalAccess', _18 => _18.modelMap, 'optionalAccess', _19 => _19.default]))), () => ( DEFAULT_OPENCODEGO_MODEL_MAP[scenario])), () => ( DEFAULT_OPENCODEGO_MODEL_MAP.default));
1036
+ const entry = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_optionalChain([config, 'optionalAccess', _34 => _34.modelMap, 'optionalAccess', _35 => _35[scenario]]), () => ( _optionalChain([config, 'optionalAccess', _36 => _36.modelMap, 'optionalAccess', _37 => _37.default]))), () => ( DEFAULT_OPENCODEGO_MODEL_MAP[scenario])), () => ( DEFAULT_OPENCODEGO_MODEL_MAP.default));
757
1037
  if (!entry) {
758
1038
  return { resolvedModel: sdkModel, scenario };
759
1039
  }
@@ -772,7 +1052,7 @@ var SubscriptionProviderRegistry = (_class8 = class {
772
1052
  // wedge permanently in half-open). When NO circuit is open this returns
773
1053
  // the same first non-attempted entry as the prior `!attempted` filter.
774
1054
  nextFallback: (scenario, attempted, config) => {
775
- const list = _nullishCoalesce(_nullishCoalesce(_optionalChain([config, 'optionalAccess', _20 => _20.fallbacks, 'optionalAccess', _21 => _21[scenario]]), () => ( DEFAULT_OPENCODEGO_FALLBACKS[scenario])), () => ( []));
1055
+ const list = _nullishCoalesce(_nullishCoalesce(_optionalChain([config, 'optionalAccess', _38 => _38.fallbacks, 'optionalAccess', _39 => _39[scenario]]), () => ( DEFAULT_OPENCODEGO_FALLBACKS[scenario])), () => ( []));
776
1056
  for (const entry of list) {
777
1057
  if (attempted.includes(entry.modelId)) continue;
778
1058
  if (this.breaker.allowRequest(entry.modelId)) return entry;
@@ -803,7 +1083,7 @@ var SubscriptionProviderRegistry = (_class8 = class {
803
1083
  * exactly the reference's long-lived `FallbackHandler`. Constructed with the
804
1084
  * default reference thresholds (3 / 30s / 3) and the default `Date.now` clock.
805
1085
  */
806
- __init14() {this.breaker = new CircuitBreakerRegistry()}
1086
+ __init18() {this.breaker = new CircuitBreakerRegistry()}
807
1087
  /** Returns the dispatch profile for a known subscription provider, or
808
1088
  * `null` for unknown ids (callers must treat null as "fall back to the
809
1089
  * legacy LLM provider DB lookup"). */
@@ -818,7 +1098,7 @@ var SubscriptionProviderRegistry = (_class8 = class {
818
1098
  const full = await this.tokens.getFullConfig();
819
1099
  return full.opencodego;
820
1100
  }
821
- }, _class8);
1101
+ }, _class9);
822
1102
  var _moduleSingleton2 = null;
823
1103
  function setSubscriptionProviderRegistry(svc) {
824
1104
  _moduleSingleton2 = svc;
@@ -830,6 +1110,10 @@ function getSubscriptionProviderRegistry() {
830
1110
 
831
1111
  // src/SubscriptionDispatcher.ts
832
1112
  var _geminicodeassistresolver = require('@omnicross/core/ports/gemini-code-assist-resolver');
1113
+
1114
+
1115
+
1116
+
833
1117
  var _matchText = require('@omnicross/core/provider-proxy/matchText');
834
1118
  var _serializeError = require('@omnicross/core/serializeError');
835
1119
 
@@ -858,6 +1142,7 @@ var SubscriptionDispatcher = class {
858
1142
  * resolution and probe-detection.
859
1143
  */
860
1144
  async dispatch(req) {
1145
+ const sessionKey = _matchText.deriveSubscriptionSessionKey.call(void 0, req.anthropicBody);
861
1146
  const ocConfig = this.profile.providerId === "opencodego" ? await this.getOpenCodeGoConfig() : void 0;
862
1147
  let scenario = "default";
863
1148
  let resolvedModel = req.fallbackModel;
@@ -868,7 +1153,7 @@ var SubscriptionDispatcher = class {
868
1153
  scenario = mapped.scenario;
869
1154
  req.anthropicBody.model = resolvedModel;
870
1155
  }
871
- const upstreamUrl = _optionalChain([this, 'access', _22 => _22.profile, 'access', _23 => _23.resolveUpstreamUrl, 'optionalCall', _24 => _24(resolvedModel, ocConfig)]);
1156
+ const upstreamUrl = _optionalChain([this, 'access', _40 => _40.profile, 'access', _41 => _41.resolveUpstreamUrl, 'optionalCall', _42 => _42(resolvedModel, ocConfig)]);
872
1157
  if (!upstreamUrl) {
873
1158
  throw new Error(`[SubscriptionDispatcher] profile=${this.profile.providerId} missing resolveUpstreamUrl`);
874
1159
  }
@@ -876,41 +1161,53 @@ var SubscriptionDispatcher = class {
876
1161
  provider: resolveOpenCodeGoHalf(resolvedModel, ocConfig),
877
1162
  modelId: resolvedModel
878
1163
  }) === "anthropic") {
879
- await this.dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig);
1164
+ await this.dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey);
880
1165
  return;
881
1166
  }
882
- await this.dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig);
1167
+ await this.dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey);
883
1168
  }
884
1169
  /** Bypass path for OpenCodeGo MiniMax models — forwards Anthropic body verbatim. */
885
- async dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
1170
+ async dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey) {
886
1171
  const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
887
1172
  const attempted = gate.attempted;
888
1173
  let currentModel = gate.firstModel;
1174
+ let usedAccountId;
1175
+ const reportSelection = (accountId) => {
1176
+ usedAccountId = accountId;
1177
+ };
889
1178
  while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
890
1179
  attempted.push(currentModel);
891
1180
  req.anthropicBody.model = currentModel;
892
1181
  const headers = { "content-type": "application/json" };
893
- await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
1182
+ await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel, sessionKey, reportSelection });
894
1183
  console.info(
895
1184
  `[AgentProxy:subscription] REQ#${req.reqId} | opencodego anthropic-shape -> ${upstreamUrl} model=${currentModel} attempt=${attempted.length}`
896
1185
  );
897
1186
  try {
898
1187
  const upstream = await this.hooks.fetchWithRetry(upstreamUrl, headers, req.anthropicBody, currentModel);
899
- _optionalChain([this, 'access', _25 => _25.profile, 'access', _26 => _26.recordModelOutcome, 'optionalCall', _27 => _27(currentModel, true)]);
1188
+ _optionalChain([this, 'access', _43 => _43.profile, 'access', _44 => _44.recordModelOutcome, 'optionalCall', _45 => _45(currentModel, true)]);
1189
+ this.markHealth(usedAccountId, 200);
900
1190
  await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
901
1191
  return;
902
1192
  } catch (err) {
903
- const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
1193
+ const handled = await this.maybeRetryAfterError(err, headers, req, currentModel, sessionKey);
904
1194
  if (handled.retryOnce) {
905
- const upstream = await this.hooks.fetchWithRetry(upstreamUrl, handled.headers, req.anthropicBody, currentModel);
906
- _optionalChain([this, 'access', _28 => _28.profile, 'access', _29 => _29.recordModelOutcome, 'optionalCall', _30 => _30(currentModel, true)]);
907
- await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
908
- return;
1195
+ try {
1196
+ const upstream = await this.hooks.fetchWithRetry(upstreamUrl, handled.headers, req.anthropicBody, currentModel);
1197
+ _optionalChain([this, 'access', _46 => _46.profile, 'access', _47 => _47.recordModelOutcome, 'optionalCall', _48 => _48(currentModel, true)]);
1198
+ this.markHealth(usedAccountId, 200);
1199
+ await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
1200
+ return;
1201
+ } catch (retryErr) {
1202
+ this.markHealth(usedAccountId, errStatus(retryErr), retryErr);
1203
+ throw retryErr;
1204
+ }
909
1205
  }
910
1206
  if (caughtErrorBreakerOutcome(err) === "failure") {
911
- _optionalChain([this, 'access', _31 => _31.profile, 'access', _32 => _32.recordModelOutcome, 'optionalCall', _33 => _33(currentModel, false)]);
1207
+ _optionalChain([this, 'access', _49 => _49.profile, 'access', _50 => _50.recordModelOutcome, 'optionalCall', _51 => _51(currentModel, false)]);
912
1208
  }
913
- const next = _optionalChain([this, 'access', _34 => _34.profile, 'access', _35 => _35.nextFallback, 'optionalCall', _36 => _36(scenario, attempted, ocConfig)]);
1209
+ this.markHealth(usedAccountId, errStatus(err), err);
1210
+ const next = _optionalChain([this, 'access', _52 => _52.profile, 'access', _53 => _53.nextFallback, 'optionalCall', _54 => _54(scenario, attempted, ocConfig)]);
914
1211
  if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
915
1212
  throw err;
916
1213
  }
@@ -923,8 +1220,8 @@ var SubscriptionDispatcher = class {
923
1220
  }
924
1221
  }
925
1222
  /** Standard subscription transformer chain — Codex/Gemini/OpenCodeGo OpenAI-shape. */
926
- async dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
927
- const providerNames = _nullishCoalesce(_optionalChain([this, 'access', _37 => _37.profile, 'access', _38 => _38.resolveProviderTransformerNames, 'optionalCall', _39 => _39(resolvedModel, ocConfig)]), () => ( this.profile.providerTransformerNames));
1223
+ async dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey) {
1224
+ const providerNames = _nullishCoalesce(_optionalChain([this, 'access', _55 => _55.profile, 'access', _56 => _56.resolveProviderTransformerNames, 'optionalCall', _57 => _57(resolvedModel, ocConfig)]), () => ( this.profile.providerTransformerNames));
928
1225
  const providerTransformers = this.resolveTransformers(providerNames);
929
1226
  const modelTransformers = this.resolveTransformers(this.profile.modelTransformerNames);
930
1227
  const transformerProvider = {
@@ -940,6 +1237,10 @@ var SubscriptionDispatcher = class {
940
1237
  const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
941
1238
  const attempted = gate.attempted;
942
1239
  let currentModel = gate.firstModel;
1240
+ let usedAccountId;
1241
+ const reportSelection = (accountId) => {
1242
+ usedAccountId = accountId;
1243
+ };
943
1244
  while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
944
1245
  attempted.push(currentModel);
945
1246
  req.anthropicBody.model = currentModel;
@@ -954,14 +1255,15 @@ var SubscriptionDispatcher = class {
954
1255
  ...config.headers
955
1256
  };
956
1257
  stripAuthHeaders(headers);
957
- await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
1258
+ await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel, sessionKey, reportSelection });
958
1259
  const fetchUrl = usesResponsesChain(providerNames) ? upstreamUrl : _nullishCoalesce(resolveConfigUrl(config.url), () => ( upstreamUrl));
959
1260
  console.info(
960
1261
  `[AgentProxy:subscription] REQ#${req.reqId} | provider=${this.profile.providerId} -> ${fetchUrl} model=${currentModel} attempt=${attempted.length}`
961
1262
  );
962
1263
  try {
963
1264
  const upstream = await this.hooks.fetchWithRetry(fetchUrl, headers, requestBody, currentModel);
964
- _optionalChain([this, 'access', _40 => _40.profile, 'access', _41 => _41.recordModelOutcome, 'optionalCall', _42 => _42(currentModel, true)]);
1265
+ _optionalChain([this, 'access', _58 => _58.profile, 'access', _59 => _59.recordModelOutcome, 'optionalCall', _60 => _60(currentModel, true)]);
1266
+ this.markHealth(usedAccountId, 200);
965
1267
  const finalResponse = await this.hooks.executor.executeResponseChain(
966
1268
  requestBody,
967
1269
  upstream,
@@ -972,24 +1274,31 @@ var SubscriptionDispatcher = class {
972
1274
  await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
973
1275
  return;
974
1276
  } catch (err) {
975
- const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
1277
+ const handled = await this.maybeRetryAfterError(err, headers, req, currentModel, sessionKey);
976
1278
  if (handled.retryOnce) {
977
- const upstream = await this.hooks.fetchWithRetry(fetchUrl, handled.headers, requestBody, currentModel);
978
- _optionalChain([this, 'access', _43 => _43.profile, 'access', _44 => _44.recordModelOutcome, 'optionalCall', _45 => _45(currentModel, true)]);
979
- const finalResponse = await this.hooks.executor.executeResponseChain(
980
- requestBody,
981
- upstream,
982
- transformerProvider,
983
- { providerTransformers, modelTransformers },
984
- { endpointTransformer: this.hooks.endpointTransformer }
985
- );
986
- await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
987
- return;
1279
+ try {
1280
+ const upstream = await this.hooks.fetchWithRetry(fetchUrl, handled.headers, requestBody, currentModel);
1281
+ _optionalChain([this, 'access', _61 => _61.profile, 'access', _62 => _62.recordModelOutcome, 'optionalCall', _63 => _63(currentModel, true)]);
1282
+ this.markHealth(usedAccountId, 200);
1283
+ const finalResponse = await this.hooks.executor.executeResponseChain(
1284
+ requestBody,
1285
+ upstream,
1286
+ transformerProvider,
1287
+ { providerTransformers, modelTransformers },
1288
+ { endpointTransformer: this.hooks.endpointTransformer }
1289
+ );
1290
+ await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
1291
+ return;
1292
+ } catch (retryErr) {
1293
+ this.markHealth(usedAccountId, errStatus(retryErr), retryErr);
1294
+ throw retryErr;
1295
+ }
988
1296
  }
989
1297
  if (caughtErrorBreakerOutcome(err) === "failure") {
990
- _optionalChain([this, 'access', _46 => _46.profile, 'access', _47 => _47.recordModelOutcome, 'optionalCall', _48 => _48(currentModel, false)]);
1298
+ _optionalChain([this, 'access', _64 => _64.profile, 'access', _65 => _65.recordModelOutcome, 'optionalCall', _66 => _66(currentModel, false)]);
991
1299
  }
992
- const next = _optionalChain([this, 'access', _49 => _49.profile, 'access', _50 => _50.nextFallback, 'optionalCall', _51 => _51(scenario, attempted, ocConfig)]);
1300
+ this.markHealth(usedAccountId, errStatus(err), err);
1301
+ const next = _optionalChain([this, 'access', _67 => _67.profile, 'access', _68 => _68.nextFallback, 'optionalCall', _69 => _69(scenario, attempted, ocConfig)]);
993
1302
  if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
994
1303
  throw err;
995
1304
  }
@@ -1019,7 +1328,7 @@ var SubscriptionDispatcher = class {
1019
1328
  return { firstModel: primaryModel, attempted: [] };
1020
1329
  }
1021
1330
  const skipped = [primaryModel];
1022
- const firstAdmitting = _optionalChain([this, 'access', _52 => _52.profile, 'access', _53 => _53.nextFallback, 'optionalCall', _54 => _54(scenario, skipped, ocConfig)]);
1331
+ const firstAdmitting = _optionalChain([this, 'access', _70 => _70.profile, 'access', _71 => _71.nextFallback, 'optionalCall', _72 => _72(scenario, skipped, ocConfig)]);
1023
1332
  if (firstAdmitting) {
1024
1333
  console.warn(
1025
1334
  `[AgentProxy:subscription] opencodego primary ${primaryModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
@@ -1036,12 +1345,12 @@ var SubscriptionDispatcher = class {
1036
1345
  * Returns `{ retryOnce: true, headers }` when the strategy refreshed
1037
1346
  * successfully (caller should retry once); otherwise re-throws.
1038
1347
  */
1039
- async maybeRetryAfterError(err, headers, req, resolvedModel) {
1040
- const status = _nullishCoalesce(_optionalChain([err, 'optionalAccess', _55 => _55.status]), () => ( 0));
1348
+ async maybeRetryAfterError(err, headers, req, resolvedModel, sessionKey) {
1349
+ const status = _nullishCoalesce(_optionalChain([err, 'optionalAccess', _73 => _73.status]), () => ( 0));
1041
1350
  if (status !== 401) {
1042
1351
  return { retryOnce: false, headers };
1043
1352
  }
1044
- const refreshed = await this.profile.authStrategy.onUnauthorized();
1353
+ const refreshed = await this.profile.authStrategy.onUnauthorized(sessionKey);
1045
1354
  if (!refreshed) {
1046
1355
  console.warn(
1047
1356
  `[AgentProxy:subscription] REQ#${req.reqId} | 401 not recoverable for provider=${this.profile.providerId}`
@@ -1050,7 +1359,7 @@ var SubscriptionDispatcher = class {
1050
1359
  }
1051
1360
  const fresh = { ...headers };
1052
1361
  stripAuthHeaders(fresh);
1053
- await this.applyHeadersWithRetry(fresh, { upstreamUrl: "", resolvedModel });
1362
+ await this.applyHeadersWithRetry(fresh, { upstreamUrl: "", resolvedModel, sessionKey });
1054
1363
  return { retryOnce: true, headers: fresh };
1055
1364
  }
1056
1365
  async applyHeadersWithRetry(headers, hints) {
@@ -1060,6 +1369,28 @@ var SubscriptionDispatcher = class {
1060
1369
  console.warn("[AgentProxy:subscription] authStrategy.applyHeaders threw:", _serializeError.serializeError.call(void 0, err));
1061
1370
  }
1062
1371
  }
1372
+ /**
1373
+ * Mark the served account's health against ONE attempt's outcome
1374
+ * (subscription-account-health, task 3.4). No-op when no account was reported
1375
+ * (non-pooled / single-account) or on a session-cancel (status 0). On a caught
1376
+ * error `err` is passed so the 429-reset / 403-ban drivers are read STRUCTURALLY
1377
+ * from the error's upstream `headers` + `bodyText` (the `fetchWithRetry`
1378
+ * contract) — so daemon-path 429 cooldown + ban blocking function for
1379
+ * multi-account codex/gemini/opencodego pools; absent headers ⇒ a bare-429
1380
+ * (unmarked, lazy re-probe). Success (2xx) clears; 401/5xx/thrown → transient.
1381
+ */
1382
+ markHealth(accountId, status, err) {
1383
+ if (accountId === void 0 || status === 0) return;
1384
+ const headers = err !== void 0 ? errHeaders(err) : void 0;
1385
+ const reset = headers ? _SubscriptionAccountHealth.resolveResetSeconds.call(void 0, this.profile.providerId, headers) : { resetHeaderSeconds: null, retryAfterSeconds: null };
1386
+ const bodyText = status === 403 && err !== void 0 ? errBodyText(err) : void 0;
1387
+ _SubscriptionAccountHealth.getSharedAccountHealth.call(void 0, ).recordUpstreamOutcome(this.profile.providerId, accountId, {
1388
+ status,
1389
+ resetHeaderSeconds: reset.resetHeaderSeconds,
1390
+ retryAfterSeconds: reset.retryAfterSeconds,
1391
+ bodyText
1392
+ });
1393
+ }
1063
1394
  /**
1064
1395
  * Resolve the Code Assist project for the gemini subscription profile. Pulls
1065
1396
  * the Bearer the bound `AuthStrategy` would inject (so the strategy stays the
@@ -1136,8 +1467,24 @@ var SubscriptionDispatcher = class {
1136
1467
  }
1137
1468
  };
1138
1469
  var MAX_FALLBACK_ATTEMPTS_LOCAL = 3;
1470
+ function errStatus(err) {
1471
+ const status = _optionalChain([err, 'optionalAccess', _74 => _74.status]);
1472
+ return typeof status === "number" ? status : null;
1473
+ }
1474
+ function errHeaders(err) {
1475
+ const h = _optionalChain([err, 'optionalAccess', _75 => _75.headers]);
1476
+ if (!h) return void 0;
1477
+ if (typeof h.get === "function") return h;
1478
+ if (typeof h === "object") return h;
1479
+ return void 0;
1480
+ }
1481
+ function errBodyText(err) {
1482
+ const e = err;
1483
+ const raw = typeof _optionalChain([e, 'optionalAccess', _76 => _76.bodyText]) === "string" ? e.bodyText : typeof _optionalChain([e, 'optionalAccess', _77 => _77.body]) === "string" ? e.body : void 0;
1484
+ return _optionalChain([raw, 'optionalAccess', _78 => _78.slice, 'call', _79 => _79(0, 2048)]);
1485
+ }
1139
1486
  function caughtErrorBreakerOutcome(err) {
1140
- const status = _optionalChain([err, 'optionalAccess', _56 => _56.status]);
1487
+ const status = _optionalChain([err, 'optionalAccess', _80 => _80.status]);
1141
1488
  if (typeof status !== "number") return "failure";
1142
1489
  if (status === 0) return "neutral";
1143
1490
  if (status >= 500 || status === 429) return "failure";
@@ -1171,4 +1518,8 @@ function stripAuthHeaders(headers) {
1171
1518
 
1172
1519
 
1173
1520
 
1174
- exports.SubscriptionAccountService = SubscriptionAccountService; exports.SubscriptionDispatcher = SubscriptionDispatcher; exports.SubscriptionProviderRegistry = SubscriptionProviderRegistry; exports.claudeOAuth = _chunkTPW5Q25Ycjs.claude_exports; exports.codexOAuth = _chunkTPW5Q25Ycjs.codex_exports; exports.geminiOAuth = _chunkTPW5Q25Ycjs.gemini_exports; exports.getSubscriptionAccountService = getSubscriptionAccountService; exports.getSubscriptionProviderRegistry = getSubscriptionProviderRegistry; exports.setSubscriptionAccountService = setSubscriptionAccountService; exports.setSubscriptionProviderRegistry = setSubscriptionProviderRegistry;
1521
+
1522
+
1523
+
1524
+
1525
+ exports.DEFAULT_ACCOUNT_PRIORITY = DEFAULT_ACCOUNT_PRIORITY; exports.LAST_USED_PERSIST_THROTTLE_MS = LAST_USED_PERSIST_THROTTLE_MS; exports.SESSION_AFFINITY_TTL_MS = SESSION_AFFINITY_TTL_MS; exports.SubscriptionAccountSelector = SubscriptionAccountSelector; exports.SubscriptionAccountService = SubscriptionAccountService; exports.SubscriptionDispatcher = SubscriptionDispatcher; exports.SubscriptionProviderRegistry = SubscriptionProviderRegistry; exports.claudeOAuth = _chunkTPW5Q25Ycjs.claude_exports; exports.codexOAuth = _chunkTPW5Q25Ycjs.codex_exports; exports.geminiOAuth = _chunkTPW5Q25Ycjs.gemini_exports; exports.getSubscriptionAccountService = getSubscriptionAccountService; exports.getSubscriptionProviderRegistry = getSubscriptionProviderRegistry; exports.setSubscriptionAccountService = setSubscriptionAccountService; exports.setSubscriptionProviderRegistry = setSubscriptionProviderRegistry;