codex-account-orchestrator 1.0.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/account_manager.d.ts +9 -0
- package/dist/account_manager.js +103 -0
- package/dist/cli_main.d.ts +2 -0
- package/dist/cli_main.js +334 -0
- package/dist/codex_auth.d.ts +2 -0
- package/dist/codex_auth.js +29 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +20 -0
- package/dist/gateway/account_pool.d.ts +25 -0
- package/dist/gateway/account_pool.js +125 -0
- package/dist/gateway/codex_config.d.ts +6 -0
- package/dist/gateway/codex_config.js +73 -0
- package/dist/gateway/codex_shim.d.ts +7 -0
- package/dist/gateway/codex_shim.js +83 -0
- package/dist/gateway/gateway_config.d.ts +14 -0
- package/dist/gateway/gateway_config.js +46 -0
- package/dist/gateway/openai_gateway.d.ts +15 -0
- package/dist/gateway/openai_gateway.js +478 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +23 -0
- package/dist/gateway/token_utils.d.ts +18 -0
- package/dist/gateway/token_utils.js +73 -0
- package/dist/output_capture.d.ts +6 -0
- package/dist/output_capture.js +25 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +23 -0
- package/dist/process_runner.d.ts +5 -0
- package/dist/process_runner.js +40 -0
- package/dist/quota_detector.d.ts +1 -0
- package/dist/quota_detector.js +7 -0
- package/dist/registry_store.d.ts +6 -0
- package/dist/registry_store.js +26 -0
- package/package.json +55 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OpenAiGateway = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const token_utils_1 = require("./token_utils");
|
|
10
|
+
const TOKEN_REFRESH_BUFFER_SECONDS = 90;
|
|
11
|
+
class OpenAiGateway {
|
|
12
|
+
pool;
|
|
13
|
+
config;
|
|
14
|
+
inFlightRefresh = new Map();
|
|
15
|
+
constructor(pool, config) {
|
|
16
|
+
this.pool = pool;
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
async handleRequest(req, res) {
|
|
20
|
+
if (!req.url) {
|
|
21
|
+
res.writeHead(400).end("Missing URL");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const url = new url_1.URL(req.url, `http://${req.headers.host ?? "localhost"}`);
|
|
25
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
26
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
27
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const body = await readBody(req);
|
|
31
|
+
logRequestDebug(req, body);
|
|
32
|
+
const sessionKey = resolveSessionKey(req);
|
|
33
|
+
const excluded = new Set();
|
|
34
|
+
let attempts = 0;
|
|
35
|
+
while (attempts < this.config.maxRetryPasses + this.pool.getAccounts().length) {
|
|
36
|
+
attempts += 1;
|
|
37
|
+
const selected = this.selectAccount(sessionKey, excluded);
|
|
38
|
+
if (!selected) {
|
|
39
|
+
res.writeHead(429, { "content-type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify({ error: "all_accounts_exhausted" }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`Gateway: ${req.method ?? "REQ"} ${url.pathname} -> ${selected.name}\n`);
|
|
44
|
+
const response = await this.forwardRequest(selected, req, body);
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
this.pool.markSuccess(selected);
|
|
47
|
+
await streamResponse(res, response.response);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (response.quota) {
|
|
51
|
+
excluded.add(selected.name);
|
|
52
|
+
this.pool.markQuota(selected, this.config.cooldownSeconds, response.resetAtMs);
|
|
53
|
+
this.pool.clearAssignment(sessionKey);
|
|
54
|
+
process.stdout.write(`Gateway: quota hit, switching from ${selected.name}\n`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (response.authFailure) {
|
|
58
|
+
excluded.add(selected.name);
|
|
59
|
+
this.pool.markAuthFailure(selected, "auth_failed");
|
|
60
|
+
this.pool.clearAssignment(sessionKey);
|
|
61
|
+
const detail = response.bodyText ? truncate(response.bodyText, 200) : "unknown";
|
|
62
|
+
process.stdout.write(`Gateway: auth failure on ${selected.name} (${detail})\n`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(`Gateway: upstream error ${response.status} on ${selected.name}\n`);
|
|
66
|
+
await writeErrorResponse(res, response.status, response.bodyText);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
res.writeHead(500).end("gateway_exhausted");
|
|
70
|
+
}
|
|
71
|
+
selectAccount(sessionKey, excluded) {
|
|
72
|
+
const sticky = this.pool.getStickyAccount(sessionKey);
|
|
73
|
+
if (sticky && !excluded.has(sticky.name) && sticky.cooldownUntilMs <= Date.now()) {
|
|
74
|
+
return sticky;
|
|
75
|
+
}
|
|
76
|
+
const picked = this.pool.pickNextAvailable(excluded);
|
|
77
|
+
if (picked) {
|
|
78
|
+
this.pool.assignAccount(sessionKey, picked.name);
|
|
79
|
+
}
|
|
80
|
+
return picked;
|
|
81
|
+
}
|
|
82
|
+
async forwardRequest(account, req, body) {
|
|
83
|
+
const targetUrl = buildTargetUrl(this.config.baseUrl, req.url ?? "");
|
|
84
|
+
if (shouldForceQuota(account.name)) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
status: 429,
|
|
88
|
+
authFailure: false,
|
|
89
|
+
quota: true,
|
|
90
|
+
bodyText: "forced_quota"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const accessToken = this.config.overrideAuth ? await this.ensureAccessToken(account) : undefined;
|
|
94
|
+
if (this.config.overrideAuth && !accessToken) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
status: 401,
|
|
98
|
+
authFailure: true,
|
|
99
|
+
quota: false,
|
|
100
|
+
bodyText: "missing_access_token"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const attempt = async (token) => {
|
|
104
|
+
const headers = buildForwardHeaders(req, account, token, this.config);
|
|
105
|
+
return this.fetchOnce(targetUrl, req.method, headers, body);
|
|
106
|
+
};
|
|
107
|
+
let response = await attempt(accessToken);
|
|
108
|
+
if (response.authFailure && this.config.overrideAuth && account.tokens.idToken) {
|
|
109
|
+
process.stdout.write(`Gateway: access token rejected for ${account.name}, trying id_token\n`);
|
|
110
|
+
response = await attempt(account.tokens.idToken);
|
|
111
|
+
}
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
async fetchOnce(targetUrl, method, headers, body) {
|
|
115
|
+
logUpstreamDebug(method ?? "REQ", targetUrl, headers);
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(targetUrl, {
|
|
120
|
+
method,
|
|
121
|
+
headers,
|
|
122
|
+
body: body.length > 0 ? body : undefined,
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
if (response.status === 401 || response.status === 403) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
status: response.status,
|
|
130
|
+
authFailure: true,
|
|
131
|
+
quota: false,
|
|
132
|
+
bodyText: await safeReadText(response)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const errorBody = await tryReadErrorBody(response);
|
|
136
|
+
if (errorBody?.quota) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
status: response.status,
|
|
140
|
+
authFailure: false,
|
|
141
|
+
quota: true,
|
|
142
|
+
resetAtMs: errorBody.resetAtMs,
|
|
143
|
+
bodyText: errorBody.rawText
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
status: response.status,
|
|
150
|
+
authFailure: false,
|
|
151
|
+
quota: false,
|
|
152
|
+
bodyText: errorBody?.rawText ?? (await safeReadText(response))
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
status: response.status,
|
|
158
|
+
response,
|
|
159
|
+
quota: false,
|
|
160
|
+
authFailure: false
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 502,
|
|
168
|
+
authFailure: false,
|
|
169
|
+
quota: false,
|
|
170
|
+
bodyText: error instanceof Error ? error.message : "gateway_fetch_error"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async ensureAccessToken(account) {
|
|
175
|
+
if (this.pool.isTokenFresh(account, TOKEN_REFRESH_BUFFER_SECONDS)) {
|
|
176
|
+
return account.tokens.accessToken;
|
|
177
|
+
}
|
|
178
|
+
const existing = this.inFlightRefresh.get(account.name);
|
|
179
|
+
if (existing) {
|
|
180
|
+
const updated = await existing;
|
|
181
|
+
return updated.accessToken;
|
|
182
|
+
}
|
|
183
|
+
const refreshPromise = this.refreshToken(account);
|
|
184
|
+
this.inFlightRefresh.set(account.name, refreshPromise);
|
|
185
|
+
try {
|
|
186
|
+
const refreshed = await refreshPromise;
|
|
187
|
+
return refreshed.accessToken;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
this.inFlightRefresh.delete(account.name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async refreshToken(account) {
|
|
194
|
+
const response = await fetch("https://auth.openai.com/oauth/token", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
198
|
+
},
|
|
199
|
+
body: new URLSearchParams({
|
|
200
|
+
grant_type: "refresh_token",
|
|
201
|
+
refresh_token: account.tokens.refreshToken,
|
|
202
|
+
client_id: this.config.oauthClientId
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const errorText = await safeReadText(response);
|
|
207
|
+
throw new Error(`token_refresh_failed: ${errorText}`);
|
|
208
|
+
}
|
|
209
|
+
const payload = (await response.json());
|
|
210
|
+
const details = (0, token_utils_1.deriveTokenDetails)(payload.access_token, payload.id_token);
|
|
211
|
+
const updated = {
|
|
212
|
+
accessToken: payload.access_token,
|
|
213
|
+
refreshToken: payload.refresh_token,
|
|
214
|
+
idToken: payload.id_token,
|
|
215
|
+
accountId: payload.account_id ?? details.chatgptAccountId,
|
|
216
|
+
...details
|
|
217
|
+
};
|
|
218
|
+
this.pool.updateTokens(account, updated);
|
|
219
|
+
return updated;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.OpenAiGateway = OpenAiGateway;
|
|
223
|
+
function resolveSessionKey(req) {
|
|
224
|
+
const headerKeys = ["x-session-id", "openai-session", "x-openai-session", "x-request-id"];
|
|
225
|
+
for (const key of headerKeys) {
|
|
226
|
+
const value = req.headers[key];
|
|
227
|
+
if (value) {
|
|
228
|
+
return `${key}:${value}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return req.socket.remoteAddress ? `ip:${req.socket.remoteAddress}` : "default";
|
|
232
|
+
}
|
|
233
|
+
function buildForwardHeaders(req, account, accessToken, config) {
|
|
234
|
+
const headers = new Headers();
|
|
235
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
236
|
+
if (!value) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const lowerKey = key.toLowerCase();
|
|
240
|
+
if (lowerKey === "host") {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (lowerKey === "content-length") {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (config.overrideAuth && lowerKey === "authorization") {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (config.overrideAuth && lowerKey === "cookie") {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (Array.isArray(value)) {
|
|
253
|
+
headers.set(key, value.join(","));
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
headers.set(key, value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (config.overrideAuth && accessToken) {
|
|
260
|
+
headers.set("authorization", `Bearer ${accessToken}`);
|
|
261
|
+
}
|
|
262
|
+
if (config.overrideAuth) {
|
|
263
|
+
applyAccountHeaders(headers, account);
|
|
264
|
+
}
|
|
265
|
+
return headers;
|
|
266
|
+
}
|
|
267
|
+
function applyAccountHeaders(headers, account) {
|
|
268
|
+
const sessionId = account.tokens.sessionId;
|
|
269
|
+
if (sessionId) {
|
|
270
|
+
headers.set("openai-session", sessionId);
|
|
271
|
+
headers.set("x-openai-session", sessionId);
|
|
272
|
+
}
|
|
273
|
+
const accountId = account.tokens.chatgptAccountId ?? account.tokens.accountId;
|
|
274
|
+
if (accountId) {
|
|
275
|
+
headers.set("openai-account-id", accountId);
|
|
276
|
+
headers.set("x-openai-account-id", accountId);
|
|
277
|
+
}
|
|
278
|
+
const userId = account.tokens.userId ?? account.tokens.chatgptUserId;
|
|
279
|
+
if (userId) {
|
|
280
|
+
headers.set("openai-user-id", userId);
|
|
281
|
+
headers.set("x-openai-user-id", userId);
|
|
282
|
+
}
|
|
283
|
+
const organizationId = account.tokens.organizationId;
|
|
284
|
+
if (organizationId) {
|
|
285
|
+
headers.set("openai-organization", organizationId);
|
|
286
|
+
headers.set("openai-organization-id", organizationId);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function logRequestDebug(req, body) {
|
|
290
|
+
if (process.env.CAO_DEBUG_HEADERS !== "1") {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const headerLines = [];
|
|
294
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
295
|
+
if (!value) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!shouldLogHeader(key)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const rawValue = Array.isArray(value) ? value.join(",") : value;
|
|
302
|
+
headerLines.push(`${key}: ${redactHeaderValue(key, rawValue)}`);
|
|
303
|
+
}
|
|
304
|
+
if (headerLines.length > 0) {
|
|
305
|
+
process.stdout.write(`Gateway debug headers:\n${headerLines.join("\n")}\n`);
|
|
306
|
+
}
|
|
307
|
+
const authHeader = req.headers.authorization;
|
|
308
|
+
if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
|
|
309
|
+
const token = authHeader.slice("Bearer ".length);
|
|
310
|
+
const sessionId = (0, token_utils_1.parseJwtSessionId)(token);
|
|
311
|
+
if (sessionId) {
|
|
312
|
+
process.stdout.write(`Gateway debug incoming session_id: ${sessionId}\n`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (process.env.CAO_DEBUG_BODY === "1") {
|
|
316
|
+
const snippet = formatBodySnippet(body);
|
|
317
|
+
process.stdout.write(`Gateway debug body (${body.length} bytes): ${snippet}\n`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
process.stdout.write(`Gateway debug body length: ${body.length} bytes\n`);
|
|
321
|
+
}
|
|
322
|
+
if (process.env.CAO_CAPTURE_BODY === "1") {
|
|
323
|
+
const capturePath = process.env.CAO_CAPTURE_BODY_PATH ?? "/tmp/cao-last-body.json";
|
|
324
|
+
try {
|
|
325
|
+
fs_1.default.writeFileSync(capturePath, body);
|
|
326
|
+
process.stdout.write(`Gateway debug body saved: ${capturePath}\n`);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
process.stdout.write(`Gateway debug body save failed: ${error instanceof Error ? error.message : "unknown"}\n`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function logUpstreamDebug(method, url, headers) {
|
|
334
|
+
if (process.env.CAO_DEBUG_HEADERS !== "1") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const headerLines = [];
|
|
338
|
+
headers.forEach((value, key) => {
|
|
339
|
+
if (!shouldLogHeader(key)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
headerLines.push(`${key}: ${redactHeaderValue(key, value)}`);
|
|
343
|
+
});
|
|
344
|
+
process.stdout.write(`Gateway debug upstream ${method} ${url.toString()}\n`);
|
|
345
|
+
if (headerLines.length > 0) {
|
|
346
|
+
process.stdout.write(`${headerLines.join("\n")}\n`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function shouldLogHeader(key) {
|
|
350
|
+
const lower = key.toLowerCase();
|
|
351
|
+
return (lower.includes("openai") ||
|
|
352
|
+
lower.includes("authorization") ||
|
|
353
|
+
lower.includes("session") ||
|
|
354
|
+
lower === "user-agent" ||
|
|
355
|
+
lower === "content-type");
|
|
356
|
+
}
|
|
357
|
+
function redactHeaderValue(key, value) {
|
|
358
|
+
const lower = key.toLowerCase();
|
|
359
|
+
if (lower.includes("authorization") || lower.includes("token")) {
|
|
360
|
+
return redactValue(value);
|
|
361
|
+
}
|
|
362
|
+
if (lower.includes("cookie")) {
|
|
363
|
+
return "<redacted>";
|
|
364
|
+
}
|
|
365
|
+
if (lower.includes("session")) {
|
|
366
|
+
return redactValue(value);
|
|
367
|
+
}
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
function redactValue(value) {
|
|
371
|
+
if (value.length <= 12) {
|
|
372
|
+
return "<redacted>";
|
|
373
|
+
}
|
|
374
|
+
return `${value.slice(0, 6)}…${value.slice(-4)}`;
|
|
375
|
+
}
|
|
376
|
+
function formatBodySnippet(body) {
|
|
377
|
+
const text = body.toString("utf8");
|
|
378
|
+
const sanitized = text.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, "?");
|
|
379
|
+
return truncate(sanitized, 400);
|
|
380
|
+
}
|
|
381
|
+
function shouldForceQuota(accountName) {
|
|
382
|
+
const forced = process.env.CAO_FORCE_QUOTA_ACCOUNTS;
|
|
383
|
+
if (!forced) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
return forced
|
|
387
|
+
.split(",")
|
|
388
|
+
.map((name) => name.trim())
|
|
389
|
+
.filter(Boolean)
|
|
390
|
+
.includes(accountName);
|
|
391
|
+
}
|
|
392
|
+
async function readBody(req) {
|
|
393
|
+
const chunks = [];
|
|
394
|
+
for await (const chunk of req) {
|
|
395
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
396
|
+
}
|
|
397
|
+
return Buffer.concat(chunks);
|
|
398
|
+
}
|
|
399
|
+
async function streamResponse(res, upstream) {
|
|
400
|
+
const headers = {};
|
|
401
|
+
upstream.headers.forEach((value, key) => {
|
|
402
|
+
headers[key] = value;
|
|
403
|
+
});
|
|
404
|
+
res.writeHead(upstream.status, headers);
|
|
405
|
+
if (!upstream.body) {
|
|
406
|
+
res.end();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const reader = upstream.body.getReader();
|
|
410
|
+
while (true) {
|
|
411
|
+
const { done, value } = await reader.read();
|
|
412
|
+
if (done) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
if (value) {
|
|
416
|
+
res.write(Buffer.from(value));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
res.end();
|
|
420
|
+
}
|
|
421
|
+
async function safeReadText(response) {
|
|
422
|
+
try {
|
|
423
|
+
return await response.text();
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return "";
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function writeErrorResponse(res, status, bodyText) {
|
|
430
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
431
|
+
res.end(bodyText ?? "{}");
|
|
432
|
+
}
|
|
433
|
+
function truncate(value, maxLength) {
|
|
434
|
+
if (value.length <= maxLength) {
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
return `${value.slice(0, maxLength)}...`;
|
|
438
|
+
}
|
|
439
|
+
function buildTargetUrl(baseUrl, requestPath) {
|
|
440
|
+
const base = new url_1.URL(baseUrl);
|
|
441
|
+
const requestUrl = new url_1.URL(requestPath, "http://localhost");
|
|
442
|
+
const basePath = base.pathname.endsWith("/") ? base.pathname.slice(0, -1) : base.pathname;
|
|
443
|
+
const requestPathname = requestUrl.pathname.startsWith("/")
|
|
444
|
+
? requestUrl.pathname
|
|
445
|
+
: `/${requestUrl.pathname}`;
|
|
446
|
+
base.pathname = `${basePath}${requestPathname}`;
|
|
447
|
+
base.search = requestUrl.search;
|
|
448
|
+
if (baseUrl.includes("chatgpt.com/backend-api/codex")) {
|
|
449
|
+
if (base.pathname.startsWith("/backend-api/codex/v1/responses")) {
|
|
450
|
+
base.pathname = "/backend-api/codex/responses/compact";
|
|
451
|
+
base.search = "";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return base;
|
|
455
|
+
}
|
|
456
|
+
async function tryReadErrorBody(response) {
|
|
457
|
+
if (response.ok) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
const rawText = await safeReadText(response);
|
|
461
|
+
try {
|
|
462
|
+
const parsed = JSON.parse(rawText);
|
|
463
|
+
if (parsed.error?.type === "usage_limit_reached") {
|
|
464
|
+
const resetAtMs = parsed.error.resets_at ? parsed.error.resets_at * 1000 : undefined;
|
|
465
|
+
return { quota: true, resetAtMs, rawText };
|
|
466
|
+
}
|
|
467
|
+
if (response.status === 429) {
|
|
468
|
+
const resetAtMs = parsed.error?.resets_at ? parsed.error.resets_at * 1000 : undefined;
|
|
469
|
+
return { quota: true, resetAtMs, rawText };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
if (response.status === 429) {
|
|
474
|
+
return { quota: true, rawText };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { quota: false, rawText };
|
|
478
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.startGatewayServer = startGatewayServer;
|
|
7
|
+
const http_1 = __importDefault(require("http"));
|
|
8
|
+
const openai_gateway_1 = require("./openai_gateway");
|
|
9
|
+
function startGatewayServer(pool, config) {
|
|
10
|
+
const gateway = new openai_gateway_1.OpenAiGateway(pool, config);
|
|
11
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
await gateway.handleRequest(req, res);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : "gateway_error";
|
|
17
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
18
|
+
res.end(message);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
server.listen(config.port, config.bindAddress);
|
|
22
|
+
return server;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface TokenDetails {
|
|
2
|
+
expiresAtMs?: number;
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
chatgptAccountId?: string;
|
|
5
|
+
chatgptUserId?: string;
|
|
6
|
+
userId?: string;
|
|
7
|
+
organizationId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenPair extends TokenDetails {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
refreshToken: string;
|
|
12
|
+
idToken?: string;
|
|
13
|
+
accountId?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseJwtExpiry(token: string): number | undefined;
|
|
16
|
+
export declare function parseJwtSessionId(token: string): string | undefined;
|
|
17
|
+
export declare function deriveTokenDetails(accessToken: string, idToken?: string): TokenDetails;
|
|
18
|
+
export declare function isTokenFresh(expiresAtMs: number | undefined, bufferSeconds: number): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseJwtExpiry = parseJwtExpiry;
|
|
4
|
+
exports.parseJwtSessionId = parseJwtSessionId;
|
|
5
|
+
exports.deriveTokenDetails = deriveTokenDetails;
|
|
6
|
+
exports.isTokenFresh = isTokenFresh;
|
|
7
|
+
function parseJwtExpiry(token) {
|
|
8
|
+
const payload = parseJwtPayload(token);
|
|
9
|
+
if (!payload || typeof payload.exp !== "number") {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return payload.exp * 1000;
|
|
13
|
+
}
|
|
14
|
+
function parseJwtSessionId(token) {
|
|
15
|
+
const payload = parseJwtPayload(token);
|
|
16
|
+
if (!payload) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (typeof payload.session_id === "string") {
|
|
20
|
+
return payload.session_id;
|
|
21
|
+
}
|
|
22
|
+
if (typeof payload.sid === "string") {
|
|
23
|
+
return payload.sid;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function deriveTokenDetails(accessToken, idToken) {
|
|
28
|
+
const payload = parseJwtPayload(accessToken);
|
|
29
|
+
const details = {
|
|
30
|
+
expiresAtMs: parseJwtExpiry(accessToken),
|
|
31
|
+
sessionId: parseJwtSessionId(accessToken)
|
|
32
|
+
};
|
|
33
|
+
const authPayload = payload?.["https://api.openai.com/auth"];
|
|
34
|
+
if (authPayload) {
|
|
35
|
+
details.chatgptAccountId = authPayload.chatgpt_account_id;
|
|
36
|
+
details.chatgptUserId = authPayload.chatgpt_user_id;
|
|
37
|
+
details.userId = authPayload.user_id;
|
|
38
|
+
}
|
|
39
|
+
const organizationId = parseOrganizationId(idToken ?? accessToken);
|
|
40
|
+
if (organizationId) {
|
|
41
|
+
details.organizationId = organizationId;
|
|
42
|
+
}
|
|
43
|
+
return details;
|
|
44
|
+
}
|
|
45
|
+
function parseJwtPayload(token) {
|
|
46
|
+
const parts = token.split(".");
|
|
47
|
+
if (parts.length < 2) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function parseOrganizationId(token) {
|
|
58
|
+
const payload = parseJwtPayload(token);
|
|
59
|
+
const authPayload = payload?.["https://api.openai.com/auth"];
|
|
60
|
+
const organizations = authPayload?.organizations;
|
|
61
|
+
if (!Array.isArray(organizations) || organizations.length === 0) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const defaultOrg = organizations.find((org) => org.is_default);
|
|
65
|
+
return defaultOrg?.id ?? organizations[0]?.id;
|
|
66
|
+
}
|
|
67
|
+
function isTokenFresh(expiresAtMs, bufferSeconds) {
|
|
68
|
+
if (!expiresAtMs) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
return expiresAtMs - now > bufferSeconds * 1000;
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OutputCapture = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
class OutputCapture {
|
|
6
|
+
lines = [];
|
|
7
|
+
partial = "";
|
|
8
|
+
addChunk(chunk) {
|
|
9
|
+
const text = chunk instanceof Buffer ? chunk.toString("utf8") : chunk;
|
|
10
|
+
const combined = this.partial + text;
|
|
11
|
+
const parts = combined.split(/\r?\n/);
|
|
12
|
+
this.partial = parts.pop() ?? "";
|
|
13
|
+
for (const line of parts) {
|
|
14
|
+
this.lines.push(line);
|
|
15
|
+
}
|
|
16
|
+
if (this.lines.length > constants_1.MAX_CAPTURED_LINES) {
|
|
17
|
+
this.lines.splice(0, this.lines.length - constants_1.MAX_CAPTURED_LINES);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
getText() {
|
|
21
|
+
const allLines = this.partial.length > 0 ? [...this.lines, this.partial] : this.lines;
|
|
22
|
+
return allLines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.OutputCapture = OutputCapture;
|
package/dist/paths.d.ts
ADDED
package/dist/paths.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getBaseDir = getBaseDir;
|
|
7
|
+
exports.getAccountDir = getAccountDir;
|
|
8
|
+
exports.getRegistryPath = getRegistryPath;
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
function getBaseDir(dataDir) {
|
|
13
|
+
if (dataDir && dataDir.trim().length > 0) {
|
|
14
|
+
return dataDir;
|
|
15
|
+
}
|
|
16
|
+
return path_1.default.join(os_1.default.homedir(), ".codex-account-orchestrator");
|
|
17
|
+
}
|
|
18
|
+
function getAccountDir(baseDir, accountName) {
|
|
19
|
+
return path_1.default.join(baseDir, accountName);
|
|
20
|
+
}
|
|
21
|
+
function getRegistryPath(baseDir) {
|
|
22
|
+
return path_1.default.join(baseDir, constants_1.REGISTRY_FILE_NAME);
|
|
23
|
+
}
|