@omnicross/subscriptions 0.1.4 → 0.1.6

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.js CHANGED
@@ -108,6 +108,15 @@ var SubscriptionAccountSelector = class {
108
108
  // src/SubscriptionAccountService.ts
109
109
  import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
110
110
 
111
+ // src/scheduler/accountSelection.ts
112
+ import {
113
+ AccountAllowanceExhaustedError,
114
+ getSharedAccountAllowanceScheduling
115
+ } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
116
+ import {
117
+ BoundAccountSelectionError
118
+ } from "@omnicross/core/pipeline/BoundAccountSelectionError";
119
+
111
120
  // src/scheduler/accountModelMap.ts
112
121
  function canonicalModelId(value) {
113
122
  const idx = value.indexOf(",");
@@ -154,19 +163,103 @@ function gateSchedulable(accounts, providerId, health, now, resolvedModel, suppo
154
163
  return accounts.map((a) => {
155
164
  const healthOk = health ? health.isSchedulable(providerId, a.id, now) : true;
156
165
  const modelOk = resolvedModel ? accountSupportsModel(supportedModelsById.get(a.id), resolvedModel) : true;
157
- return { ...a, schedulable: healthOk && modelOk };
166
+ return { ...a, schedulable: a.schedulable !== false && healthOk && modelOk };
158
167
  });
159
168
  }
160
169
  function isPoolGated(accounts, health, resolvedModel) {
161
170
  return (health !== void 0 || resolvedModel !== void 0) && accounts.length >= 2;
162
171
  }
172
+ function hasUsableToken(token) {
173
+ return typeof token === "string" && token.trim() !== "";
174
+ }
175
+ async function resolveStrictPreferredToken(tokens, providerId, preferredId, activeGetter, ctx) {
176
+ let config;
177
+ try {
178
+ config = await tokens.getFullConfig();
179
+ } catch {
180
+ throw new BoundAccountSelectionError(providerId, "unavailable");
181
+ }
182
+ let accounts;
183
+ let activeAccountId;
184
+ let supportedModelsById;
185
+ try {
186
+ ({ accounts, activeAccountId, supportedModelsById } = readSchedulableAccounts(config, providerId));
187
+ } catch {
188
+ throw new BoundAccountSelectionError(providerId, "unavailable");
189
+ }
190
+ const preferred = accounts.find((account) => account.id === preferredId);
191
+ if (!preferred) {
192
+ throw new BoundAccountSelectionError(providerId, "not-found");
193
+ }
194
+ if (preferred.schedulable === false) {
195
+ throw new BoundAccountSelectionError(providerId, "disabled");
196
+ }
197
+ if (ctx.health && !ctx.health.isSchedulable(providerId, preferredId, ctx.now)) {
198
+ throw new BoundAccountSelectionError(providerId, "unhealthy");
199
+ }
200
+ if (ctx.resolvedModel && !accountSupportsModel(supportedModelsById.get(preferredId), ctx.resolvedModel)) {
201
+ throw new BoundAccountSelectionError(providerId, "model-incompatible");
202
+ }
203
+ const allowance = getSharedAccountAllowanceScheduling().evaluate(
204
+ providerId,
205
+ preferredId,
206
+ preferred.priority ?? DEFAULT_ACCOUNT_PRIORITY,
207
+ ctx.now
208
+ );
209
+ if (allowance.action === "pause") {
210
+ throw new BoundAccountSelectionError(providerId, "allowance-paused", allowance.resumeAt);
211
+ }
212
+ let token = null;
213
+ try {
214
+ token = tokens.getAccessTokenForAccount ? await tokens.getAccessTokenForAccount(providerId, preferredId) : preferredId === activeAccountId ? await activeGetter() : null;
215
+ } catch {
216
+ throw new BoundAccountSelectionError(providerId, "unavailable");
217
+ }
218
+ if (!hasUsableToken(token)) {
219
+ throw new BoundAccountSelectionError(providerId, "empty-token");
220
+ }
221
+ const remapped = remapReportForAccount(supportedModelsById.get(preferredId), ctx.resolvedModel);
222
+ ctx.reportSelection?.(preferredId, preferredId === activeAccountId, remapped);
223
+ return token;
224
+ }
225
+ function gateByAllowance(accounts, providerId, now) {
226
+ const scheduling = getSharedAccountAllowanceScheduling();
227
+ const evaluatedAt = now ?? Date.now();
228
+ const eligibleBeforePolicy = accounts.filter((account) => account.schedulable !== false);
229
+ if (eligibleBeforePolicy.length === 0) return accounts;
230
+ const decisions = /* @__PURE__ */ new Map();
231
+ const gated = accounts.map((account) => {
232
+ if (account.schedulable === false) return account;
233
+ const decision = scheduling.evaluate(
234
+ providerId,
235
+ account.id,
236
+ account.priority ?? DEFAULT_ACCOUNT_PRIORITY,
237
+ evaluatedAt
238
+ );
239
+ decisions.set(account.id, decision);
240
+ return {
241
+ ...account,
242
+ priority: decision.effectivePriority,
243
+ schedulable: decision.schedulable
244
+ };
245
+ });
246
+ if (gated.some((account) => account.schedulable !== false)) return gated;
247
+ const paused = eligibleBeforePolicy.map((account) => decisions.get(account.id)).filter((decision) => decision?.action === "pause");
248
+ if (paused.length !== eligibleBeforePolicy.length) return gated;
249
+ const resumeAt = paused.map((decision) => decision?.resumeAt).filter((value) => !!value).sort()[0];
250
+ throw new AccountAllowanceExhaustedError(providerId, resumeAt);
251
+ }
163
252
  function readSchedulableAccounts(config, providerId) {
164
253
  const raw = config[ACCOUNTS_KEY[providerId]] ?? [];
165
254
  const accounts = raw.map((a) => ({
166
255
  id: a.id,
256
+ group: typeof a.group === "string" && a.group.trim() !== "" ? a.group.trim() : providerId,
167
257
  priority: a.priority,
168
258
  lastUsedAt: a.lastUsedAt,
169
- createdAt: a.createdAt
259
+ createdAt: a.createdAt,
260
+ // Persisted opt-out is absolute, including a one-account pool. Legacy rows
261
+ // omit the field and therefore remain enabled.
262
+ schedulable: a.enabled !== false
170
263
  }));
171
264
  const supportedModelsById = new Map(
172
265
  raw.map((a) => [a.id, a.supportedModels])
@@ -188,13 +281,53 @@ async function resolveSelectedToken(selector, tokens, providerId, sessionKey, ac
188
281
  const report = ctx?.reportSelection;
189
282
  const now = ctx?.now;
190
283
  const resolvedModel = ctx?.resolvedModel;
284
+ const preferredId = typeof ctx?.preferredAccountId === "string" && ctx.preferredAccountId.trim() !== "" ? ctx.preferredAccountId.trim() : void 0;
285
+ const preferredGroup = typeof ctx?.preferredAccountGroup === "string" && ctx.preferredAccountGroup.trim() !== "" ? ctx.preferredAccountGroup.trim() : void 0;
286
+ if (preferredId && ctx && ctx.boundAccountFallbackPolicy !== "pool") {
287
+ return resolveStrictPreferredToken(tokens, providerId, preferredId, activeGetter, ctx);
288
+ }
191
289
  if (selector && tokens.getAccessTokenForAccount) {
192
290
  const config = await tokens.getFullConfig();
193
291
  const { accounts, activeAccountId, supportedModelsById } = readSchedulableAccounts(config, providerId);
194
- const gated = gateSchedulable(accounts, providerId, health, now, resolvedModel, supportedModelsById);
195
- const poolGated = isPoolGated(accounts, health, resolvedModel);
292
+ const groupAccounts = preferredGroup ? accounts.filter((account) => account.group === preferredGroup) : accounts;
293
+ if (preferredGroup && groupAccounts.length === 0 && ctx?.boundAccountFallbackPolicy !== "pool") {
294
+ throw new BoundAccountSelectionError(providerId, "not-found");
295
+ }
296
+ let candidates = groupAccounts.length > 0 ? groupAccounts : accounts;
297
+ let healthAndModelGated = gateSchedulable(
298
+ candidates,
299
+ providerId,
300
+ health,
301
+ now,
302
+ resolvedModel,
303
+ supportedModelsById
304
+ );
305
+ let gated = gateByAllowance(healthAndModelGated, providerId, now);
306
+ let groupHasUsableCredential = gated.some((account) => account.schedulable !== false);
307
+ if (groupHasUsableCredential && preferredGroup && ctx?.boundAccountFallbackPolicy === "pool" && candidates !== accounts) {
308
+ groupHasUsableCredential = false;
309
+ for (const account of gated) {
310
+ if (account.schedulable === false) continue;
311
+ if (await tokens.getAccessTokenForAccount(providerId, account.id)) {
312
+ groupHasUsableCredential = true;
313
+ break;
314
+ }
315
+ }
316
+ }
317
+ if (preferredGroup && ctx?.boundAccountFallbackPolicy === "pool" && candidates !== accounts && !groupHasUsableCredential) {
318
+ candidates = accounts;
319
+ healthAndModelGated = gateSchedulable(
320
+ candidates,
321
+ providerId,
322
+ health,
323
+ now,
324
+ resolvedModel,
325
+ supportedModelsById
326
+ );
327
+ gated = gateByAllowance(healthAndModelGated, providerId, now);
328
+ }
329
+ const poolGated = isPoolGated(candidates, health, resolvedModel) || candidates.some((account) => account.schedulable === false) || preferredGroup !== void 0;
196
330
  const remapFor = (id) => remapReportForAccount(supportedModelsById.get(id), resolvedModel);
197
- const preferredId = ctx?.preferredAccountId;
198
331
  if (preferredId) {
199
332
  const preferred = gated.find((a) => a.id === preferredId);
200
333
  if (preferred && preferred.schedulable !== false) {
@@ -227,7 +360,23 @@ async function resolveSelectedToken(selector, tokens, providerId, sessionKey, ac
227
360
  }
228
361
  }
229
362
  }
230
- if (activeAccountId) report?.(activeAccountId, true, remapFor(activeAccountId));
363
+ if (activeAccountId) {
364
+ if (preferredGroup && ctx?.boundAccountFallbackPolicy !== "pool" && !candidates.some((account) => account.id === activeAccountId)) {
365
+ throw new BoundAccountSelectionError(providerId, "unavailable");
366
+ }
367
+ const persistedActive = accounts.find((account) => account.id === activeAccountId);
368
+ if (persistedActive?.schedulable === false) return null;
369
+ const activeAllowance = getSharedAccountAllowanceScheduling().evaluate(
370
+ providerId,
371
+ activeAccountId,
372
+ persistedActive?.priority ?? DEFAULT_ACCOUNT_PRIORITY,
373
+ now
374
+ );
375
+ if (activeAllowance.action === "pause") {
376
+ throw new AccountAllowanceExhaustedError(providerId, activeAllowance.resumeAt);
377
+ }
378
+ report?.(activeAccountId, true, remapFor(activeAccountId));
379
+ }
231
380
  return activeGetter();
232
381
  }
233
382
  return activeGetter();
@@ -278,7 +427,14 @@ var OAuthBearerAuthStrategy = class {
278
427
  this.providerId,
279
428
  hints?.sessionKey,
280
429
  () => this.resolveAccessToken(),
281
- { health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel, preferredAccountId: hints?.preferredAccountId }
430
+ {
431
+ health: this.health,
432
+ reportSelection: hints?.reportSelection,
433
+ resolvedModel: hints?.resolvedModel,
434
+ preferredAccountId: hints?.preferredAccountId,
435
+ preferredAccountGroup: hints?.preferredAccountGroup,
436
+ boundAccountFallbackPolicy: hints?.boundAccountFallbackPolicy
437
+ }
282
438
  );
283
439
  if (!token) {
284
440
  return;
@@ -355,7 +511,14 @@ var PassThroughAuthStrategy = class {
355
511
  "claude",
356
512
  hints?.sessionKey,
357
513
  () => this.tokens.getValidClaudeAccessToken(),
358
- { health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel, preferredAccountId: hints?.preferredAccountId }
514
+ {
515
+ health: this.health,
516
+ reportSelection: hints?.reportSelection,
517
+ resolvedModel: hints?.resolvedModel,
518
+ preferredAccountId: hints?.preferredAccountId,
519
+ preferredAccountGroup: hints?.preferredAccountGroup,
520
+ boundAccountFallbackPolicy: hints?.boundAccountFallbackPolicy
521
+ }
359
522
  );
360
523
  if (!token) return;
361
524
  headers["Authorization"] = `Bearer ${token}`;
@@ -433,7 +596,14 @@ var StaticBearerAuthStrategy = class {
433
596
  "opencodego",
434
597
  hints?.sessionKey,
435
598
  () => this.tokens.getValidOpenCodeGoApiKey(),
436
- { health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel, preferredAccountId: hints?.preferredAccountId }
599
+ {
600
+ health: this.health,
601
+ reportSelection: hints?.reportSelection,
602
+ resolvedModel: hints?.resolvedModel,
603
+ preferredAccountId: hints?.preferredAccountId,
604
+ preferredAccountGroup: hints?.preferredAccountGroup,
605
+ boundAccountFallbackPolicy: hints?.boundAccountFallbackPolicy
606
+ }
437
607
  );
438
608
  if (!key) {
439
609
  return;
@@ -523,31 +693,29 @@ import { buildCodeAssistUrl } from "@omnicross/core/transformer/transformers/Gem
523
693
 
524
694
  // src/opencodego/CircuitBreaker.ts
525
695
  var CircuitBreaker = class {
526
- state = "closed";
527
- /** CONSECUTIVE failures while closed (reset by any closed success). */
528
- failureCount = 0;
529
- /** Successes accumulated in the current half-open probe window. */
530
- successCount = 0;
531
- /** Test calls admitted in the current half-open window (cap = halfOpenMaxCalls). */
532
- halfOpenCalls = 0;
533
- /** `now()` at the last recorded failure — drives the open→half-open elapsed check. */
534
- lastFailureTime = 0;
535
- threshold;
536
- openMs;
537
- halfOpenMaxCalls;
696
+ snapshot = {
697
+ mode: "closed",
698
+ consecutiveFailures: 0,
699
+ openedAt: 0,
700
+ probeAdmissions: 0,
701
+ probeSuccesses: 0
702
+ };
703
+ limits;
538
704
  now;
539
705
  constructor(opts = {}) {
540
- this.threshold = opts.threshold ?? 3;
541
- this.openMs = opts.openMs ?? 3e4;
542
- this.halfOpenMaxCalls = opts.halfOpenMaxCalls ?? 3;
706
+ this.limits = {
707
+ threshold: opts.threshold ?? 3,
708
+ openMs: opts.openMs ?? 3e4,
709
+ halfOpenMaxCalls: opts.halfOpenMaxCalls ?? 3
710
+ };
543
711
  this.now = opts.now ?? Date.now;
544
712
  }
545
713
  /** Current state (diagnostics / tests). */
546
714
  getState() {
547
- return this.state;
715
+ return this.snapshot.mode;
548
716
  }
549
717
  /**
550
- * Admission gate (`fallback.go:54-72` `AllowRequest`). Returns whether a
718
+ * Admission gate. Returns whether a
551
719
  * request to this model is allowed RIGHT NOW. Side-effecting BY DESIGN:
552
720
  * - `closed` → always admit.
553
721
  * - `open` → if `now() - lastFailureTime > openMs`, FLIP to `half-open`,
@@ -558,65 +726,82 @@ var CircuitBreaker = class {
558
726
  * recorded outcome resolves the state).
559
727
  */
560
728
  allowRequest() {
561
- switch (this.state) {
562
- case "closed":
563
- return true;
564
- case "open":
565
- if (this.now() - this.lastFailureTime > this.openMs) {
566
- this.state = "half-open";
567
- this.successCount = 0;
568
- this.halfOpenCalls = 1;
569
- return true;
570
- }
571
- return false;
572
- case "half-open":
573
- if (this.halfOpenCalls < this.halfOpenMaxCalls) {
574
- this.halfOpenCalls += 1;
575
- return true;
576
- }
577
- return false;
578
- default:
579
- return true;
729
+ if (this.snapshot.mode === "closed") return true;
730
+ if (this.snapshot.mode === "open") {
731
+ const elapsed = this.now() - this.snapshot.openedAt;
732
+ if (elapsed <= this.limits.openMs) return false;
733
+ this.snapshot = {
734
+ ...this.snapshot,
735
+ mode: "half-open",
736
+ probeAdmissions: 1,
737
+ probeSuccesses: 0
738
+ };
739
+ return true;
580
740
  }
741
+ if (this.snapshot.probeAdmissions >= this.limits.halfOpenMaxCalls) return false;
742
+ this.snapshot = {
743
+ ...this.snapshot,
744
+ probeAdmissions: this.snapshot.probeAdmissions + 1
745
+ };
746
+ return true;
581
747
  }
582
748
  /**
583
- * Record a successful attempt (`fallback.go:75-91` `RecordSuccess`).
749
+ * Record a successful attempt.
584
750
  * - `half-open` → increment `successCount`; at `halfOpenMaxCalls` successes,
585
751
  * CLOSE the circuit and reset all counters.
586
752
  * - `closed` → reset the consecutive `failureCount` (a single good call
587
753
  * clears the streak).
588
754
  */
589
755
  recordSuccess() {
590
- if (this.state === "half-open") {
591
- this.successCount += 1;
592
- if (this.successCount >= this.halfOpenMaxCalls) {
593
- this.state = "closed";
594
- this.failureCount = 0;
595
- this.successCount = 0;
596
- this.halfOpenCalls = 0;
756
+ if (this.snapshot.mode === "open") return;
757
+ if (this.snapshot.mode === "closed") {
758
+ if (this.snapshot.consecutiveFailures !== 0) {
759
+ this.snapshot = { ...this.snapshot, consecutiveFailures: 0 };
597
760
  }
598
761
  return;
599
762
  }
600
- this.failureCount = 0;
763
+ const probeSuccesses = this.snapshot.probeSuccesses + 1;
764
+ if (probeSuccesses >= this.limits.halfOpenMaxCalls) {
765
+ this.snapshot = {
766
+ mode: "closed",
767
+ consecutiveFailures: 0,
768
+ openedAt: 0,
769
+ probeAdmissions: 0,
770
+ probeSuccesses: 0
771
+ };
772
+ return;
773
+ }
774
+ this.snapshot = { ...this.snapshot, probeSuccesses };
601
775
  }
602
776
  /**
603
- * Record a failed attempt (`fallback.go:94-115` `RecordFailure`).
777
+ * Record a failed attempt.
604
778
  * - `half-open` → immediately RE-OPEN (one probe failure is enough); stamp
605
779
  * `lastFailureTime`, reset `successCount`.
606
780
  * - `closed` → increment the consecutive `failureCount`; at `threshold`,
607
781
  * OPEN the circuit. Always stamp `lastFailureTime`.
608
782
  */
609
783
  recordFailure() {
610
- this.lastFailureTime = this.now();
611
- if (this.state === "half-open") {
612
- this.state = "open";
613
- this.successCount = 0;
614
- this.halfOpenCalls = 0;
784
+ const openedAt = this.now();
785
+ if (this.snapshot.mode === "half-open") {
786
+ this.snapshot = {
787
+ ...this.snapshot,
788
+ mode: "open",
789
+ openedAt,
790
+ probeAdmissions: 0,
791
+ probeSuccesses: 0
792
+ };
615
793
  return;
616
794
  }
617
- this.failureCount += 1;
618
- if (this.failureCount >= this.threshold) {
619
- this.state = "open";
795
+ const consecutiveFailures = this.snapshot.consecutiveFailures + 1;
796
+ this.snapshot = {
797
+ ...this.snapshot,
798
+ consecutiveFailures,
799
+ openedAt,
800
+ mode: consecutiveFailures >= this.limits.threshold ? "open" : this.snapshot.mode
801
+ };
802
+ if (this.snapshot.mode === "open") {
803
+ this.snapshot.probeAdmissions = 0;
804
+ this.snapshot.probeSuccesses = 0;
620
805
  }
621
806
  }
622
807
  };
@@ -665,8 +850,7 @@ var DEFAULT_OPENCODEGO_MODEL_MAP = {
665
850
  modelId: "glm-5"
666
851
  },
667
852
  complex: {
668
- // Reference maps `complex` `glm-5.1` (config.example.json:55-60); was
669
- // drifted to `mimo-v2-pro` (audit D4).
853
+ // Complex tasks use the higher-capability GLM variant by default.
670
854
  modelId: "glm-5.1"
671
855
  },
672
856
  fast: {
@@ -805,67 +989,44 @@ function resolveOpenCodeGoHalf(modelId, config) {
805
989
 
806
990
  // src/opencodego/ScenarioRouter.ts
807
991
  var COMPLEX_KEYWORDS = [
808
- // Architectural
809
- "architect",
810
992
  "architecture",
811
993
  "refactor",
812
994
  "redesign",
813
- "complex",
814
- "difficult",
815
- "challenging",
816
995
  "optimize",
817
996
  "performance",
818
- "efficiency",
819
- "design pattern",
820
- "best practice",
821
- // Tool-related keywords indicate complex operations
822
- "execute",
823
- "run command",
824
- "bash",
825
- "shell",
826
997
  "implement",
827
998
  "build",
828
- "create",
829
- "add feature",
830
- "write to",
831
999
  "edit file",
832
- "create file"
1000
+ "debug",
1001
+ "migrate",
1002
+ "benchmark"
833
1003
  ];
834
1004
  var THINKING_KEYWORDS = [
835
1005
  "think",
836
- "thinking",
837
1006
  "plan",
838
1007
  "reason",
839
- "reasoning",
840
1008
  "analyze",
841
- "analysis",
842
- "step by step"
1009
+ "step by step",
1010
+ "evaluate",
1011
+ "compare tradeoffs"
843
1012
  ];
844
1013
  var ANT_THINKING_MARKER = "antThinking";
845
1014
  var TOOL_BLOCKERS = [
846
1015
  "tool",
847
1016
  "function",
848
- "execute",
849
- "run command",
1017
+ "command",
850
1018
  "write",
851
1019
  "edit",
852
- "create",
853
1020
  "delete",
854
- "remove",
855
1021
  "implement",
856
1022
  "build",
857
- "add",
858
1023
  "modify"
859
1024
  ];
860
1025
  var BACKGROUND_KEYWORDS = [
861
1026
  "list directory",
862
- "ls -",
863
- "dir",
864
1027
  "show file",
865
- "view file",
866
- "cat file",
1028
+ "read file",
867
1029
  "what is",
868
- "what's",
869
1030
  "tell me about",
870
1031
  "check status",
871
1032
  "show status"
@@ -921,7 +1082,7 @@ function opencodegoTransformerNamesForShape(shape) {
921
1082
  return ["gemini"];
922
1083
  case "chat":
923
1084
  default:
924
- return ["opencodego"];
1085
+ return ["openai"];
925
1086
  }
926
1087
  }
927
1088
  function resolveOpenCodeGoTarget(modelId, config) {
@@ -976,7 +1137,7 @@ var SubscriptionProviderRegistry = class {
976
1137
  authStrategy: codex,
977
1138
  mode: "transformer",
978
1139
  // ChatGPT internal endpoint — accepts the OpenAI Responses API
979
- // format. Mirrors `_others/claude-relay-service/src/routes/openaiRoutes.js:454`.
1140
+ // format and uses the Codex OAuth access token below.
980
1141
  // The Codex OAuth access token grants access here; the public
981
1142
  // `api.openai.com/v1/responses` endpoint would reject the same token.
982
1143
  resolveUpstreamUrl: () => "https://chatgpt.com/backend-api/codex/responses",
@@ -1022,8 +1183,8 @@ var SubscriptionProviderRegistry = class {
1022
1183
  // `ocConfig`; the core `/v1/messages` plan builder passes the opaque
1023
1184
  // `route.subscriptionConfig`). With NO zen config every resolved model
1024
1185
  // is go-half → byte-identical to the prior resolver.
1025
- // `// UNVERIFIED (no live zen key)`: the zen endpoint hosts/paths are
1026
- // ported from the reference + proven in-process only.
1186
+ // `// UNVERIFIED (no live zen key)`: the ZEN endpoint hosts and paths
1187
+ // are covered by in-process tests only.
1027
1188
  resolveUpstreamUrl: (model, config) => {
1028
1189
  const oc = config;
1029
1190
  const { half, shape } = resolveOpenCodeGoTarget(model, oc);
@@ -1031,7 +1192,7 @@ var SubscriptionProviderRegistry = class {
1031
1192
  return buildOpenCodeGoUrl(half, shape, override);
1032
1193
  },
1033
1194
  // zen seam (Decision 3): vary the provider transformer chain by resolved
1034
- // shape (anthropic⇒[] verbatim, chat⇒opencodego, responses⇒openai-response,
1195
+ // shape (anthropic⇒[] verbatim, chat⇒openai, responses⇒openai-response,
1035
1196
  // gemini⇒gemini). OPTIONAL on the profile type — only opencodego sets it;
1036
1197
  // claude/codex/gemini omit it and fall back to `providerTransformerNames`,
1037
1198
  // keeping their routing byte-identical. The static `providerTransformerNames`
@@ -1041,7 +1202,7 @@ var SubscriptionProviderRegistry = class {
1041
1202
  const { shape } = resolveOpenCodeGoTarget(model, config);
1042
1203
  return opencodegoTransformerNamesForShape(shape);
1043
1204
  },
1044
- providerTransformerNames: ["opencodego"],
1205
+ providerTransformerNames: ["openai"],
1045
1206
  modelTransformerNames: [],
1046
1207
  modelMapper: (sdkModel, summary, config) => {
1047
1208
  const scenario = resolveOpenCodeGoScenario(summary, config);
@@ -1055,9 +1216,8 @@ var SubscriptionProviderRegistry = class {
1055
1216
  // circuit is open. `breaker.allowRequest(modelId)` is the admission
1056
1217
  // gate — calling it has the side effect of flipping an `open` model to
1057
1218
  // `half-open` once its 30s window elapses AND counting a half-open admit
1058
- // slot. It MUST therefore be consulted EXACTLY ONCE per returned model,
1059
- // on the candidate about to be attempted — mirroring the reference
1060
- // (`fallback.go` calls `AllowRequest` once, on the model it returns).
1219
+ // slot. It MUST therefore be consulted exactly once per returned model,
1220
+ // on the candidate about to be attempted.
1061
1221
  // An early-returning scan (NOT `Array.filter`, which would `allowRequest`
1062
1222
  // every candidate and burn the admit slots of half-open models AFTER the
1063
1223
  // chosen one — those are never attempted, never recorded, so they would
@@ -1091,9 +1251,9 @@ var SubscriptionProviderRegistry = class {
1091
1251
  * process singleton, built here and captured by the opencodego profile's
1092
1252
  * `nextFallback` (consult) + `recordModelOutcome` (record) closures. Because
1093
1253
  * the `SubscriptionProviderRegistry` is itself a process singleton (via
1094
- * `setSubscriptionProviderRegistry`), breaker state persists across requests
1095
- * exactly the reference's long-lived `FallbackHandler`. Constructed with the
1096
- * default reference thresholds (3 / 30s / 3) and the default `Date.now` clock.
1254
+ * `setSubscriptionProviderRegistry`), breaker state persists across requests.
1255
+ * Constructed with the production thresholds (3 / 30s / 3) and the default
1256
+ * `Date.now` clock.
1097
1257
  */
1098
1258
  breaker = new CircuitBreakerRegistry();
1099
1259
  /** Returns the dispatch profile for a known subscription provider, or
@@ -1121,6 +1281,8 @@ function getSubscriptionProviderRegistry() {
1121
1281
  }
1122
1282
 
1123
1283
  // src/SubscriptionDispatcher.ts
1284
+ import { isAccountAllowanceExhaustedError } from "@omnicross/core/pipeline/AccountAllowanceScheduling";
1285
+ import { isBoundAccountSelectionError } from "@omnicross/core/pipeline/BoundAccountSelectionError";
1124
1286
  import { getGeminiCodeAssistResolver } from "@omnicross/core/ports/gemini-code-assist-resolver";
1125
1287
  import {
1126
1288
  getSharedAccountHealth as getSharedAccountHealth2,
@@ -1378,6 +1540,7 @@ var SubscriptionDispatcher = class {
1378
1540
  try {
1379
1541
  await this.profile.authStrategy.applyHeaders(headers, hints);
1380
1542
  } catch (err) {
1543
+ if (isAccountAllowanceExhaustedError(err) || isBoundAccountSelectionError(err)) throw err;
1381
1544
  console.warn("[AgentProxy:subscription] authStrategy.applyHeaders threw:", serializeError(err));
1382
1545
  }
1383
1546
  }
package/dist/oauth.d.cts CHANGED
@@ -10,7 +10,7 @@ import { TokenExchangeRequest, OAuthParams } from '@omnicross/contracts/account-
10
10
  * through an INJECTED `FetchLike` port — NO `electron`, NO `net`, NO host path
11
11
  * (a desktop host can inject an electron-net adapter; the daemon injects global
12
12
  * `fetch`). Claude `code=true`, `state` carried in exchange, setup-token has no
13
- * refresh_token. Reference: claude-relay-service `src/utils/oauthHelper.js`.
13
+ * refresh_token.
14
14
  *
15
15
  * @module @omnicross/subscriptions/oauth/flows/claude
16
16
  */
@@ -60,8 +60,6 @@ declare namespace claude {
60
60
  * the exchange body, refresh carries `scope=openid profile email`, and the refresh
61
61
  * defaults `expiresIn` to 3600 + returns an `idToken`. Network goes through the
62
62
  * injected `FetchLike`.
63
- * Reference: claude-relay-service `src/services/openaiAccountService.js`.
64
- *
65
63
  * @module @omnicross/subscriptions/oauth/flows/codex
66
64
  */
67
65
 
@@ -97,8 +95,6 @@ declare namespace codex {
97
95
  * `refreshGeminiToken`). Network goes through the injected `FetchLike`.
98
96
  * NOTE: `exchangeCodeForTokens` keeps a POSITIONAL signature
99
97
  * `(authorizationCode, codeVerifier)`.
100
- * Reference: claude-relay-service `src/services/geminiAccountService.js`.
101
- *
102
98
  * @module @omnicross/subscriptions/oauth/flows/gemini
103
99
  */
104
100
 
package/dist/oauth.d.ts CHANGED
@@ -10,7 +10,7 @@ import { TokenExchangeRequest, OAuthParams } from '@omnicross/contracts/account-
10
10
  * through an INJECTED `FetchLike` port — NO `electron`, NO `net`, NO host path
11
11
  * (a desktop host can inject an electron-net adapter; the daemon injects global
12
12
  * `fetch`). Claude `code=true`, `state` carried in exchange, setup-token has no
13
- * refresh_token. Reference: claude-relay-service `src/utils/oauthHelper.js`.
13
+ * refresh_token.
14
14
  *
15
15
  * @module @omnicross/subscriptions/oauth/flows/claude
16
16
  */
@@ -60,8 +60,6 @@ declare namespace claude {
60
60
  * the exchange body, refresh carries `scope=openid profile email`, and the refresh
61
61
  * defaults `expiresIn` to 3600 + returns an `idToken`. Network goes through the
62
62
  * injected `FetchLike`.
63
- * Reference: claude-relay-service `src/services/openaiAccountService.js`.
64
- *
65
63
  * @module @omnicross/subscriptions/oauth/flows/codex
66
64
  */
67
65
 
@@ -97,8 +95,6 @@ declare namespace codex {
97
95
  * `refreshGeminiToken`). Network goes through the injected `FetchLike`.
98
96
  * NOTE: `exchangeCodeForTokens` keeps a POSITIONAL signature
99
97
  * `(authorizationCode, codeVerifier)`.
100
- * Reference: claude-relay-service `src/services/geminiAccountService.js`.
101
- *
102
98
  * @module @omnicross/subscriptions/oauth/flows/gemini
103
99
  */
104
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnicross/subscriptions",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Omnicross subscription-as-provider auth strategies, OAuth flows, and the OpenCodeGo scenario dispatcher.",
5
5
  "license": "MIT",
6
6
  "author": "Sayo (https://github.com/Dumoedss)",