@bitkyc08/opencodex 2.6.0 → 2.6.1
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/gui/dist/assets/index-BmHrbTmO.js +9 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/codex-account-usability.ts +5 -0
- package/src/codex-auth-api.ts +19 -3
- package/src/codex-auth-context.ts +21 -3
- package/src/codex-main-account.ts +46 -0
- package/src/codex-routing.ts +19 -1
- package/src/codex-websocket-registry.ts +1 -1
- package/src/crash-guard.ts +82 -0
- package/src/provider-label.ts +4 -0
- package/src/server.ts +10 -4
- package/src/usage-summary.ts +0 -0
- package/gui/dist/assets/index-BS4X1QDi.js +0 -9
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BmHrbTmO.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BwvDb198.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
writeRuntimePort,
|
|
18
18
|
} from "./config";
|
|
19
19
|
import { collectStatus } from "./cli-status";
|
|
20
|
+
import { installCrashGuards } from "./crash-guard";
|
|
20
21
|
import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./cli-help";
|
|
21
22
|
import { findAvailablePort, shouldPersistSelectedPort } from "./ports";
|
|
22
23
|
import { killProxy } from "./process-control";
|
|
@@ -136,6 +137,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
136
137
|
const port = await chooseListenPort(requestedPort);
|
|
137
138
|
|
|
138
139
|
const server = startServer(port);
|
|
140
|
+
// A single request's streaming error must never crash the daemon serving every
|
|
141
|
+
// other Codex session — capture the full stack to crash.log and stay up.
|
|
142
|
+
installCrashGuards();
|
|
139
143
|
writePid(process.pid);
|
|
140
144
|
|
|
141
145
|
const config = loadConfig();
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { getCodexAccountCredential } from "./codex-account-store";
|
|
2
2
|
import { isAccountNeedsReauth } from "./codex-account-runtime-state";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID, isMainAccountTokenLive } from "./codex-main-account";
|
|
3
4
|
import type { OcxConfig } from "./types";
|
|
4
5
|
|
|
5
6
|
export function isCodexAccountUsable(config: OcxConfig, accountId: string): boolean {
|
|
7
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
8
|
+
// Main account: credential is the read-only ~/.codex/auth.json token (Option A).
|
|
9
|
+
return isMainAccountTokenLive() && !isAccountNeedsReauth(accountId);
|
|
10
|
+
}
|
|
6
11
|
const exists = (config.codexAccounts ?? []).some(account => !account.isMain && account.id === accountId);
|
|
7
12
|
if (!exists) return false;
|
|
8
13
|
if (isAccountNeedsReauth(accountId)) return false;
|
package/src/codex-auth-api.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./codex-quota";
|
|
25
25
|
export { clearAccountQuota, getAccountQuota, parseUsageQuota, updateAccountQuota } from "./codex-quota";
|
|
26
26
|
import { extractAccountId, decodeJwtPayload } from "./oauth/chatgpt";
|
|
27
|
+
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./codex-main-account";
|
|
27
28
|
import { maskEmail } from "./privacy";
|
|
28
29
|
export { maskEmail } from "./privacy";
|
|
29
30
|
import type { CodexAccount, OcxConfig } from "./types";
|
|
@@ -88,7 +89,7 @@ async function resolveResetCreditAuth(
|
|
|
88
89
|
| { ok: true; isMain: boolean; accessToken: string; chatgptAccountId: string }
|
|
89
90
|
| { ok: false; response: Response }
|
|
90
91
|
> {
|
|
91
|
-
if (accountId ===
|
|
92
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
92
93
|
const tokens = readCodexTokens();
|
|
93
94
|
if (!tokens) return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
94
95
|
return { ok: true, isMain: true, accessToken: tokens.access_token, chatgptAccountId: tokens.account_id };
|
|
@@ -202,6 +203,21 @@ async function fetchMainAccountInfo(forceRefresh = false): Promise<{ email: stri
|
|
|
202
203
|
ts: Date.now(),
|
|
203
204
|
};
|
|
204
205
|
mainAccountCache = result;
|
|
206
|
+
// Mirror main quota + plan into the shared stores so the rotation engine can
|
|
207
|
+
// score and auto-switch the main account exactly like a pool account (Option A).
|
|
208
|
+
setMainAccountPlan(result.plan);
|
|
209
|
+
if (result.quota) {
|
|
210
|
+
updateAccountQuota(
|
|
211
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
212
|
+
result.quota.weeklyPercent,
|
|
213
|
+
result.quota.fiveHourPercent,
|
|
214
|
+
result.quota.weeklyResetAt,
|
|
215
|
+
result.quota.fiveHourResetAt,
|
|
216
|
+
result.quota.monthlyPercent,
|
|
217
|
+
result.quota.monthlyResetAt,
|
|
218
|
+
result.quota.resetCredits,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
205
221
|
return result;
|
|
206
222
|
} catch {
|
|
207
223
|
return { email: null, plan: null, quota: null };
|
|
@@ -265,7 +281,7 @@ export async function handleCodexAuthAPI(
|
|
|
265
281
|
return poolAccountDto(a, quotaResult, !!cred);
|
|
266
282
|
});
|
|
267
283
|
const main = {
|
|
268
|
-
id:
|
|
284
|
+
id: MAIN_CODEX_ACCOUNT_ID,
|
|
269
285
|
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
270
286
|
plan: mainInfo.plan,
|
|
271
287
|
isMain: true,
|
|
@@ -329,7 +345,7 @@ export async function handleCodexAuthAPI(
|
|
|
329
345
|
let body: { accountId: string | null };
|
|
330
346
|
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
331
347
|
const runtimeConfig = getRuntimeConfig(config);
|
|
332
|
-
if (body.accountId != null) {
|
|
348
|
+
if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
333
349
|
const exists = (runtimeConfig.codexAccounts ?? []).some(a => a.id === body.accountId);
|
|
334
350
|
if (!exists) return jsonResponse({ error: "Account not found" }, 400);
|
|
335
351
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./codex-account-store";
|
|
7
7
|
import { markAccountNeedsReauth } from "./codex-account-runtime-state";
|
|
8
8
|
import { isCodexAccountUsable } from "./codex-account-usability";
|
|
9
|
+
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./codex-main-account";
|
|
9
10
|
import { getCodexAccountCooldownUntil, resolveCodexAccountForThreadDetailed } from "./codex-routing";
|
|
10
11
|
import type { OcxConfig, OcxProviderConfig } from "./types";
|
|
11
12
|
import { FORWARD_HEADERS } from "./adapters/openai-responses";
|
|
@@ -18,6 +19,14 @@ export type CodexAuthContext =
|
|
|
18
19
|
generation: number;
|
|
19
20
|
accessToken: string;
|
|
20
21
|
chatgptAccountId: string;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
// Main Codex account participating in rotation: token injected from ~/.codex/auth.json
|
|
25
|
+
// (Option A). Distinct from "main" (passthrough fallback that forwards the client token).
|
|
26
|
+
kind: "main-pool";
|
|
27
|
+
accountId: string;
|
|
28
|
+
accessToken: string;
|
|
29
|
+
chatgptAccountId: string;
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
export type OcxRuntimeProviderConfig = OcxProviderConfig & {
|
|
@@ -70,6 +79,14 @@ export async function resolveCodexAuthContext(headers: Headers, config: OcxConfi
|
|
|
70
79
|
const cooldownUntil = getCodexAccountCooldownUntil(accountId);
|
|
71
80
|
if (cooldownUntil) throw new CodexAccountCooldownError(accountId, cooldownUntil);
|
|
72
81
|
|
|
82
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
83
|
+
// Main account in rotation: inject the read-only auth.json token. If the token vanished
|
|
84
|
+
// since selection, fall back to passthrough rather than failing the request.
|
|
85
|
+
const token = getMainAccountToken();
|
|
86
|
+
if (!token) return { kind: "main", accountId: null };
|
|
87
|
+
return { kind: "main-pool", accountId, accessToken: token.accessToken, chatgptAccountId: token.chatgptAccountId };
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
try {
|
|
74
91
|
const token = await getValidCodexToken(accountId);
|
|
75
92
|
return {
|
|
@@ -88,7 +105,7 @@ export async function resolveCodexAuthContext(headers: Headers, config: OcxConfi
|
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
export function assertCodexAuthContextNotCooled(ctx: CodexAuthContext | undefined): void {
|
|
91
|
-
if (ctx?.kind !== "pool") return;
|
|
108
|
+
if (ctx?.kind !== "pool" && ctx?.kind !== "main-pool") return;
|
|
92
109
|
const cooldownUntil = getCodexAccountCooldownUntil(ctx.accountId);
|
|
93
110
|
if (cooldownUntil) throw new CodexAccountCooldownError(ctx.accountId, cooldownUntil);
|
|
94
111
|
}
|
|
@@ -97,7 +114,7 @@ export function applyCodexAuthContextToProvider(
|
|
|
97
114
|
provider: OcxProviderConfig,
|
|
98
115
|
ctx: CodexAuthContext,
|
|
99
116
|
): OcxRuntimeProviderConfig {
|
|
100
|
-
if (ctx.kind !== "pool" || provider.authMode !== "forward") return provider;
|
|
117
|
+
if ((ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
|
|
101
118
|
return {
|
|
102
119
|
...provider,
|
|
103
120
|
_codexAccountOverride: {
|
|
@@ -114,7 +131,7 @@ export function headersForCodexAuthContext(headers: Headers, ctx: CodexAuthConte
|
|
|
114
131
|
const value = headers.get(name);
|
|
115
132
|
if (value) selected.set(name, value);
|
|
116
133
|
}
|
|
117
|
-
if (ctx.kind === "pool") {
|
|
134
|
+
if (ctx.kind === "pool" || ctx.kind === "main-pool") {
|
|
118
135
|
selected.set("authorization", `Bearer ${ctx.accessToken}`);
|
|
119
136
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
120
137
|
}
|
|
@@ -123,6 +140,7 @@ export function headersForCodexAuthContext(headers: Headers, ctx: CodexAuthConte
|
|
|
123
140
|
|
|
124
141
|
export function isCodexAuthContextUsable(ctx: CodexAuthContext, config: OcxConfig): boolean {
|
|
125
142
|
if (ctx.kind === "main") return true;
|
|
143
|
+
if (ctx.kind === "main-pool") return isCodexAccountUsable(config, ctx.accountId);
|
|
126
144
|
return isCodexAccountUsable(config, ctx.accountId) && isCodexAccountGenerationLive(ctx.accountId, ctx.generation);
|
|
127
145
|
}
|
|
128
146
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readCodexTokens } from "./codex-auth-collision";
|
|
2
|
+
import { decodeJwtPayload } from "./oauth/chatgpt";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stable id under which the "main" Codex account (the Codex CLI login stored in
|
|
6
|
+
* ~/.codex/auth.json) participates in opencodex's account rotation. The main account is
|
|
7
|
+
* NOT imported into the managed credential store (Option A): its token is read-only from
|
|
8
|
+
* auth.json, so opencodex never refreshes it — an expired token surfaces as a reauth
|
|
9
|
+
* notice (re-login via the Codex CLI) rather than a background refresh.
|
|
10
|
+
*/
|
|
11
|
+
export const MAIN_CODEX_ACCOUNT_ID = "__main__";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Main account plan (e.g. "plus", "go", "free", "team"), populated from the WHAM usage
|
|
15
|
+
* fetch. Used by the rotation usage-score so go/free main accounts score on monthly
|
|
16
|
+
* percent, matching pool-account behavior.
|
|
17
|
+
*/
|
|
18
|
+
let mainAccountPlan: string | null = null;
|
|
19
|
+
|
|
20
|
+
export function setMainAccountPlan(plan: string | null): void {
|
|
21
|
+
mainAccountPlan = plan;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getMainAccountPlan(): string | undefined {
|
|
25
|
+
return mainAccountPlan ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read-only main account token from ~/.codex/auth.json, or null when not logged in. */
|
|
29
|
+
export function getMainAccountToken(): { accessToken: string; chatgptAccountId: string } | null {
|
|
30
|
+
const tokens = readCodexTokens();
|
|
31
|
+
if (!tokens?.access_token) return null;
|
|
32
|
+
return { accessToken: tokens.access_token, chatgptAccountId: tokens.account_id };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The main token is usable when it exists and — if its JWT carries a decodable `exp` — is
|
|
37
|
+
* not expired. When `exp` cannot be decoded we treat the token as live (best-effort); an
|
|
38
|
+
* actually-invalid token then surfaces via the upstream 401 → cooldown path.
|
|
39
|
+
*/
|
|
40
|
+
export function isMainAccountTokenLive(now = Date.now()): boolean {
|
|
41
|
+
const tokens = readCodexTokens();
|
|
42
|
+
if (!tokens?.access_token) return false;
|
|
43
|
+
const payload = decodeJwtPayload(tokens.access_token);
|
|
44
|
+
const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : undefined;
|
|
45
|
+
return exp === undefined || exp > now;
|
|
46
|
+
}
|
package/src/codex-routing.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { codexAccountLogLabel } from "./codex-account-label";
|
|
|
4
4
|
import { isCodexAccountUsable } from "./codex-account-usability";
|
|
5
5
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./codex-account-runtime-state";
|
|
6
6
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./codex-quota";
|
|
7
|
+
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "./codex-main-account";
|
|
7
8
|
import type { OcxConfig } from "./types";
|
|
8
9
|
|
|
9
10
|
type ThreadAffinityEntry = {
|
|
@@ -43,6 +44,7 @@ export type CodexUpstreamOutcomeMeta = {
|
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
function hasConfiguredPoolAccount(config: OcxConfig, accountId: string): boolean {
|
|
47
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return isCodexAccountUsable(config, accountId);
|
|
46
48
|
return (config.codexAccounts ?? []).some(account => !account.isMain && account.id === accountId);
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -203,14 +205,26 @@ function bindThreadAffinity(threadId: string, accountId: string, now: number): v
|
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
function getEligiblePoolAccounts(config: OcxConfig, excludeId?: string, now = Date.now()): string[] {
|
|
206
|
-
|
|
208
|
+
const ids = (config.codexAccounts ?? [])
|
|
207
209
|
.filter(account => !account.isMain && account.id !== excludeId && !isAccountNeedsReauth(account.id))
|
|
208
210
|
.filter(account => !isCodexAccountInCooldown(account.id, now))
|
|
209
211
|
.filter(account => isCodexAccountUsable(config, account.id))
|
|
210
212
|
.map(account => account.id);
|
|
213
|
+
// The main Codex account is not stored in config.codexAccounts; include it as a
|
|
214
|
+
// first-class rotation candidate when its read-only token is usable (Option A).
|
|
215
|
+
if (
|
|
216
|
+
excludeId !== MAIN_CODEX_ACCOUNT_ID
|
|
217
|
+
&& !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
|
|
218
|
+
&& !isCodexAccountInCooldown(MAIN_CODEX_ACCOUNT_ID, now)
|
|
219
|
+
&& isCodexAccountUsable(config, MAIN_CODEX_ACCOUNT_ID)
|
|
220
|
+
) {
|
|
221
|
+
ids.unshift(MAIN_CODEX_ACCOUNT_ID);
|
|
222
|
+
}
|
|
223
|
+
return ids;
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
function getPoolAccountPlan(config: OcxConfig, accountId: string): string | undefined {
|
|
227
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return getMainAccountPlan();
|
|
214
228
|
return (config.codexAccounts ?? []).find(account => !account.isMain && account.id === accountId)?.plan;
|
|
215
229
|
}
|
|
216
230
|
|
|
@@ -387,6 +401,10 @@ export function recordCodexUpstreamOutcome(
|
|
|
387
401
|
|
|
388
402
|
export function formatCodexProviderForLog(providerName: string, accountId: string | null, config: OcxConfig): string {
|
|
389
403
|
if (!accountId) return providerName;
|
|
404
|
+
// The main Codex login participates in rotation as "main-pool" (MAIN_CODEX_ACCOUNT_ID) but is the
|
|
405
|
+
// same physical account as the "main" passthrough (null accountId). Log both under the base provider
|
|
406
|
+
// name so usage/tokens aggregate into a single row instead of splitting into `chatgpt` + `chatgpt-main`.
|
|
407
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) return providerName;
|
|
390
408
|
const account = (config.codexAccounts ?? []).find(a => !a.isMain && a.id === accountId);
|
|
391
409
|
return account ? `${providerName}-${codexAccountLogLabel(account)}` : providerName;
|
|
392
410
|
}
|
|
@@ -5,7 +5,7 @@ const socketsByAccount = new Map<string, Set<ServerWebSocket<WsData>>>();
|
|
|
5
5
|
|
|
6
6
|
function trackedAccountId(ws: ServerWebSocket<WsData>): string | null {
|
|
7
7
|
const ctx = ws.data.authContext;
|
|
8
|
-
return ctx?.kind === "pool" ? ctx.accountId : null;
|
|
8
|
+
return ctx?.kind === "pool" || ctx?.kind === "main-pool" ? ctx.accountId : null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function addSocketForAccount(accountId: string | null, ws: ServerWebSocket<WsData>): void {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "./config";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Process-level safety net for the long-running proxy daemon.
|
|
7
|
+
*
|
|
8
|
+
* A single request can trigger an async error inside a Bun.serve streaming
|
|
9
|
+
* handler (e.g. a ReadableStream `start(controller)` callback hitting an
|
|
10
|
+
* unexpected upstream response shape). Without a handler, Bun's default
|
|
11
|
+
* behaviour prints the raw error — shown as `(function (controller, error)
|
|
12
|
+
* {"use strict"; ... TypeError: null is not an object` — and can tear down
|
|
13
|
+
* the whole proxy, killing every other in-flight Codex session.
|
|
14
|
+
*
|
|
15
|
+
* We must NOT let one bad stream crash the daemon. These handlers:
|
|
16
|
+
* 1. Append the full error + stack to `<configDir>/crash.log` so the exact
|
|
17
|
+
* fault (with the JSC `(evaluating 'x.y')` clause and file:line) is
|
|
18
|
+
* captured for a precise root-cause fix.
|
|
19
|
+
* 2. Keep the process alive — the failed request is already isolated by
|
|
20
|
+
* Bun.serve; surviving is strictly better than terminating.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
let installed = false;
|
|
24
|
+
|
|
25
|
+
function crashLogPath(): string {
|
|
26
|
+
const dir = getConfigDir();
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(dir, { recursive: true });
|
|
29
|
+
} catch {
|
|
30
|
+
/* best-effort: directory usually already exists */
|
|
31
|
+
}
|
|
32
|
+
return join(dir, "crash.log");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatError(kind: string, err: unknown): string {
|
|
36
|
+
const ts = new Date().toISOString();
|
|
37
|
+
const detail =
|
|
38
|
+
err instanceof Error
|
|
39
|
+
? `${err.name}: ${err.message}\n${err.stack ?? "(no stack)"}`
|
|
40
|
+
: typeof err === "object"
|
|
41
|
+
? safeStringify(err)
|
|
42
|
+
: String(err);
|
|
43
|
+
return `\n[${ts}] ${kind}\n${detail}\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function safeStringify(value: unknown): string {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
49
|
+
} catch {
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function record(kind: string, err: unknown): void {
|
|
55
|
+
const line = formatError(kind, err);
|
|
56
|
+
// Always surface to stderr so foreground `ocx start` users still see it,
|
|
57
|
+
// then persist for later diagnosis.
|
|
58
|
+
console.error(`⚠️ ${kind} (proxy stayed up; logged to crash.log)`);
|
|
59
|
+
console.error(line.trimStart());
|
|
60
|
+
try {
|
|
61
|
+
appendFileSync(crashLogPath(), line);
|
|
62
|
+
} catch {
|
|
63
|
+
/* logging must never throw */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register global handlers that keep the proxy alive and capture full stacks.
|
|
69
|
+
* Idempotent: safe to call more than once.
|
|
70
|
+
*/
|
|
71
|
+
export function installCrashGuards(): void {
|
|
72
|
+
if (installed) return;
|
|
73
|
+
installed = true;
|
|
74
|
+
|
|
75
|
+
process.on("unhandledRejection", reason => {
|
|
76
|
+
record("unhandledRejection", reason);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
process.on("uncaughtException", err => {
|
|
80
|
+
record("uncaughtException", err);
|
|
81
|
+
});
|
|
82
|
+
}
|
package/src/provider-label.ts
CHANGED
|
@@ -4,5 +4,9 @@ export function baseProviderLabel(provider: string): string {
|
|
|
4
4
|
const cut = provider.lastIndexOf("-");
|
|
5
5
|
if (cut <= 0) return provider;
|
|
6
6
|
const suffix = provider.slice(cut + 1);
|
|
7
|
+
// `-main` is the legacy log label for the main Codex account (MAIN_CODEX_ACCOUNT_ID). New entries
|
|
8
|
+
// log under the base provider name, but historical `<provider>-main` entries must still collapse so
|
|
9
|
+
// the usage table aggregates the main account into a single row instead of a stray split.
|
|
10
|
+
if (suffix === "main") return provider.slice(0, cut);
|
|
7
11
|
return CODEX_ACCOUNT_LOG_LABEL_RE.test(suffix) ? provider.slice(0, cut) : provider;
|
|
8
12
|
}
|
package/src/server.ts
CHANGED
|
@@ -298,16 +298,22 @@ export function resolveAdapter(providerConfig: OcxProviderConfig) {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
function sidecarOutcomeRecorder(config: OcxConfig, authCtx: CodexAuthContext): ((outcome: CodexUpstreamOutcome) => void) | undefined {
|
|
301
|
-
return authCtx.kind === "pool"
|
|
301
|
+
return authCtx.kind === "pool" || authCtx.kind === "main-pool"
|
|
302
302
|
? outcome => recordCodexUpstreamOutcome(config, authCtx.accountId, outcome)
|
|
303
303
|
: undefined;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/** Account id to attribute log labels / upstream outcomes to (pool + rotation-injected main). */
|
|
307
|
+
function codexLogAccountId(authCtx: CodexAuthContext): string | null {
|
|
308
|
+
return authCtx.kind === "pool" || authCtx.kind === "main-pool" ? authCtx.accountId : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
306
311
|
function usesCodexForwardPoolAuth(
|
|
307
312
|
authCtx: CodexAuthContext,
|
|
308
313
|
provider: OcxProviderConfig,
|
|
309
|
-
): authCtx is Extract<CodexAuthContext, { kind: "pool" }> {
|
|
310
|
-
return authCtx.kind === "pool"
|
|
314
|
+
): authCtx is Extract<CodexAuthContext, { kind: "pool" | "main-pool" }> {
|
|
315
|
+
return (authCtx.kind === "pool" || authCtx.kind === "main-pool")
|
|
316
|
+
&& provider.authMode === "forward" && provider.adapter === "openai-responses";
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
function codexForwardTerminalOutcomeRecorder(
|
|
@@ -399,7 +405,7 @@ async function handleResponses(
|
|
|
399
405
|
return formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication");
|
|
400
406
|
}
|
|
401
407
|
route.provider = applyCodexAuthContextToProvider(route.provider, authCtx);
|
|
402
|
-
logCtx.provider = formatCodexProviderForLog(route.providerName, authCtx
|
|
408
|
+
logCtx.provider = formatCodexProviderForLog(route.providerName, codexLogAccountId(authCtx), config);
|
|
403
409
|
|
|
404
410
|
// OAuth providers: swap in a fresh access token (auto-refreshed) as the Bearer key, so the
|
|
405
411
|
// existing openai-chat / anthropic adapters authenticate with no change.
|
package/src/usage-summary.ts
CHANGED
|
Binary file
|