@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/LICENSE +21 -0
- package/NOTICE +45 -0
- package/README.md +15 -0
- package/dist/index.cjs +1679 -0
- package/dist/index.d.cts +868 -0
- package/dist/index.d.ts +868 -0
- package/dist/index.js +1639 -0
- package/dist/oauth.cjs +396 -0
- package/dist/oauth.d.cts +131 -0
- package/dist/oauth.d.ts +131 -0
- package/dist/oauth.js +363 -0
- package/package.json +58 -0
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
|
+
});
|