@omnicross/subscriptions 0.1.0

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 ADDED
@@ -0,0 +1,1679 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ SubscriptionAccountService: () => SubscriptionAccountService,
34
+ SubscriptionDispatcher: () => SubscriptionDispatcher,
35
+ SubscriptionProviderRegistry: () => SubscriptionProviderRegistry,
36
+ claudeOAuth: () => claude_exports,
37
+ codexOAuth: () => codex_exports,
38
+ geminiOAuth: () => gemini_exports,
39
+ getSubscriptionAccountService: () => getSubscriptionAccountService,
40
+ getSubscriptionProviderRegistry: () => getSubscriptionProviderRegistry,
41
+ setSubscriptionAccountService: () => setSubscriptionAccountService,
42
+ setSubscriptionProviderRegistry: () => setSubscriptionProviderRegistry
43
+ });
44
+ module.exports = __toCommonJS(src_exports);
45
+
46
+ // src/auth/OAuthBearerAuthStrategy.ts
47
+ var REFRESH_LEAD_MS = 5 * 6e4;
48
+ var OAuthBearerAuthStrategy = class {
49
+ constructor(providerId, tokens, mutex) {
50
+ this.tokens = tokens;
51
+ this.mutex = mutex;
52
+ this.providerId = providerId;
53
+ }
54
+ tokens;
55
+ mutex;
56
+ kind = "oauth-bearer";
57
+ providerId;
58
+ async applyHeaders(headers, _hints) {
59
+ const token = await this.resolveAccessToken();
60
+ if (!token) {
61
+ return;
62
+ }
63
+ headers["Authorization"] = `Bearer ${token}`;
64
+ }
65
+ async onUnauthorized() {
66
+ return this.mutex.run(`${this.providerId}:refresh`, async () => {
67
+ try {
68
+ return this.providerId === "codex" ? await this.tokens.refreshCodexToken() : await this.tokens.refreshGeminiToken();
69
+ } catch (err) {
70
+ console.warn(`[OAuthBearerAuthStrategy] ${this.providerId} refresh failed:`, err);
71
+ return false;
72
+ }
73
+ });
74
+ }
75
+ async describeStatus() {
76
+ const config = await this.tokens.getFullConfig();
77
+ const entry = this.providerId === "codex" ? config.codex : config.gemini;
78
+ if (!entry?.accessToken) {
79
+ return { providerId: this.providerId, ok: false, reason: "missing-credential" };
80
+ }
81
+ if (entry.status === "expired") {
82
+ return {
83
+ providerId: this.providerId,
84
+ ok: false,
85
+ reason: entry.refreshToken ? "expired" : "reauth-required",
86
+ expiresAt: entry.expiresAt
87
+ };
88
+ }
89
+ return { providerId: this.providerId, ok: true, expiresAt: entry.expiresAt };
90
+ }
91
+ /** Read the current token, refreshing in-line if it's within the lead window. */
92
+ async resolveAccessToken() {
93
+ const config = await this.tokens.getFullConfig();
94
+ const entry = this.providerId === "codex" ? config.codex : config.gemini;
95
+ if (!entry?.accessToken) return null;
96
+ const expiresAtMs = entry.expiresAt ? new Date(entry.expiresAt).getTime() : 0;
97
+ const expiringSoon = expiresAtMs > 0 && Date.now() >= expiresAtMs - REFRESH_LEAD_MS;
98
+ if (expiringSoon && entry.refreshToken) {
99
+ const refreshed = await this.mutex.run(`${this.providerId}:refresh`, async () => {
100
+ return this.providerId === "codex" ? await this.tokens.refreshCodexToken() : await this.tokens.refreshGeminiToken();
101
+ });
102
+ if (!refreshed) return null;
103
+ const fresh = await this.tokens.getFullConfig();
104
+ const freshEntry = this.providerId === "codex" ? fresh.codex : fresh.gemini;
105
+ return freshEntry?.accessToken ?? null;
106
+ }
107
+ if (entry.status === "expired") return null;
108
+ return entry.accessToken;
109
+ }
110
+ };
111
+
112
+ // src/auth/PassThroughAuthStrategy.ts
113
+ var PassThroughAuthStrategy = class {
114
+ constructor(tokens, mutex) {
115
+ this.tokens = tokens;
116
+ this.mutex = mutex;
117
+ }
118
+ tokens;
119
+ mutex;
120
+ kind = "pass-through";
121
+ providerId = "claude";
122
+ async applyHeaders(headers, _hints) {
123
+ const token = await this.tokens.getValidClaudeAccessToken();
124
+ if (!token) return;
125
+ headers["Authorization"] = `Bearer ${token}`;
126
+ }
127
+ async onUnauthorized() {
128
+ return this.mutex.run("claude:refresh", async () => {
129
+ try {
130
+ return await this.tokens.refreshClaudeToken();
131
+ } catch (err) {
132
+ console.warn("[PassThroughAuthStrategy] Claude refresh failed:", err);
133
+ return false;
134
+ }
135
+ });
136
+ }
137
+ async describeStatus() {
138
+ const config = await this.tokens.getFullConfig();
139
+ const claude = config.claude;
140
+ if (!claude?.accessToken) {
141
+ return { providerId: "claude", ok: false, reason: "missing-credential" };
142
+ }
143
+ if (claude.status === "expired") {
144
+ return {
145
+ providerId: "claude",
146
+ ok: false,
147
+ reason: claude.refreshToken ? "expired" : "reauth-required",
148
+ expiresAt: claude.expiresAt
149
+ };
150
+ }
151
+ return { providerId: "claude", ok: true, expiresAt: claude.expiresAt };
152
+ }
153
+ };
154
+
155
+ // src/auth/RefreshMutex.ts
156
+ var RefreshMutex = class {
157
+ inflight = /* @__PURE__ */ new Map();
158
+ /**
159
+ * Run `task()` exclusively for `key`. If another caller is already running
160
+ * for the same key, this call awaits the existing promise instead of
161
+ * starting a new one — both get the SAME result.
162
+ */
163
+ async run(key, task) {
164
+ const existing = this.inflight.get(key);
165
+ if (existing) return existing;
166
+ const promise = (async () => {
167
+ try {
168
+ return await task();
169
+ } finally {
170
+ this.inflight.delete(key);
171
+ }
172
+ })();
173
+ this.inflight.set(key, promise);
174
+ return promise;
175
+ }
176
+ };
177
+
178
+ // src/auth/StaticBearerAuthStrategy.ts
179
+ var ANTHROPIC_SHAPE_PATH = "/v1/messages";
180
+ var StaticBearerAuthStrategy = class {
181
+ constructor(tokens) {
182
+ this.tokens = tokens;
183
+ }
184
+ tokens;
185
+ kind = "static-bearer";
186
+ providerId = "opencodego";
187
+ async applyHeaders(headers, hints) {
188
+ const key = await this.tokens.getValidOpenCodeGoApiKey();
189
+ if (!key) {
190
+ return;
191
+ }
192
+ headers["Authorization"] = `Bearer ${key}`;
193
+ if (hints?.upstreamUrl?.includes(ANTHROPIC_SHAPE_PATH)) {
194
+ headers["x-api-key"] = key;
195
+ }
196
+ }
197
+ async onUnauthorized() {
198
+ return false;
199
+ }
200
+ async describeStatus() {
201
+ const config = await this.tokens.getFullConfig();
202
+ const oc = config.opencodego;
203
+ if (!oc?.apiKey) {
204
+ return { providerId: "opencodego", ok: false, reason: "missing-credential" };
205
+ }
206
+ if (oc.status === "error") {
207
+ return { providerId: "opencodego", ok: false, reason: "unknown" };
208
+ }
209
+ return { providerId: "opencodego", ok: true };
210
+ }
211
+ };
212
+
213
+ // src/SubscriptionAccountService.ts
214
+ var DISPLAY_NAMES = {
215
+ claude: "Claude (Anthropic OAuth)",
216
+ codex: "Codex (ChatGPT OAuth)",
217
+ gemini: "Gemini (Google OAuth)",
218
+ opencodego: "OpenCodeGo (Bearer key)"
219
+ };
220
+ var SubscriptionAccountService = class {
221
+ mutex = new RefreshMutex();
222
+ strategies;
223
+ constructor(tokens) {
224
+ this.strategies = /* @__PURE__ */ new Map([
225
+ ["claude", new PassThroughAuthStrategy(tokens, this.mutex)],
226
+ ["codex", new OAuthBearerAuthStrategy("codex", tokens, this.mutex)],
227
+ ["gemini", new OAuthBearerAuthStrategy("gemini", tokens, this.mutex)],
228
+ ["opencodego", new StaticBearerAuthStrategy(tokens)]
229
+ ]);
230
+ }
231
+ /** Returns the strategy bound to a subscription provider, or `null` for unknown ids. */
232
+ getStrategy(providerId) {
233
+ return this.strategies.get(providerId) ?? null;
234
+ }
235
+ /** Diagnostic for the `subscription:status` IPC. */
236
+ async getStatus(providerId) {
237
+ const strategy = this.strategies.get(providerId);
238
+ if (!strategy) {
239
+ return { providerId, ok: false, reason: "unknown" };
240
+ }
241
+ return strategy.describeStatus();
242
+ }
243
+ /** Catalog entry list for the `subscription:list` IPC. */
244
+ async listAll() {
245
+ const entries = [];
246
+ for (const [providerId, strategy] of this.strategies.entries()) {
247
+ entries.push({
248
+ providerId,
249
+ displayName: DISPLAY_NAMES[providerId],
250
+ kind: strategy.kind,
251
+ credentialStatus: await strategy.describeStatus()
252
+ });
253
+ }
254
+ return entries;
255
+ }
256
+ };
257
+ var _moduleSingleton = null;
258
+ function setSubscriptionAccountService(svc) {
259
+ _moduleSingleton = svc;
260
+ }
261
+ function getSubscriptionAccountService() {
262
+ return _moduleSingleton;
263
+ }
264
+
265
+ // ../core/src/outbound-api/subscriptionRegistryPort.ts
266
+ var _registry = null;
267
+ function setSubscriptionRegistryForOutbound(registry) {
268
+ _registry = registry;
269
+ }
270
+
271
+ // ../core/src/transformer/transformers/GeminiCodeAssistTransformer.ts
272
+ var DEFAULT_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
273
+ var DEFAULT_CODE_ASSIST_API_VERSION = "v1internal";
274
+ function resolveCodeAssistEndpoint() {
275
+ return (process.env.CODE_ASSIST_ENDPOINT || DEFAULT_CODE_ASSIST_ENDPOINT).replace(/\/+$/, "");
276
+ }
277
+ function resolveCodeAssistApiVersion() {
278
+ return process.env.CODE_ASSIST_API_VERSION || DEFAULT_CODE_ASSIST_API_VERSION;
279
+ }
280
+ function buildCodeAssistUrl(stream) {
281
+ const base = resolveCodeAssistEndpoint();
282
+ const version = resolveCodeAssistApiVersion();
283
+ const method = stream ? "streamGenerateContent?alt=sse" : "generateContent";
284
+ return `${base}/${version}:${method}`;
285
+ }
286
+
287
+ // src/opencodego/CircuitBreaker.ts
288
+ var CircuitBreaker = class {
289
+ state = "closed";
290
+ /** CONSECUTIVE failures while closed (reset by any closed success). */
291
+ failureCount = 0;
292
+ /** Successes accumulated in the current half-open probe window. */
293
+ successCount = 0;
294
+ /** Test calls admitted in the current half-open window (cap = halfOpenMaxCalls). */
295
+ halfOpenCalls = 0;
296
+ /** `now()` at the last recorded failure — drives the open→half-open elapsed check. */
297
+ lastFailureTime = 0;
298
+ threshold;
299
+ openMs;
300
+ halfOpenMaxCalls;
301
+ now;
302
+ constructor(opts = {}) {
303
+ this.threshold = opts.threshold ?? 3;
304
+ this.openMs = opts.openMs ?? 3e4;
305
+ this.halfOpenMaxCalls = opts.halfOpenMaxCalls ?? 3;
306
+ this.now = opts.now ?? Date.now;
307
+ }
308
+ /** Current state (diagnostics / tests). */
309
+ getState() {
310
+ return this.state;
311
+ }
312
+ /**
313
+ * Admission gate (`fallback.go:54-72` `AllowRequest`). Returns whether a
314
+ * request to this model is allowed RIGHT NOW. Side-effecting BY DESIGN:
315
+ * - `closed` → always admit.
316
+ * - `open` → if `now() - lastFailureTime > openMs`, FLIP to `half-open`,
317
+ * reset the probe counters, admit the FIRST test call;
318
+ * otherwise reject.
319
+ * - `half-open` → admit while `halfOpenCalls < halfOpenMaxCalls`, counting
320
+ * each admission; reject once the cap is reached (until a
321
+ * recorded outcome resolves the state).
322
+ */
323
+ allowRequest() {
324
+ switch (this.state) {
325
+ case "closed":
326
+ return true;
327
+ case "open":
328
+ if (this.now() - this.lastFailureTime > this.openMs) {
329
+ this.state = "half-open";
330
+ this.successCount = 0;
331
+ this.halfOpenCalls = 1;
332
+ return true;
333
+ }
334
+ return false;
335
+ case "half-open":
336
+ if (this.halfOpenCalls < this.halfOpenMaxCalls) {
337
+ this.halfOpenCalls += 1;
338
+ return true;
339
+ }
340
+ return false;
341
+ default:
342
+ return true;
343
+ }
344
+ }
345
+ /**
346
+ * Record a successful attempt (`fallback.go:75-91` `RecordSuccess`).
347
+ * - `half-open` → increment `successCount`; at `halfOpenMaxCalls` successes,
348
+ * CLOSE the circuit and reset all counters.
349
+ * - `closed` → reset the consecutive `failureCount` (a single good call
350
+ * clears the streak).
351
+ */
352
+ recordSuccess() {
353
+ if (this.state === "half-open") {
354
+ this.successCount += 1;
355
+ if (this.successCount >= this.halfOpenMaxCalls) {
356
+ this.state = "closed";
357
+ this.failureCount = 0;
358
+ this.successCount = 0;
359
+ this.halfOpenCalls = 0;
360
+ }
361
+ return;
362
+ }
363
+ this.failureCount = 0;
364
+ }
365
+ /**
366
+ * Record a failed attempt (`fallback.go:94-115` `RecordFailure`).
367
+ * - `half-open` → immediately RE-OPEN (one probe failure is enough); stamp
368
+ * `lastFailureTime`, reset `successCount`.
369
+ * - `closed` → increment the consecutive `failureCount`; at `threshold`,
370
+ * OPEN the circuit. Always stamp `lastFailureTime`.
371
+ */
372
+ recordFailure() {
373
+ this.lastFailureTime = this.now();
374
+ if (this.state === "half-open") {
375
+ this.state = "open";
376
+ this.successCount = 0;
377
+ this.halfOpenCalls = 0;
378
+ return;
379
+ }
380
+ this.failureCount += 1;
381
+ if (this.failureCount >= this.threshold) {
382
+ this.state = "open";
383
+ }
384
+ }
385
+ };
386
+ var CircuitBreakerRegistry = class {
387
+ constructor(options = {}) {
388
+ this.options = options;
389
+ }
390
+ options;
391
+ breakers = /* @__PURE__ */ new Map();
392
+ /** Get (or lazily create) the breaker for a model id. */
393
+ get(modelId) {
394
+ let breaker = this.breakers.get(modelId);
395
+ if (!breaker) {
396
+ breaker = new CircuitBreaker(this.options);
397
+ this.breakers.set(modelId, breaker);
398
+ }
399
+ return breaker;
400
+ }
401
+ /** Admission gate for a model (creates a fresh closed breaker on first sight). */
402
+ allowRequest(modelId) {
403
+ return this.get(modelId).allowRequest();
404
+ }
405
+ /** Record a successful attempt for a model. */
406
+ recordSuccess(modelId) {
407
+ this.get(modelId).recordSuccess();
408
+ }
409
+ /** Record a failed attempt for a model. */
410
+ recordFailure(modelId) {
411
+ this.get(modelId).recordFailure();
412
+ }
413
+ };
414
+
415
+ // src/opencodego/defaults.ts
416
+ var DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD = 8e4;
417
+ var DEFAULT_OPENCODEGO_MODEL_MAP = {
418
+ default: {
419
+ modelId: "kimi-k2.6",
420
+ temperature: 0.7,
421
+ maxTokens: 4096
422
+ },
423
+ long_context: {
424
+ modelId: "minimax-m2.5",
425
+ contextThreshold: DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD
426
+ },
427
+ think: {
428
+ modelId: "glm-5"
429
+ },
430
+ complex: {
431
+ // Reference maps `complex` → `glm-5.1` (config.example.json:55-60); was
432
+ // drifted to `mimo-v2-pro` (audit D4).
433
+ modelId: "glm-5.1"
434
+ },
435
+ fast: {
436
+ modelId: "qwen3.6-plus"
437
+ },
438
+ // AUTO-SELECTABLE (D3): the scenario router auto-selects `background` via the
439
+ // keyword heuristics in `ScenarioRouter.resolveOpenCodeGoScenario` (NO
440
+ // tool-blocker keyword AND ≥1 background keyword). It also remains reachable via
441
+ // an explicit user `modelMap.background` key. Mirrors config.example.json:9-15.
442
+ background: {
443
+ modelId: "qwen3.5-plus",
444
+ temperature: 0.5,
445
+ maxTokens: 2048
446
+ }
447
+ };
448
+ var DEFAULT_OPENCODEGO_FALLBACKS = {
449
+ default: [
450
+ { modelId: "mimo-v2-pro" },
451
+ { modelId: "qwen3.6-plus" }
452
+ ],
453
+ long_context: [
454
+ { modelId: "minimax-m2.7" },
455
+ { modelId: "kimi-k2.6" }
456
+ ],
457
+ think: [
458
+ { modelId: "kimi-k2.6" },
459
+ { modelId: "mimo-v2-pro" }
460
+ ],
461
+ complex: [
462
+ { modelId: "glm-5" },
463
+ { modelId: "kimi-k2.6" }
464
+ ],
465
+ fast: [
466
+ { modelId: "qwen3.5-plus" },
467
+ { modelId: "minimax-m2.5" }
468
+ ],
469
+ // AUTO-SELECTABLE (D3) — see DEFAULT_OPENCODEGO_MODEL_MAP.background. Mirrors
470
+ // config.example.json:69-73.
471
+ background: [
472
+ { modelId: "qwen3.6-plus" },
473
+ { modelId: "minimax-m2.5" }
474
+ ]
475
+ };
476
+
477
+ // src/opencodego/endpoints.ts
478
+ var OPENCODEGO_OPENAI_SHAPE_URL = "https://opencode.ai/zen/go/v1/chat/completions";
479
+ var OPENCODEGO_ANTHROPIC_SHAPE_URL = "https://opencode.ai/zen/go/v1/messages";
480
+ var OPENCODEGO_ZEN_CHAT_URL = "https://opencode.ai/zen/v1/chat/completions";
481
+ var OPENCODEGO_ZEN_ANTHROPIC_URL = "https://opencode.ai/zen/v1/messages";
482
+ var OPENCODEGO_ZEN_RESPONSES_URL = "https://opencode.ai/zen/v1/responses";
483
+ var OPENCODEGO_ZEN_GEMINI_BASE = "https://opencode.ai/zen/v1/models/";
484
+ var OPENCODEGO_CHAT_PATH = "/v1/chat/completions";
485
+ var OPENCODEGO_ANTHROPIC_PATH = "/v1/messages";
486
+ var OPENCODEGO_RESPONSES_PATH = "/v1/responses";
487
+ var OPENCODEGO_GEMINI_PATH = "/v1/models/";
488
+ function normalizeOpenCodeGoBaseUrl(base) {
489
+ return base.replace(/\/+$/, "").replace(/\/v\d+(\/.*)?$/, "");
490
+ }
491
+ function defaultUrlFor(half, shape) {
492
+ if (half === "zen") {
493
+ switch (shape) {
494
+ case "anthropic":
495
+ return OPENCODEGO_ZEN_ANTHROPIC_URL;
496
+ case "responses":
497
+ return OPENCODEGO_ZEN_RESPONSES_URL;
498
+ case "gemini":
499
+ return OPENCODEGO_ZEN_GEMINI_BASE;
500
+ case "chat":
501
+ default:
502
+ return OPENCODEGO_ZEN_CHAT_URL;
503
+ }
504
+ }
505
+ return shape === "anthropic" ? OPENCODEGO_ANTHROPIC_SHAPE_URL : OPENCODEGO_OPENAI_SHAPE_URL;
506
+ }
507
+ function pathFor(shape) {
508
+ switch (shape) {
509
+ case "anthropic":
510
+ return OPENCODEGO_ANTHROPIC_PATH;
511
+ case "responses":
512
+ return OPENCODEGO_RESPONSES_PATH;
513
+ case "gemini":
514
+ return OPENCODEGO_GEMINI_PATH;
515
+ case "chat":
516
+ default:
517
+ return OPENCODEGO_CHAT_PATH;
518
+ }
519
+ }
520
+ function buildOpenCodeGoUrl(half, shape, baseOverride) {
521
+ if (baseOverride) {
522
+ const base = normalizeOpenCodeGoBaseUrl(baseOverride);
523
+ return `${base}${pathFor(shape)}`;
524
+ }
525
+ return defaultUrlFor(half, shape);
526
+ }
527
+
528
+ // src/opencodego/model-shape.ts
529
+ var GO_ANTHROPIC_SHAPE_PREFIXES = ["minimax-", "minimax_"];
530
+ function isZenResponsesModel(modelId) {
531
+ return modelId.startsWith("gpt-5") || modelId.endsWith("-codex");
532
+ }
533
+ function isZenGeminiModel(modelId) {
534
+ return modelId.startsWith("gemini-");
535
+ }
536
+ function isZenAnthropicModel(modelId) {
537
+ if (modelId.startsWith("claude") || modelId.startsWith("minimax")) return true;
538
+ return modelId === "qwen3.7-max";
539
+ }
540
+ function classifyZenShape(modelId) {
541
+ const normalized = modelId.toLowerCase();
542
+ if (isZenAnthropicModel(normalized)) return "anthropic";
543
+ if (isZenGeminiModel(normalized)) return "gemini";
544
+ if (isZenResponsesModel(normalized)) return "responses";
545
+ return "chat";
546
+ }
547
+ function resolveOpenCodeGoShape(entry) {
548
+ const half = entry.provider ?? "go";
549
+ if (half === "zen") return classifyZenShape(entry.modelId);
550
+ const normalized = entry.modelId.toLowerCase();
551
+ if (GO_ANTHROPIC_SHAPE_PREFIXES.some((p) => normalized.startsWith(p))) {
552
+ return "anthropic";
553
+ }
554
+ return "chat";
555
+ }
556
+ function resolveOpenCodeGoHalf(modelId, config) {
557
+ if (!config) return "go";
558
+ for (const entry of Object.values(config.modelMap ?? {})) {
559
+ if (entry?.modelId === modelId) return entry.provider ?? "go";
560
+ }
561
+ for (const list of Object.values(config.fallbacks ?? {})) {
562
+ for (const entry of list ?? []) {
563
+ if (entry?.modelId === modelId) return entry.provider ?? "go";
564
+ }
565
+ }
566
+ return "go";
567
+ }
568
+
569
+ // src/opencodego/ScenarioRouter.ts
570
+ var COMPLEX_KEYWORDS = [
571
+ // Architectural
572
+ "architect",
573
+ "architecture",
574
+ "refactor",
575
+ "redesign",
576
+ "complex",
577
+ "difficult",
578
+ "challenging",
579
+ "optimize",
580
+ "performance",
581
+ "efficiency",
582
+ "design pattern",
583
+ "best practice",
584
+ // Tool-related keywords indicate complex operations
585
+ "execute",
586
+ "run command",
587
+ "bash",
588
+ "shell",
589
+ "implement",
590
+ "build",
591
+ "create",
592
+ "add feature",
593
+ "write to",
594
+ "edit file",
595
+ "create file"
596
+ ];
597
+ var THINKING_KEYWORDS = [
598
+ "think",
599
+ "thinking",
600
+ "plan",
601
+ "reason",
602
+ "reasoning",
603
+ "analyze",
604
+ "analysis",
605
+ "step by step"
606
+ ];
607
+ var ANT_THINKING_MARKER = "antThinking";
608
+ var TOOL_BLOCKERS = [
609
+ "tool",
610
+ "function",
611
+ "execute",
612
+ "run command",
613
+ "write",
614
+ "edit",
615
+ "create",
616
+ "delete",
617
+ "remove",
618
+ "implement",
619
+ "build",
620
+ "add",
621
+ "modify"
622
+ ];
623
+ var BACKGROUND_KEYWORDS = [
624
+ "list directory",
625
+ "ls -",
626
+ "dir",
627
+ "show file",
628
+ "view file",
629
+ "cat file",
630
+ "what is",
631
+ "what's",
632
+ "tell me about",
633
+ "check status",
634
+ "show status"
635
+ ];
636
+ function toLowerSlices(matchText) {
637
+ if (!matchText || matchText.length === 0) return [];
638
+ return matchText.map((s) => s.toLowerCase());
639
+ }
640
+ function containsAny(loweredSlices, keywords) {
641
+ for (const slice of loweredSlices) {
642
+ for (const kw of keywords) {
643
+ if (slice.includes(kw)) return true;
644
+ }
645
+ }
646
+ return false;
647
+ }
648
+ function hasComplexPattern(loweredSlices) {
649
+ return containsAny(loweredSlices, COMPLEX_KEYWORDS);
650
+ }
651
+ function hasThinkingPattern(loweredSlices, rawSlices) {
652
+ if (containsAny(loweredSlices, THINKING_KEYWORDS)) return true;
653
+ for (const slice of rawSlices) {
654
+ if (slice.includes(ANT_THINKING_MARKER)) return true;
655
+ }
656
+ return false;
657
+ }
658
+ function hasBackgroundPattern(loweredSlices) {
659
+ if (containsAny(loweredSlices, TOOL_BLOCKERS)) return false;
660
+ return containsAny(loweredSlices, BACKGROUND_KEYWORDS);
661
+ }
662
+ function resolveOpenCodeGoScenario(summary, config) {
663
+ const longContextThreshold = config?.modelMap?.long_context?.contextThreshold ?? DEFAULT_OPENCODEGO_LONG_CONTEXT_THRESHOLD;
664
+ if (summary.estimatedInputTokens >= longContextThreshold) {
665
+ return "long_context";
666
+ }
667
+ const rawSlices = summary.matchText ?? [];
668
+ const loweredSlices = toLowerSlices(summary.matchText);
669
+ if (hasComplexPattern(loweredSlices)) return "complex";
670
+ if (hasThinkingPattern(loweredSlices, rawSlices)) return "think";
671
+ if (hasBackgroundPattern(loweredSlices)) return "background";
672
+ return "default";
673
+ }
674
+
675
+ // src/SubscriptionProviderRegistry.ts
676
+ var CLAUDE_MESSAGES_UPSTREAM_URL = "https://api.anthropic.com/v1/messages";
677
+ function opencodegoTransformerNamesForShape(shape) {
678
+ switch (shape) {
679
+ case "anthropic":
680
+ return [];
681
+ case "responses":
682
+ return ["openai-response"];
683
+ case "gemini":
684
+ return ["gemini"];
685
+ case "chat":
686
+ default:
687
+ return ["opencodego"];
688
+ }
689
+ }
690
+ function resolveOpenCodeGoTarget(modelId, config) {
691
+ const half = resolveOpenCodeGoHalf(modelId, config);
692
+ const shape = resolveOpenCodeGoShape({ provider: half, modelId });
693
+ return { half, shape };
694
+ }
695
+ var SubscriptionProviderRegistry = class {
696
+ constructor(accounts, tokens) {
697
+ this.accounts = accounts;
698
+ this.tokens = tokens;
699
+ const claude = this.accounts.getStrategy("claude");
700
+ const codex = this.accounts.getStrategy("codex");
701
+ const gemini = this.accounts.getStrategy("gemini");
702
+ const opencodego = this.accounts.getStrategy("opencodego");
703
+ if (!claude || !codex || !gemini || !opencodego) {
704
+ throw new Error("[SubscriptionProviderRegistry] Missing strategy in SubscriptionAccountService");
705
+ }
706
+ this.profiles = /* @__PURE__ */ new Map([
707
+ [
708
+ "claude",
709
+ {
710
+ providerId: "claude",
711
+ displayName: "Claude (Anthropic OAuth)",
712
+ authStrategy: claude,
713
+ // The MAIN claude path stays `pass-through`: the Anthropic-ingress
714
+ // proxy (`buildCodeCliPassThroughResult` → `handlePassThroughRequest`)
715
+ // forwards the SDK's Anthropic body VERBATIM to `api.anthropic.com`
716
+ // with the user's OAuth, and it NEVER reads the two fields below
717
+ // (`resolveUpstreamUrl` / `providerTransformerNames`) — it hard-codes
718
+ // the URL and skips the transformer chain + the AuthStrategy entirely.
719
+ // So these fields are INERT for the pass-through path; they exist
720
+ // SOLELY so the generalized Codex/Responses route-to plan
721
+ // (`resolveSubscriptionChain` in `buildSubscriptionPlan`) can serve a
722
+ // `Codex CLI → Claude subscription` route. That plan needs (a) a
723
+ // provider transformer chain to re-encode Unified → Anthropic Messages
724
+ // and (b) an upstream URL — both supplied here, reusing the existing
725
+ // claude OAuth `authStrategy`. See `cliRouteResolution.ts`
726
+ // SOUND_SUBSCRIPTION_PROVIDERS for the un-gate rationale.
727
+ mode: "pass-through",
728
+ // Route-to (Responses ingress) only: Unified → Anthropic Messages.
729
+ resolveUpstreamUrl: () => CLAUDE_MESSAGES_UPSTREAM_URL,
730
+ providerTransformerNames: ["anthropic"],
731
+ modelTransformerNames: []
732
+ }
733
+ ],
734
+ [
735
+ "codex",
736
+ {
737
+ providerId: "codex",
738
+ displayName: "Codex (ChatGPT OAuth)",
739
+ authStrategy: codex,
740
+ mode: "transformer",
741
+ // ChatGPT internal endpoint — accepts the OpenAI Responses API
742
+ // format. Mirrors `_others/claude-relay-service/src/routes/openaiRoutes.js:454`.
743
+ // The Codex OAuth access token grants access here; the public
744
+ // `api.openai.com/v1/responses` endpoint would reject the same token.
745
+ resolveUpstreamUrl: () => "https://chatgpt.com/backend-api/codex/responses",
746
+ providerTransformerNames: ["openai-response"],
747
+ modelTransformerNames: []
748
+ }
749
+ ],
750
+ [
751
+ "gemini",
752
+ {
753
+ providerId: "gemini",
754
+ displayName: "Gemini (Google OAuth)",
755
+ authStrategy: gemini,
756
+ mode: "transformer",
757
+ // GAP CLOSED: Gemini CLI subscription tokens are minted for Google's
758
+ // **Code Assist** endpoint (`cloudcode-pa.googleapis.com`), which
759
+ // wraps `generateContent` in a project/session envelope and uses a
760
+ // colon-method URL (`v1internal:generateContent`, NO `/models/<model>`
761
+ // path — the model lives in the body). The `gemini-code-assist`
762
+ // transformer now does that envelope work (delegating inner encoding
763
+ // to the shared gemini utils), and the dispatch seam threads the
764
+ // resolved Code Assist project id onto `transformerProvider.geminiProject`
765
+ // (resolved once per account via `GeminiCodeAssistProjectResolver`).
766
+ // `resolveUpstreamUrl` ignores the model (Code Assist has no per-model
767
+ // path); the URL is the version-segment colon-method endpoint.
768
+ resolveUpstreamUrl: (_model) => buildCodeAssistUrl(false),
769
+ providerTransformerNames: ["gemini-code-assist"],
770
+ modelTransformerNames: []
771
+ }
772
+ ],
773
+ [
774
+ "opencodego",
775
+ {
776
+ providerId: "opencodego",
777
+ displayName: "OpenCodeGo (Bearer key)",
778
+ authStrategy: opencodego,
779
+ mode: "transformer",
780
+ // D1 + zen: resolve the per-model `(half, shape)` from the resolved
781
+ // model id + the opaque per-account config, then build the half-specific
782
+ // URL honoring the half-appropriate host override (`baseUrl` for go,
783
+ // `zenBaseUrl` for zen). The OPTIONAL `config` arg is threaded by BOTH
784
+ // dispatch paths (the `SubscriptionDispatcher` passes its already-fetched
785
+ // `ocConfig`; the core `/v1/messages` plan builder passes the opaque
786
+ // `route.subscriptionConfig`). With NO zen config every resolved model
787
+ // is go-half → byte-identical to the prior resolver.
788
+ // `// UNVERIFIED (no live zen key)`: the zen endpoint hosts/paths are
789
+ // ported from the reference + proven in-process only.
790
+ resolveUpstreamUrl: (model, config) => {
791
+ const oc = config;
792
+ const { half, shape } = resolveOpenCodeGoTarget(model, oc);
793
+ const override = half === "zen" ? oc?.zenBaseUrl : oc?.baseUrl;
794
+ return buildOpenCodeGoUrl(half, shape, override);
795
+ },
796
+ // zen seam (Decision 3): vary the provider transformer chain by resolved
797
+ // shape (anthropic⇒[] verbatim, chat⇒opencodego, responses⇒openai-response,
798
+ // gemini⇒gemini). OPTIONAL on the profile type — only opencodego sets it;
799
+ // claude/codex/gemini omit it and fall back to `providerTransformerNames`,
800
+ // keeping their routing byte-identical. The static `providerTransformerNames`
801
+ // below stays the go-half default both ingress paths use when this method
802
+ // is somehow unconsulted.
803
+ resolveProviderTransformerNames: (model, config) => {
804
+ const { shape } = resolveOpenCodeGoTarget(model, config);
805
+ return opencodegoTransformerNamesForShape(shape);
806
+ },
807
+ providerTransformerNames: ["opencodego"],
808
+ modelTransformerNames: [],
809
+ modelMapper: (sdkModel, summary, config) => {
810
+ const scenario = resolveOpenCodeGoScenario(summary, config);
811
+ const entry = config?.modelMap?.[scenario] ?? config?.modelMap?.default ?? DEFAULT_OPENCODEGO_MODEL_MAP[scenario] ?? DEFAULT_OPENCODEGO_MODEL_MAP.default;
812
+ if (!entry) {
813
+ return { resolvedModel: sdkModel, scenario };
814
+ }
815
+ return { resolvedModel: entry.modelId, scenario };
816
+ },
817
+ // D2 CONSULT: skip both already-attempted models AND models whose
818
+ // circuit is open. `breaker.allowRequest(modelId)` is the admission
819
+ // gate — calling it has the side effect of flipping an `open` model to
820
+ // `half-open` once its 30s window elapses AND counting a half-open admit
821
+ // slot. It MUST therefore be consulted EXACTLY ONCE per returned model,
822
+ // on the candidate about to be attempted — mirroring the reference
823
+ // (`fallback.go` calls `AllowRequest` once, on the model it returns).
824
+ // An early-returning scan (NOT `Array.filter`, which would `allowRequest`
825
+ // every candidate and burn the admit slots of half-open models AFTER the
826
+ // chosen one — those are never attempted, never recorded, so they would
827
+ // wedge permanently in half-open). When NO circuit is open this returns
828
+ // the same first non-attempted entry as the prior `!attempted` filter.
829
+ nextFallback: (scenario, attempted, config) => {
830
+ const list = config?.fallbacks?.[scenario] ?? DEFAULT_OPENCODEGO_FALLBACKS[scenario] ?? [];
831
+ for (const entry of list) {
832
+ if (attempted.includes(entry.modelId)) continue;
833
+ if (this.breaker.allowRequest(entry.modelId)) return entry;
834
+ }
835
+ return null;
836
+ },
837
+ // D2 PRIMARY-GATING: admission gate the loops consult for the PRIMARY
838
+ // (mapped) model before attempt #1 — `nextFallback` only covers
839
+ // fallbacks. Same side-effecting `allowRequest` semantics.
840
+ allowModel: (modelId) => this.breaker.allowRequest(modelId),
841
+ // D3 RECORD: the cross-path seam. Both fallback loops call this after
842
+ // each attempt; it drives the per-model breaker. Only the opencodego
843
+ // profile sets it — claude / codex / gemini leave it UNSET (no-op).
844
+ recordModelOutcome: (modelId, ok) => ok ? this.breaker.recordSuccess(modelId) : this.breaker.recordFailure(modelId)
845
+ }
846
+ ]
847
+ ]);
848
+ }
849
+ accounts;
850
+ tokens;
851
+ profiles;
852
+ /**
853
+ * Per-model circuit breaker for opencodego routing (D5). ONE registry-owned
854
+ * process singleton, built here and captured by the opencodego profile's
855
+ * `nextFallback` (consult) + `recordModelOutcome` (record) closures. Because
856
+ * the `SubscriptionProviderRegistry` is itself a process singleton (via
857
+ * `setSubscriptionProviderRegistry`), breaker state persists across requests —
858
+ * exactly the reference's long-lived `FallbackHandler`. Constructed with the
859
+ * default reference thresholds (3 / 30s / 3) and the default `Date.now` clock.
860
+ */
861
+ breaker = new CircuitBreakerRegistry();
862
+ /** Returns the dispatch profile for a known subscription provider, or
863
+ * `null` for unknown ids (callers must treat null as "fall back to the
864
+ * legacy LLM provider DB lookup"). */
865
+ getProfile(providerId) {
866
+ return this.profiles.get(providerId) ?? null;
867
+ }
868
+ /** Read the currently-stored OpenCodeGo config so the proxy can pick up
869
+ * user overrides (modelMap / fallbacks / baseUrl). Wraps the injected
870
+ * `SubscriptionCredentialStore` so the proxy doesn't need to know about
871
+ * that surface. */
872
+ async getOpenCodeGoConfig() {
873
+ const full = await this.tokens.getFullConfig();
874
+ return full.opencodego;
875
+ }
876
+ };
877
+ var _moduleSingleton2 = null;
878
+ function setSubscriptionProviderRegistry(svc) {
879
+ _moduleSingleton2 = svc;
880
+ setSubscriptionRegistryForOutbound(svc ?? null);
881
+ }
882
+ function getSubscriptionProviderRegistry() {
883
+ return _moduleSingleton2;
884
+ }
885
+
886
+ // ../core/src/ports/gemini-code-assist-resolver.ts
887
+ var resolver = null;
888
+ function getGeminiCodeAssistResolver() {
889
+ return resolver;
890
+ }
891
+
892
+ // ../core/src/provider-proxy/matchText.ts
893
+ var MATCH_TEXT_PER_MESSAGE_CAP = 8192;
894
+ var MATCH_TEXT_RECENT_MESSAGES = 6;
895
+ function flattenMatchText(value) {
896
+ if (typeof value === "string") return value;
897
+ if (Array.isArray(value)) {
898
+ const parts = [];
899
+ for (const item of value) {
900
+ const text = flattenMatchText(item);
901
+ if (text) parts.push(text);
902
+ }
903
+ return parts.join("\n");
904
+ }
905
+ if (value && typeof value === "object") {
906
+ const obj = value;
907
+ if (obj.type === "tool_result" && obj.content !== void 0) {
908
+ return flattenMatchText(obj.content);
909
+ }
910
+ if (typeof obj.text === "string") return obj.text;
911
+ }
912
+ return "";
913
+ }
914
+ function collectMatchText(anthropicBody) {
915
+ const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
916
+ const slices = [];
917
+ const sys = flattenMatchText(anthropicBody.system).trim();
918
+ if (sys) slices.push(sys.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
919
+ const recent = [];
920
+ for (let i = messages.length - 1; i >= 0 && recent.length < MATCH_TEXT_RECENT_MESSAGES; i--) {
921
+ const message = messages[i];
922
+ if (!message || typeof message !== "object") continue;
923
+ const role = message.role;
924
+ if (role !== "user" && role !== "system") continue;
925
+ const text = flattenMatchText(message.content).trim();
926
+ if (text) recent.push(text.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
927
+ }
928
+ for (let i = recent.length - 1; i >= 0; i--) slices.push(recent[i]);
929
+ return slices;
930
+ }
931
+
932
+ // ../core/src/serializeError.ts
933
+ function serializeError(err) {
934
+ if (err == null) return "Unknown error (null)";
935
+ if (err instanceof Error) {
936
+ let msg = err.message || err.name || "Error";
937
+ if (err.cause) {
938
+ msg += ` [cause: ${serializeError(err.cause)}]`;
939
+ }
940
+ const anyErr = err;
941
+ if (anyErr.status != null) msg += ` (status: ${anyErr.status})`;
942
+ else if (anyErr.code != null) msg += ` (code: ${anyErr.code})`;
943
+ return msg;
944
+ }
945
+ if (typeof err === "string") return err || "Empty error string";
946
+ if (typeof err !== "object") return String(err);
947
+ const obj = err;
948
+ if (typeof obj.message === "string" && obj.message) {
949
+ let msg = obj.message;
950
+ if (obj.status != null) msg += ` (status: ${obj.status})`;
951
+ else if (obj.code != null) msg += ` (code: ${obj.code})`;
952
+ if (typeof obj.type === "string") msg += ` [type: ${obj.type}]`;
953
+ return msg;
954
+ }
955
+ if (typeof obj.error === "string" && obj.error) {
956
+ return obj.error;
957
+ }
958
+ if (obj.error && typeof obj.error === "object") {
959
+ const inner = obj.error;
960
+ if (typeof inner.message === "string" && inner.message) {
961
+ let msg = inner.message;
962
+ if (typeof inner.type === "string") msg += ` [type: ${inner.type}]`;
963
+ return msg;
964
+ }
965
+ }
966
+ try {
967
+ const json = JSON.stringify(err, getCircularReplacer(), 2);
968
+ if (json && json.length > 1e3) {
969
+ return json.slice(0, 1e3) + "... (truncated)";
970
+ }
971
+ return json || "Unserializable error";
972
+ } catch {
973
+ return `Unserializable error: ${Object.prototype.toString.call(err)}`;
974
+ }
975
+ }
976
+ function getCircularReplacer() {
977
+ const seen = /* @__PURE__ */ new WeakSet();
978
+ return (_key, value) => {
979
+ if (typeof value === "object" && value !== null) {
980
+ if (seen.has(value)) return "[Circular]";
981
+ seen.add(value);
982
+ }
983
+ return value;
984
+ };
985
+ }
986
+
987
+ // src/opencodego/token-count.ts
988
+ var cachedEncode = null;
989
+ function estimateTokensCachedSync(text) {
990
+ if (!text) return 0;
991
+ if (cachedEncode) {
992
+ return cachedEncode(text).length;
993
+ }
994
+ return Math.ceil(text.length / 4);
995
+ }
996
+
997
+ // src/SubscriptionDispatcher.ts
998
+ var SubscriptionDispatcher = class {
999
+ constructor(profile, hooks, getOpenCodeGoConfig) {
1000
+ this.profile = profile;
1001
+ this.hooks = hooks;
1002
+ this.getOpenCodeGoConfig = getOpenCodeGoConfig;
1003
+ }
1004
+ profile;
1005
+ hooks;
1006
+ getOpenCodeGoConfig;
1007
+ /**
1008
+ * Entry point — called by the host proxy's request handler after model
1009
+ * resolution and probe-detection.
1010
+ */
1011
+ async dispatch(req) {
1012
+ const ocConfig = this.profile.providerId === "opencodego" ? await this.getOpenCodeGoConfig() : void 0;
1013
+ let scenario = "default";
1014
+ let resolvedModel = req.fallbackModel;
1015
+ if (this.profile.modelMapper) {
1016
+ const summary = this.buildRequestSummary(req.anthropicBody);
1017
+ const mapped = this.profile.modelMapper(req.sdkModel, summary, ocConfig);
1018
+ resolvedModel = mapped.resolvedModel;
1019
+ scenario = mapped.scenario;
1020
+ req.anthropicBody.model = resolvedModel;
1021
+ }
1022
+ const upstreamUrl = this.profile.resolveUpstreamUrl?.(resolvedModel, ocConfig);
1023
+ if (!upstreamUrl) {
1024
+ throw new Error(`[SubscriptionDispatcher] profile=${this.profile.providerId} missing resolveUpstreamUrl`);
1025
+ }
1026
+ if (this.profile.providerId === "opencodego" && resolveOpenCodeGoShape({
1027
+ provider: resolveOpenCodeGoHalf(resolvedModel, ocConfig),
1028
+ modelId: resolvedModel
1029
+ }) === "anthropic") {
1030
+ await this.dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig);
1031
+ return;
1032
+ }
1033
+ await this.dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig);
1034
+ }
1035
+ /** Bypass path for OpenCodeGo MiniMax models — forwards Anthropic body verbatim. */
1036
+ async dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
1037
+ const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
1038
+ const attempted = gate.attempted;
1039
+ let currentModel = gate.firstModel;
1040
+ while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
1041
+ attempted.push(currentModel);
1042
+ req.anthropicBody.model = currentModel;
1043
+ const headers = { "content-type": "application/json" };
1044
+ await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
1045
+ console.info(
1046
+ `[AgentProxy:subscription] REQ#${req.reqId} | opencodego anthropic-shape -> ${upstreamUrl} model=${currentModel} attempt=${attempted.length}`
1047
+ );
1048
+ try {
1049
+ const upstream = await this.hooks.fetchWithRetry(upstreamUrl, headers, req.anthropicBody, currentModel);
1050
+ this.profile.recordModelOutcome?.(currentModel, true);
1051
+ await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
1052
+ return;
1053
+ } catch (err) {
1054
+ const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
1055
+ if (handled.retryOnce) {
1056
+ const upstream = await this.hooks.fetchWithRetry(upstreamUrl, handled.headers, req.anthropicBody, currentModel);
1057
+ this.profile.recordModelOutcome?.(currentModel, true);
1058
+ await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
1059
+ return;
1060
+ }
1061
+ if (caughtErrorBreakerOutcome(err) === "failure") {
1062
+ this.profile.recordModelOutcome?.(currentModel, false);
1063
+ }
1064
+ const next = this.profile.nextFallback?.(scenario, attempted, ocConfig);
1065
+ if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
1066
+ throw err;
1067
+ }
1068
+ console.warn(
1069
+ `[AgentProxy:subscription] REQ#${req.reqId} | opencodego fallback ${currentModel} -> ${next.modelId} after error:`,
1070
+ serializeError(err)
1071
+ );
1072
+ currentModel = next.modelId;
1073
+ }
1074
+ }
1075
+ }
1076
+ /** Standard subscription transformer chain — Codex/Gemini/OpenCodeGo OpenAI-shape. */
1077
+ async dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
1078
+ const providerNames = this.profile.resolveProviderTransformerNames?.(resolvedModel, ocConfig) ?? this.profile.providerTransformerNames;
1079
+ const providerTransformers = this.resolveTransformers(providerNames);
1080
+ const modelTransformers = this.resolveTransformers(this.profile.modelTransformerNames);
1081
+ const transformerProvider = {
1082
+ name: this.profile.providerId,
1083
+ baseUrl: upstreamUrl,
1084
+ apiKey: "",
1085
+ // Subscription mode uses AuthStrategy; transformer auth is stripped.
1086
+ models: [resolvedModel]
1087
+ };
1088
+ if (this.profile.providerId === "gemini") {
1089
+ transformerProvider.geminiProject = await this.resolveGeminiProject();
1090
+ }
1091
+ const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
1092
+ const attempted = gate.attempted;
1093
+ let currentModel = gate.firstModel;
1094
+ while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
1095
+ attempted.push(currentModel);
1096
+ req.anthropicBody.model = currentModel;
1097
+ const { requestBody, config } = await this.hooks.executor.executeRequestChain(
1098
+ req.anthropicBody,
1099
+ transformerProvider,
1100
+ { providerTransformers, modelTransformers },
1101
+ { endpointTransformer: this.hooks.endpointTransformer }
1102
+ );
1103
+ const headers = {
1104
+ "content-type": "application/json",
1105
+ ...config.headers
1106
+ };
1107
+ stripAuthHeaders(headers);
1108
+ await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
1109
+ const fetchUrl = usesResponsesChain(providerNames) ? upstreamUrl : resolveConfigUrl(config.url) ?? upstreamUrl;
1110
+ console.info(
1111
+ `[AgentProxy:subscription] REQ#${req.reqId} | provider=${this.profile.providerId} -> ${fetchUrl} model=${currentModel} attempt=${attempted.length}`
1112
+ );
1113
+ try {
1114
+ const upstream = await this.hooks.fetchWithRetry(fetchUrl, headers, requestBody, currentModel);
1115
+ this.profile.recordModelOutcome?.(currentModel, true);
1116
+ const finalResponse = await this.hooks.executor.executeResponseChain(
1117
+ requestBody,
1118
+ upstream,
1119
+ transformerProvider,
1120
+ { providerTransformers, modelTransformers },
1121
+ { endpointTransformer: this.hooks.endpointTransformer }
1122
+ );
1123
+ await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
1124
+ return;
1125
+ } catch (err) {
1126
+ const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
1127
+ if (handled.retryOnce) {
1128
+ const upstream = await this.hooks.fetchWithRetry(fetchUrl, handled.headers, requestBody, currentModel);
1129
+ this.profile.recordModelOutcome?.(currentModel, true);
1130
+ const finalResponse = await this.hooks.executor.executeResponseChain(
1131
+ requestBody,
1132
+ upstream,
1133
+ transformerProvider,
1134
+ { providerTransformers, modelTransformers },
1135
+ { endpointTransformer: this.hooks.endpointTransformer }
1136
+ );
1137
+ await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
1138
+ return;
1139
+ }
1140
+ if (caughtErrorBreakerOutcome(err) === "failure") {
1141
+ this.profile.recordModelOutcome?.(currentModel, false);
1142
+ }
1143
+ const next = this.profile.nextFallback?.(scenario, attempted, ocConfig);
1144
+ if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
1145
+ throw err;
1146
+ }
1147
+ console.warn(
1148
+ `[AgentProxy:subscription] REQ#${req.reqId} | ${this.profile.providerId} fallback ${currentModel} -> ${next.modelId} after error:`,
1149
+ serializeError(err)
1150
+ );
1151
+ currentModel = next.modelId;
1152
+ }
1153
+ }
1154
+ }
1155
+ /**
1156
+ * D2 PRIMARY-GATING (opencodego, D5): pick the first-attempt model for a
1157
+ * fallback loop. Consults the breaker for the mapped primary; when the primary's
1158
+ * circuit is open, advance to the first admitting `nextFallback` candidate
1159
+ * WITHOUT an upstream round-trip on the open primary. When EVERY candidate is
1160
+ * open (all-open) the breaker FAILS OPEN — it attempts the original primary
1161
+ * anyway. Returns the resolved first-attempt model plus the `attempted` list
1162
+ * seeded for the loop (the SKIPPED primary is recorded at chain index 0 so the
1163
+ * loop's `nextFallback` excludes it; on fail-open the list is left empty so the
1164
+ * primary is the first real attempt). When the profile has no `allowModel`
1165
+ * (claude / codex / gemini, or breaker unset) this is byte-identical to the
1166
+ * prior behavior: the primary is the first attempt, `attempted` empty.
1167
+ */
1168
+ gatePrimaryModel(primaryModel, scenario, ocConfig) {
1169
+ if (!this.profile.allowModel || this.profile.allowModel(primaryModel)) {
1170
+ return { firstModel: primaryModel, attempted: [] };
1171
+ }
1172
+ const skipped = [primaryModel];
1173
+ const firstAdmitting = this.profile.nextFallback?.(scenario, skipped, ocConfig);
1174
+ if (firstAdmitting) {
1175
+ console.warn(
1176
+ `[AgentProxy:subscription] opencodego primary ${primaryModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
1177
+ );
1178
+ return { firstModel: firstAdmitting.modelId, attempted: skipped };
1179
+ }
1180
+ console.warn(
1181
+ `[AgentProxy:subscription] all opencodego circuits open -> fail open to primary ${primaryModel}`
1182
+ );
1183
+ return { firstModel: primaryModel, attempted: [] };
1184
+ }
1185
+ /**
1186
+ * On a 401 error from the upstream, ask the AuthStrategy whether to retry.
1187
+ * Returns `{ retryOnce: true, headers }` when the strategy refreshed
1188
+ * successfully (caller should retry once); otherwise re-throws.
1189
+ */
1190
+ async maybeRetryAfterError(err, headers, req, resolvedModel) {
1191
+ const status = err?.status ?? 0;
1192
+ if (status !== 401) {
1193
+ return { retryOnce: false, headers };
1194
+ }
1195
+ const refreshed = await this.profile.authStrategy.onUnauthorized();
1196
+ if (!refreshed) {
1197
+ console.warn(
1198
+ `[AgentProxy:subscription] REQ#${req.reqId} | 401 not recoverable for provider=${this.profile.providerId}`
1199
+ );
1200
+ return { retryOnce: false, headers };
1201
+ }
1202
+ const fresh = { ...headers };
1203
+ stripAuthHeaders(fresh);
1204
+ await this.applyHeadersWithRetry(fresh, { upstreamUrl: "", resolvedModel });
1205
+ return { retryOnce: true, headers: fresh };
1206
+ }
1207
+ async applyHeadersWithRetry(headers, hints) {
1208
+ try {
1209
+ await this.profile.authStrategy.applyHeaders(headers, hints);
1210
+ } catch (err) {
1211
+ console.warn("[AgentProxy:subscription] authStrategy.applyHeaders threw:", serializeError(err));
1212
+ }
1213
+ }
1214
+ /**
1215
+ * Resolve the Code Assist project for the gemini subscription profile. Pulls
1216
+ * the Bearer the bound `AuthStrategy` would inject (so the strategy stays the
1217
+ * single source of the token), then runs the cached handshake. Returns
1218
+ * `undefined` for a fresh free-tier account (valid — the envelope omits the
1219
+ * project). A handshake hard failure (403/429) propagates to the dispatch
1220
+ * error handler.
1221
+ */
1222
+ async resolveGeminiProject() {
1223
+ const probe = {};
1224
+ await this.applyHeadersWithRetry(probe, { upstreamUrl: "", resolvedModel: "" });
1225
+ const bearer = probe.Authorization ?? probe.authorization ?? "";
1226
+ const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
1227
+ if (!accessToken) return void 0;
1228
+ const resolver2 = getGeminiCodeAssistResolver();
1229
+ if (!resolver2) return void 0;
1230
+ return resolver2.resolveProject(accessToken);
1231
+ }
1232
+ resolveTransformers(names) {
1233
+ if (!names || names.length === 0) return [];
1234
+ const resolved = [];
1235
+ for (const name of names) {
1236
+ const t = this.hooks.transformerService.getTransformer(name);
1237
+ if (!t) {
1238
+ console.warn(`[AgentProxy:subscription] Transformer not registered: ${name}`);
1239
+ continue;
1240
+ }
1241
+ const instance = typeof t === "function" ? new t() : t;
1242
+ resolved.push(instance);
1243
+ }
1244
+ return resolved;
1245
+ }
1246
+ /** Build a lightweight request summary for OpenCodeGo scenario routing. */
1247
+ buildRequestSummary(anthropicBody) {
1248
+ const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
1249
+ let totalChars = 0;
1250
+ const system = anthropicBody.system;
1251
+ if (typeof system === "string") {
1252
+ totalChars += system.length;
1253
+ } else if (Array.isArray(system)) {
1254
+ for (const block of system) {
1255
+ if (block && typeof block === "object" && "text" in block) {
1256
+ totalChars += String(block.text ?? "").length;
1257
+ }
1258
+ }
1259
+ }
1260
+ for (const msg of messages) {
1261
+ const m = msg;
1262
+ const c = m.content;
1263
+ if (typeof c === "string") {
1264
+ totalChars += c.length;
1265
+ } else if (Array.isArray(c)) {
1266
+ for (const block of c) {
1267
+ if (block && typeof block === "object") {
1268
+ const b = block;
1269
+ if (b.type === "text" && typeof b.text === "string") {
1270
+ totalChars += b.text.length;
1271
+ } else if (b.type === "tool_result" && typeof b.content === "string") {
1272
+ totalChars += b.content.length;
1273
+ }
1274
+ }
1275
+ }
1276
+ }
1277
+ }
1278
+ return {
1279
+ messageCount: messages.length,
1280
+ estimatedInputTokens: estimateTokensCachedSync("x".repeat(totalChars)),
1281
+ // Shared core flattener (single source of truth) so this dispatcher path
1282
+ // and the core `/v1/messages` path produce IDENTICAL `matchText` for the
1283
+ // same body — equivalence by construction. `@omnicross/subscriptions` →
1284
+ // `@omnicross/core` is the allowed direction; core imports nothing back.
1285
+ matchText: collectMatchText(anthropicBody)
1286
+ };
1287
+ }
1288
+ };
1289
+ var MAX_FALLBACK_ATTEMPTS_LOCAL = 3;
1290
+ function caughtErrorBreakerOutcome(err) {
1291
+ const status = err?.status;
1292
+ if (typeof status !== "number") return "failure";
1293
+ if (status === 0) return "neutral";
1294
+ if (status >= 500 || status === 429) return "failure";
1295
+ if (status >= 400 && status < 500) return "neutral";
1296
+ return "failure";
1297
+ }
1298
+ function usesResponsesChain(names) {
1299
+ return !!names && names.includes("openai-response");
1300
+ }
1301
+ function resolveConfigUrl(url) {
1302
+ if (url instanceof URL) return url.toString();
1303
+ if (typeof url === "string" && url.length > 0) return url;
1304
+ return null;
1305
+ }
1306
+ function stripAuthHeaders(headers) {
1307
+ delete headers.authorization;
1308
+ delete headers.Authorization;
1309
+ delete headers["x-api-key"];
1310
+ delete headers["X-Api-Key"];
1311
+ delete headers["x-goog-api-key"];
1312
+ delete headers["X-Goog-Api-Key"];
1313
+ }
1314
+
1315
+ // src/oauth/flows/claude.ts
1316
+ var claude_exports = {};
1317
+ __export(claude_exports, {
1318
+ exchangeCodeForTokens: () => exchangeCodeForTokens,
1319
+ exchangeSetupTokenCode: () => exchangeSetupTokenCode,
1320
+ generateAuthParams: () => generateAuthParams,
1321
+ generateSetupTokenParams: () => generateSetupTokenParams,
1322
+ refreshAccessToken: () => refreshAccessToken
1323
+ });
1324
+ var import_node_crypto = __toESM(require("crypto"), 1);
1325
+
1326
+ // src/oauth/fetchPort.ts
1327
+ function errorMessage(error, errorDescription) {
1328
+ if (errorDescription) return errorDescription;
1329
+ if (typeof error === "string") return error;
1330
+ if (error && typeof error === "object") {
1331
+ const e = error;
1332
+ if (typeof e.message === "string" && e.message) return e.message;
1333
+ if (typeof e.error_description === "string" && e.error_description) {
1334
+ return e.error_description;
1335
+ }
1336
+ return JSON.stringify(error);
1337
+ }
1338
+ return String(error);
1339
+ }
1340
+ async function postForm(fetchImpl, url, params, parseErrorMessage) {
1341
+ const response = await fetchImpl(url, {
1342
+ method: "POST",
1343
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1344
+ body: params.toString()
1345
+ });
1346
+ const responseData = await response.text();
1347
+ let data;
1348
+ try {
1349
+ data = JSON.parse(responseData);
1350
+ } catch {
1351
+ throw new Error(parseErrorMessage);
1352
+ }
1353
+ if (data.error) {
1354
+ throw new Error(errorMessage(data.error, data.error_description));
1355
+ }
1356
+ return data;
1357
+ }
1358
+ async function postJson(fetchImpl, url, body, parseErrorMessage, extraHeaders = {}) {
1359
+ const response = await fetchImpl(url, {
1360
+ method: "POST",
1361
+ headers: { "Content-Type": "application/json", ...extraHeaders },
1362
+ body: JSON.stringify(body)
1363
+ });
1364
+ const responseData = await response.text();
1365
+ let data;
1366
+ try {
1367
+ data = JSON.parse(responseData);
1368
+ } catch {
1369
+ throw new Error(parseErrorMessage);
1370
+ }
1371
+ if (data.error) {
1372
+ throw new Error(errorMessage(data.error, data.error_description));
1373
+ }
1374
+ return data;
1375
+ }
1376
+
1377
+ // src/oauth/flows/claude.ts
1378
+ var CLAUDE_TOKEN_HEADERS = {
1379
+ "User-Agent": "claude-cli/1.0.56 (external, cli)",
1380
+ Accept: "application/json, text/plain, */*",
1381
+ "Accept-Language": "en-US,en;q=0.9",
1382
+ Referer: "https://claude.ai/",
1383
+ Origin: "https://claude.ai"
1384
+ };
1385
+ var CLAUDE_OAUTH_CONFIG = {
1386
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
1387
+ authorizationEndpoint: "https://claude.ai/oauth/authorize",
1388
+ // The token endpoint stays on console.anthropic.com (still the live value —
1389
+ // matches the official Claude Code CLI / claude-relay-service reference); only
1390
+ // the OAuth callback moved to platform.claude.com (2026). The redirect_uri MUST
1391
+ // match what the client is registered for AND match between authorize + token
1392
+ // exchange. Scopes mirror the live Claude Code authorize URL.
1393
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
1394
+ redirectUri: "https://platform.claude.com/oauth/code/callback",
1395
+ scopes: [
1396
+ "org:create_api_key",
1397
+ "user:profile",
1398
+ "user:inference",
1399
+ "user:sessions:claude_code",
1400
+ "user:mcp_servers",
1401
+ "user:file_upload"
1402
+ ]
1403
+ };
1404
+ var SETUP_TOKEN_CONFIG = {
1405
+ scopes: ["user:inference"]
1406
+ // Only inference permission, no API key creation
1407
+ };
1408
+ function generatePkce() {
1409
+ const codeVerifier = import_node_crypto.default.randomBytes(32).toString("base64url");
1410
+ const codeChallenge = import_node_crypto.default.createHash("sha256").update(codeVerifier).digest("base64url");
1411
+ const state = import_node_crypto.default.randomBytes(16).toString("hex");
1412
+ return { codeVerifier, codeChallenge, state };
1413
+ }
1414
+ function generateAuthParams() {
1415
+ const { codeVerifier, codeChallenge, state } = generatePkce();
1416
+ const params = new URLSearchParams({
1417
+ code: "true",
1418
+ client_id: CLAUDE_OAUTH_CONFIG.clientId,
1419
+ response_type: "code",
1420
+ redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
1421
+ scope: CLAUDE_OAUTH_CONFIG.scopes.join(" "),
1422
+ code_challenge: codeChallenge,
1423
+ code_challenge_method: "S256",
1424
+ state
1425
+ });
1426
+ const authUrl = `${CLAUDE_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
1427
+ return { authUrl, codeVerifier, state };
1428
+ }
1429
+ function generateSetupTokenParams() {
1430
+ const { codeVerifier, codeChallenge, state } = generatePkce();
1431
+ const params = new URLSearchParams({
1432
+ code: "true",
1433
+ client_id: CLAUDE_OAUTH_CONFIG.clientId,
1434
+ response_type: "code",
1435
+ redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
1436
+ scope: SETUP_TOKEN_CONFIG.scopes.join(" "),
1437
+ code_challenge: codeChallenge,
1438
+ code_challenge_method: "S256",
1439
+ state
1440
+ });
1441
+ const authUrl = `${CLAUDE_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
1442
+ return { authUrl, codeVerifier, state };
1443
+ }
1444
+ async function exchangeCodeForTokens(request, fetchImpl) {
1445
+ const { authorizationCode, codeVerifier, state } = request;
1446
+ const code = authorizationCode.split("#")[0]?.split("&")[0] ?? authorizationCode;
1447
+ const data = await postJson(
1448
+ fetchImpl,
1449
+ CLAUDE_OAUTH_CONFIG.tokenEndpoint,
1450
+ {
1451
+ grant_type: "authorization_code",
1452
+ client_id: CLAUDE_OAUTH_CONFIG.clientId,
1453
+ code,
1454
+ code_verifier: codeVerifier,
1455
+ redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
1456
+ state
1457
+ },
1458
+ "Failed to parse token response",
1459
+ CLAUDE_TOKEN_HEADERS
1460
+ );
1461
+ return {
1462
+ accessToken: data.access_token,
1463
+ // The authorization_code grant always returns a refresh_token; the original
1464
+ // helper read it from an untyped `data` and declared the field `string`.
1465
+ refreshToken: data.refresh_token,
1466
+ expiresIn: data.expires_in,
1467
+ scopes: data.scope?.split(" ") || CLAUDE_OAUTH_CONFIG.scopes
1468
+ };
1469
+ }
1470
+ async function exchangeSetupTokenCode(request, fetchImpl) {
1471
+ const { authorizationCode, codeVerifier, state } = request;
1472
+ const code = authorizationCode.split("#")[0]?.split("&")[0] ?? authorizationCode;
1473
+ const data = await postJson(
1474
+ fetchImpl,
1475
+ CLAUDE_OAUTH_CONFIG.tokenEndpoint,
1476
+ {
1477
+ grant_type: "authorization_code",
1478
+ client_id: CLAUDE_OAUTH_CONFIG.clientId,
1479
+ code,
1480
+ code_verifier: codeVerifier,
1481
+ redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
1482
+ state
1483
+ },
1484
+ "Failed to parse setup token response",
1485
+ CLAUDE_TOKEN_HEADERS
1486
+ );
1487
+ return {
1488
+ accessToken: data.access_token,
1489
+ expiresIn: data.expires_in,
1490
+ scopes: data.scope?.split(" ") || SETUP_TOKEN_CONFIG.scopes
1491
+ };
1492
+ }
1493
+ async function refreshAccessToken(refreshToken, fetchImpl) {
1494
+ const data = await postJson(
1495
+ fetchImpl,
1496
+ CLAUDE_OAUTH_CONFIG.tokenEndpoint,
1497
+ {
1498
+ grant_type: "refresh_token",
1499
+ client_id: CLAUDE_OAUTH_CONFIG.clientId,
1500
+ refresh_token: refreshToken
1501
+ },
1502
+ "Failed to parse refresh response",
1503
+ CLAUDE_TOKEN_HEADERS
1504
+ );
1505
+ return {
1506
+ accessToken: data.access_token,
1507
+ refreshToken: data.refresh_token || refreshToken,
1508
+ expiresIn: data.expires_in
1509
+ };
1510
+ }
1511
+
1512
+ // src/oauth/flows/codex.ts
1513
+ var codex_exports = {};
1514
+ __export(codex_exports, {
1515
+ exchangeCodeForTokens: () => exchangeCodeForTokens2,
1516
+ generateAuthParams: () => generateAuthParams2,
1517
+ refreshAccessToken: () => refreshAccessToken2
1518
+ });
1519
+ var import_node_crypto2 = __toESM(require("crypto"), 1);
1520
+ var CODEX_OAUTH_CONFIG = {
1521
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
1522
+ authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
1523
+ tokenEndpoint: "https://auth.openai.com/oauth/token",
1524
+ redirectUri: "http://localhost:1455/auth/callback",
1525
+ scopes: ["openid", "profile", "email", "offline_access"]
1526
+ };
1527
+ function generateAuthParams2() {
1528
+ const codeVerifier = import_node_crypto2.default.randomBytes(64).toString("hex");
1529
+ const codeChallenge = import_node_crypto2.default.createHash("sha256").update(codeVerifier).digest("base64url");
1530
+ const state = import_node_crypto2.default.randomBytes(16).toString("hex");
1531
+ const params = new URLSearchParams({
1532
+ response_type: "code",
1533
+ client_id: CODEX_OAUTH_CONFIG.clientId,
1534
+ redirect_uri: CODEX_OAUTH_CONFIG.redirectUri,
1535
+ scope: CODEX_OAUTH_CONFIG.scopes.join(" "),
1536
+ code_challenge: codeChallenge,
1537
+ code_challenge_method: "S256",
1538
+ state
1539
+ });
1540
+ const authUrl = `${CODEX_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
1541
+ return { authUrl, codeVerifier, state };
1542
+ }
1543
+ async function exchangeCodeForTokens2(request, fetchImpl) {
1544
+ const { authorizationCode, codeVerifier } = request;
1545
+ const params = new URLSearchParams({
1546
+ grant_type: "authorization_code",
1547
+ client_id: CODEX_OAUTH_CONFIG.clientId,
1548
+ code: authorizationCode,
1549
+ code_verifier: codeVerifier,
1550
+ redirect_uri: CODEX_OAUTH_CONFIG.redirectUri
1551
+ });
1552
+ const data = await postForm(
1553
+ fetchImpl,
1554
+ CODEX_OAUTH_CONFIG.tokenEndpoint,
1555
+ params,
1556
+ "Failed to parse token response"
1557
+ );
1558
+ return {
1559
+ accessToken: data.access_token,
1560
+ // authorization_code grant returns both; the original helper read them from
1561
+ // an untyped `data` and declared the fields `string`.
1562
+ refreshToken: data.refresh_token,
1563
+ idToken: data.id_token,
1564
+ expiresIn: data.expires_in
1565
+ };
1566
+ }
1567
+ async function refreshAccessToken2(refreshToken, fetchImpl) {
1568
+ const params = new URLSearchParams({
1569
+ grant_type: "refresh_token",
1570
+ client_id: CODEX_OAUTH_CONFIG.clientId,
1571
+ refresh_token: refreshToken,
1572
+ scope: "openid profile email"
1573
+ });
1574
+ const data = await postForm(
1575
+ fetchImpl,
1576
+ CODEX_OAUTH_CONFIG.tokenEndpoint,
1577
+ params,
1578
+ "Failed to parse refresh response"
1579
+ );
1580
+ return {
1581
+ accessToken: data.access_token,
1582
+ idToken: data.id_token,
1583
+ refreshToken: data.refresh_token || refreshToken,
1584
+ expiresIn: data.expires_in || 3600
1585
+ };
1586
+ }
1587
+
1588
+ // src/oauth/flows/gemini.ts
1589
+ var gemini_exports = {};
1590
+ __export(gemini_exports, {
1591
+ exchangeCodeForTokens: () => exchangeCodeForTokens3,
1592
+ generateAuthParams: () => generateAuthParams3,
1593
+ refreshAccessToken: () => refreshAccessToken3
1594
+ });
1595
+ var import_node_crypto3 = __toESM(require("crypto"), 1);
1596
+ var GEMINI_OAUTH_CONFIG = {
1597
+ clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
1598
+ // The Gemini CLI's *public* installed-app OAuth client secret (mirrors the
1599
+ // upstream CLI). Per Google's OAuth docs, native-app client secrets are not
1600
+ // treated as confidential — not a leaked key.
1601
+ clientSecret: "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",
1602
+ // allowlist-secret
1603
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
1604
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
1605
+ redirectUri: "urn:ietf:wg:oauth:2.0:oob",
1606
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"]
1607
+ };
1608
+ function generateAuthParams3() {
1609
+ const codeVerifier = import_node_crypto3.default.randomBytes(32).toString("base64url");
1610
+ const codeChallenge = import_node_crypto3.default.createHash("sha256").update(codeVerifier).digest("base64url");
1611
+ const state = import_node_crypto3.default.randomBytes(16).toString("hex");
1612
+ const params = new URLSearchParams({
1613
+ client_id: GEMINI_OAUTH_CONFIG.clientId,
1614
+ redirect_uri: GEMINI_OAUTH_CONFIG.redirectUri,
1615
+ scope: GEMINI_OAUTH_CONFIG.scopes.join(" "),
1616
+ response_type: "code",
1617
+ code_challenge: codeChallenge,
1618
+ code_challenge_method: "S256",
1619
+ state,
1620
+ access_type: "offline",
1621
+ prompt: "consent"
1622
+ });
1623
+ const authUrl = `${GEMINI_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
1624
+ return { authUrl, codeVerifier, state };
1625
+ }
1626
+ async function exchangeCodeForTokens3(authorizationCode, codeVerifier, fetchImpl) {
1627
+ const params = new URLSearchParams({
1628
+ grant_type: "authorization_code",
1629
+ client_id: GEMINI_OAUTH_CONFIG.clientId,
1630
+ client_secret: GEMINI_OAUTH_CONFIG.clientSecret,
1631
+ code: authorizationCode,
1632
+ code_verifier: codeVerifier,
1633
+ redirect_uri: GEMINI_OAUTH_CONFIG.redirectUri
1634
+ });
1635
+ const data = await postForm(
1636
+ fetchImpl,
1637
+ GEMINI_OAUTH_CONFIG.tokenEndpoint,
1638
+ params,
1639
+ "Failed to parse token response"
1640
+ );
1641
+ return {
1642
+ accessToken: data.access_token,
1643
+ // authorization_code grant returns a refresh_token; the original helper read
1644
+ // it from an untyped `data` and declared the field `string`.
1645
+ refreshToken: data.refresh_token,
1646
+ expiresIn: data.expires_in
1647
+ };
1648
+ }
1649
+ async function refreshAccessToken3(refreshToken, fetchImpl) {
1650
+ const params = new URLSearchParams({
1651
+ grant_type: "refresh_token",
1652
+ client_id: GEMINI_OAUTH_CONFIG.clientId,
1653
+ client_secret: GEMINI_OAUTH_CONFIG.clientSecret,
1654
+ refresh_token: refreshToken
1655
+ });
1656
+ const data = await postForm(
1657
+ fetchImpl,
1658
+ GEMINI_OAUTH_CONFIG.tokenEndpoint,
1659
+ params,
1660
+ "Failed to parse refresh response"
1661
+ );
1662
+ return {
1663
+ accessToken: data.access_token,
1664
+ expiresIn: data.expires_in
1665
+ };
1666
+ }
1667
+ // Annotate the CommonJS export names for ESM import in node:
1668
+ 0 && (module.exports = {
1669
+ SubscriptionAccountService,
1670
+ SubscriptionDispatcher,
1671
+ SubscriptionProviderRegistry,
1672
+ claudeOAuth,
1673
+ codexOAuth,
1674
+ geminiOAuth,
1675
+ getSubscriptionAccountService,
1676
+ getSubscriptionProviderRegistry,
1677
+ setSubscriptionAccountService,
1678
+ setSubscriptionProviderRegistry
1679
+ });