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.
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_CAPTURED_LINES = exports.QUOTA_ERROR_PATTERNS = exports.DEFAULT_CONFIG_TOML = exports.AUTH_FILE_NAME = exports.REGISTRY_FILE_NAME = void 0;
4
+ exports.REGISTRY_FILE_NAME = "registry.json";
5
+ exports.AUTH_FILE_NAME = "auth.json";
6
+ exports.DEFAULT_CONFIG_TOML = [
7
+ "# Codex config for this account",
8
+ "cli_auth_credentials_store = \"file\"",
9
+ "forced_login_method = \"chatgpt\"",
10
+ ""
11
+ ].join("\n");
12
+ exports.QUOTA_ERROR_PATTERNS = [
13
+ /usage\s*limit/i,
14
+ /quota/i,
15
+ /exceeded/i,
16
+ /insufficient\s+credits/i,
17
+ /insufficient\s+quota/i,
18
+ /credits?\s+exhausted/i
19
+ ];
20
+ exports.MAX_CAPTURED_LINES = 200;
@@ -0,0 +1,25 @@
1
+ import { TokenPair } from "./token_utils";
2
+ export interface AccountState {
3
+ name: string;
4
+ accountDir: string;
5
+ tokens: TokenPair;
6
+ cooldownUntilMs: number;
7
+ lastError?: string;
8
+ consecutiveFailures: number;
9
+ }
10
+ export declare class AccountPool {
11
+ private readonly accounts;
12
+ private readonly sessionAssignments;
13
+ constructor(accounts: AccountState[]);
14
+ static loadFromRegistry(baseDir: string): AccountPool;
15
+ getAccounts(): AccountState[];
16
+ getStickyAccount(sessionKey: string): AccountState | undefined;
17
+ assignAccount(sessionKey: string, accountName: string): void;
18
+ clearAssignment(sessionKey: string): void;
19
+ pickNextAvailable(excluded: Set<string>): AccountState | undefined;
20
+ markQuota(account: AccountState, cooldownSeconds: number, resetsAtMs?: number): void;
21
+ markAuthFailure(account: AccountState, message: string): void;
22
+ markSuccess(account: AccountState): void;
23
+ updateTokens(account: AccountState, tokens: TokenPair): void;
24
+ isTokenFresh(account: AccountState, bufferSeconds: number): boolean;
25
+ }
@@ -0,0 +1,125 @@
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.AccountPool = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const paths_1 = require("../paths");
10
+ const registry_store_1 = require("../registry_store");
11
+ const account_manager_1 = require("../account_manager");
12
+ const token_utils_1 = require("./token_utils");
13
+ class AccountPool {
14
+ accounts;
15
+ sessionAssignments = new Map();
16
+ constructor(accounts) {
17
+ this.accounts = accounts;
18
+ }
19
+ static loadFromRegistry(baseDir) {
20
+ const registry = (0, registry_store_1.loadRegistry)(baseDir);
21
+ const orderedAccounts = (0, account_manager_1.getAccountOrder)(registry);
22
+ const accounts = [];
23
+ for (const name of orderedAccounts) {
24
+ const accountDir = (0, paths_1.getAccountDir)(baseDir, name);
25
+ const tokens = loadTokens(accountDir);
26
+ if (!tokens) {
27
+ continue;
28
+ }
29
+ accounts.push({
30
+ name,
31
+ accountDir,
32
+ tokens,
33
+ cooldownUntilMs: 0,
34
+ consecutiveFailures: 0
35
+ });
36
+ }
37
+ return new AccountPool(accounts);
38
+ }
39
+ getAccounts() {
40
+ return this.accounts;
41
+ }
42
+ getStickyAccount(sessionKey) {
43
+ const assigned = this.sessionAssignments.get(sessionKey);
44
+ if (!assigned) {
45
+ return undefined;
46
+ }
47
+ return this.accounts.find((account) => account.name === assigned);
48
+ }
49
+ assignAccount(sessionKey, accountName) {
50
+ this.sessionAssignments.set(sessionKey, accountName);
51
+ }
52
+ clearAssignment(sessionKey) {
53
+ this.sessionAssignments.delete(sessionKey);
54
+ }
55
+ pickNextAvailable(excluded) {
56
+ const now = Date.now();
57
+ for (const account of this.accounts) {
58
+ if (excluded.has(account.name)) {
59
+ continue;
60
+ }
61
+ if (account.cooldownUntilMs > now) {
62
+ continue;
63
+ }
64
+ return account;
65
+ }
66
+ return undefined;
67
+ }
68
+ markQuota(account, cooldownSeconds, resetsAtMs) {
69
+ account.consecutiveFailures += 1;
70
+ account.lastError = "usage_limit_reached";
71
+ const until = resetsAtMs && resetsAtMs > Date.now() ? resetsAtMs : Date.now() + cooldownSeconds * 1000;
72
+ account.cooldownUntilMs = until;
73
+ }
74
+ markAuthFailure(account, message) {
75
+ account.consecutiveFailures += 1;
76
+ account.lastError = message;
77
+ account.cooldownUntilMs = Date.now() + 60 * 1000;
78
+ }
79
+ markSuccess(account) {
80
+ account.consecutiveFailures = 0;
81
+ account.lastError = undefined;
82
+ }
83
+ updateTokens(account, tokens) {
84
+ account.tokens = tokens;
85
+ persistTokens(account.accountDir, tokens);
86
+ }
87
+ isTokenFresh(account, bufferSeconds) {
88
+ return (0, token_utils_1.isTokenFresh)(account.tokens.expiresAtMs, bufferSeconds);
89
+ }
90
+ }
91
+ exports.AccountPool = AccountPool;
92
+ function loadTokens(accountDir) {
93
+ const authPath = path_1.default.join(accountDir, "auth.json");
94
+ if (!fs_1.default.existsSync(authPath)) {
95
+ return undefined;
96
+ }
97
+ const raw = fs_1.default.readFileSync(authPath, "utf8");
98
+ const data = JSON.parse(raw);
99
+ const tokens = data.tokens ?? {};
100
+ if (!tokens.access_token || !tokens.refresh_token) {
101
+ return undefined;
102
+ }
103
+ const details = (0, token_utils_1.deriveTokenDetails)(tokens.access_token, tokens.id_token);
104
+ return {
105
+ accessToken: tokens.access_token,
106
+ refreshToken: tokens.refresh_token,
107
+ idToken: tokens.id_token,
108
+ accountId: tokens.account_id ?? details.chatgptAccountId,
109
+ ...details
110
+ };
111
+ }
112
+ function persistTokens(accountDir, tokens) {
113
+ const authPath = path_1.default.join(accountDir, "auth.json");
114
+ const data = {
115
+ OPENAI_API_KEY: null,
116
+ tokens: {
117
+ access_token: tokens.accessToken,
118
+ refresh_token: tokens.refreshToken,
119
+ id_token: tokens.idToken,
120
+ account_id: tokens.accountId
121
+ },
122
+ last_refresh: new Date().toISOString()
123
+ };
124
+ fs_1.default.writeFileSync(authPath, JSON.stringify(data, null, 2) + "\n", "utf8");
125
+ }
@@ -0,0 +1,6 @@
1
+ export interface CodexConfigUpdate {
2
+ baseUrl: string;
3
+ }
4
+ export declare function enableGatewayConfig(update: CodexConfigUpdate): void;
5
+ export declare function disableGatewayConfig(): void;
6
+ export declare function getCodexConfigPath(): string;
@@ -0,0 +1,73 @@
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.enableGatewayConfig = enableGatewayConfig;
7
+ exports.disableGatewayConfig = disableGatewayConfig;
8
+ exports.getCodexConfigPath = getCodexConfigPath;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const BACKUP_SUFFIX = ".cao.bak";
13
+ function enableGatewayConfig(update) {
14
+ const configPath = getCodexConfigPath();
15
+ const original = fs_1.default.existsSync(configPath) ? fs_1.default.readFileSync(configPath, "utf8") : "";
16
+ ensureBackup(configPath, original);
17
+ let updated = original;
18
+ if (/^model_provider\s*=.*$/m.test(updated)) {
19
+ updated = updated.replace(/^model_provider\s*=.*$/m, 'model_provider = "openai"');
20
+ }
21
+ else {
22
+ updated = `model_provider = "openai"\n${updated}`;
23
+ }
24
+ updated = upsertOpenAiBaseUrl(updated, update.baseUrl);
25
+ fs_1.default.writeFileSync(configPath, `${updated.trimEnd()}\n`, "utf8");
26
+ }
27
+ function disableGatewayConfig() {
28
+ const configPath = getCodexConfigPath();
29
+ const backupPath = `${configPath}${BACKUP_SUFFIX}`;
30
+ if (!fs_1.default.existsSync(backupPath)) {
31
+ return;
32
+ }
33
+ const backup = fs_1.default.readFileSync(backupPath, "utf8");
34
+ fs_1.default.writeFileSync(configPath, backup, "utf8");
35
+ fs_1.default.rmSync(backupPath, { force: true });
36
+ }
37
+ function getCodexConfigPath() {
38
+ return path_1.default.join(os_1.default.homedir(), ".codex", "config.toml");
39
+ }
40
+ function ensureBackup(configPath, content) {
41
+ const backupPath = `${configPath}${BACKUP_SUFFIX}`;
42
+ if (fs_1.default.existsSync(backupPath)) {
43
+ return;
44
+ }
45
+ fs_1.default.writeFileSync(backupPath, content, "utf8");
46
+ }
47
+ function buildProviderBlock(baseUrl) {
48
+ return [
49
+ "[model_providers.openai]",
50
+ "name = \"OpenAI\"",
51
+ `base_url = \"${baseUrl}\"`,
52
+ ""
53
+ ].join("\n");
54
+ }
55
+ function upsertOpenAiBaseUrl(configText, baseUrl) {
56
+ const blockRegex = /\[model_providers\.openai\][\s\S]*?(?=\n\[|$)/;
57
+ if (blockRegex.test(configText)) {
58
+ const block = configText.match(blockRegex)?.[0] ?? "";
59
+ let updatedBlock = block;
60
+ if (/^base_url\s*=.*$/m.test(updatedBlock)) {
61
+ updatedBlock = updatedBlock.replace(/^base_url\s*=.*$/m, `base_url = \"${baseUrl}\"`);
62
+ }
63
+ else {
64
+ updatedBlock = `${updatedBlock.trimEnd()}\nbase_url = \"${baseUrl}\"\n`;
65
+ }
66
+ if (!/^name\s*=.*$/m.test(updatedBlock)) {
67
+ updatedBlock = updatedBlock.replace(/\[model_providers\.openai\]\n?/, "[model_providers.openai]\nname = \"OpenAI\"\n");
68
+ }
69
+ return configText.replace(blockRegex, updatedBlock.trimEnd());
70
+ }
71
+ const appendedBlock = buildProviderBlock(baseUrl);
72
+ return `${configText.trimEnd()}\n\n${appendedBlock}`;
73
+ }
@@ -0,0 +1,7 @@
1
+ export interface ShimResult {
2
+ shimPath: string;
3
+ realCodexPath: string;
4
+ inPath: boolean;
5
+ }
6
+ export declare function enableGatewayShim(baseUrl: string): ShimResult;
7
+ export declare function disableGatewayShim(): boolean;
@@ -0,0 +1,83 @@
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.enableGatewayShim = enableGatewayShim;
7
+ exports.disableGatewayShim = disableGatewayShim;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const child_process_1 = require("child_process");
12
+ const SHIM_MARKER = "# CAO GATEWAY SHIM";
13
+ const REAL_CODEX_PATH_FILE = "real_codex_path";
14
+ function enableGatewayShim(baseUrl) {
15
+ const realCodexPath = resolveCodexPath();
16
+ const shimDir = resolveShimDir();
17
+ const shimPath = path_1.default.join(shimDir, "codex");
18
+ if (!fs_1.default.existsSync(shimDir)) {
19
+ fs_1.default.mkdirSync(shimDir, { recursive: true });
20
+ }
21
+ const script = [
22
+ "#!/usr/bin/env bash",
23
+ SHIM_MARKER,
24
+ `REAL_CODEX=\"${realCodexPath}\"`,
25
+ `export OPENAI_BASE_URL=\"${baseUrl}\"`,
26
+ "exec \"$REAL_CODEX\" \"$@\"",
27
+ ""
28
+ ].join("\n");
29
+ fs_1.default.writeFileSync(shimPath, script, { encoding: "utf8", mode: 0o755 });
30
+ const dataDir = path_1.default.join(os_1.default.homedir(), ".codex-account-orchestrator");
31
+ if (!fs_1.default.existsSync(dataDir)) {
32
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
33
+ }
34
+ fs_1.default.writeFileSync(path_1.default.join(dataDir, REAL_CODEX_PATH_FILE), realCodexPath, "utf8");
35
+ return {
36
+ shimPath,
37
+ realCodexPath,
38
+ inPath: isDirInPath(shimDir)
39
+ };
40
+ }
41
+ function disableGatewayShim() {
42
+ const shimDir = resolveShimDir();
43
+ const shimPath = path_1.default.join(shimDir, "codex");
44
+ if (!fs_1.default.existsSync(shimPath)) {
45
+ return false;
46
+ }
47
+ const contents = fs_1.default.readFileSync(shimPath, "utf8");
48
+ if (!contents.includes(SHIM_MARKER)) {
49
+ return false;
50
+ }
51
+ fs_1.default.rmSync(shimPath, { force: true });
52
+ return true;
53
+ }
54
+ function resolveCodexPath() {
55
+ const shimPath = path_1.default.join(resolveShimDir(), "codex");
56
+ const whichOutput = (0, child_process_1.execSync)("which -a codex", { encoding: "utf8" })
57
+ .split("\n")
58
+ .map((line) => line.trim())
59
+ .filter(Boolean);
60
+ for (const line of whichOutput) {
61
+ if (line.includes("aliased to")) {
62
+ continue;
63
+ }
64
+ if (line === shimPath) {
65
+ continue;
66
+ }
67
+ if (fs_1.default.existsSync(line)) {
68
+ return line;
69
+ }
70
+ }
71
+ const fallback = (0, child_process_1.execSync)("command -v codex", { encoding: "utf8" }).trim();
72
+ if (!fallback || fallback === shimPath) {
73
+ throw new Error("codex binary not found in PATH (or only the CAO shim is present). Disable the shim first.");
74
+ }
75
+ return fallback;
76
+ }
77
+ function resolveShimDir() {
78
+ return path_1.default.join(os_1.default.homedir(), ".local", "bin");
79
+ }
80
+ function isDirInPath(dir) {
81
+ const envPath = process.env.PATH ?? "";
82
+ return envPath.split(":").includes(dir);
83
+ }
@@ -0,0 +1,14 @@
1
+ export interface GatewayConfig {
2
+ bindAddress: string;
3
+ port: number;
4
+ baseUrl: string;
5
+ oauthClientId: string;
6
+ cooldownSeconds: number;
7
+ maxRetryPasses: number;
8
+ requestTimeoutMs: number;
9
+ overrideAuth: boolean;
10
+ }
11
+ export declare function resolveGatewayConfig(overrides: Partial<GatewayConfig>): GatewayConfig;
12
+ export declare function getGatewayConfigPath(): string;
13
+ export declare function loadGatewayConfig(): Partial<GatewayConfig>;
14
+ export declare function saveGatewayConfig(config: Partial<GatewayConfig>): void;
@@ -0,0 +1,46 @@
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.resolveGatewayConfig = resolveGatewayConfig;
7
+ exports.getGatewayConfigPath = getGatewayConfigPath;
8
+ exports.loadGatewayConfig = loadGatewayConfig;
9
+ exports.saveGatewayConfig = saveGatewayConfig;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const DEFAULT_CONFIG = {
13
+ bindAddress: "127.0.0.1",
14
+ port: 4319,
15
+ baseUrl: "https://chatgpt.com/backend-api/codex",
16
+ oauthClientId: "app_EMoamEEZ73f0CkXaXp7hrann",
17
+ cooldownSeconds: 900,
18
+ maxRetryPasses: 1,
19
+ requestTimeoutMs: 120_000,
20
+ overrideAuth: true
21
+ };
22
+ function resolveGatewayConfig(overrides) {
23
+ return {
24
+ ...DEFAULT_CONFIG,
25
+ ...overrides
26
+ };
27
+ }
28
+ function getGatewayConfigPath() {
29
+ return path_1.default.join(process.env.HOME ?? "", ".codex-account-orchestrator", "gateway.json");
30
+ }
31
+ function loadGatewayConfig() {
32
+ const configPath = getGatewayConfigPath();
33
+ if (!fs_1.default.existsSync(configPath)) {
34
+ return {};
35
+ }
36
+ const raw = fs_1.default.readFileSync(configPath, "utf8");
37
+ return JSON.parse(raw);
38
+ }
39
+ function saveGatewayConfig(config) {
40
+ const configPath = getGatewayConfigPath();
41
+ const dir = path_1.default.dirname(configPath);
42
+ if (!fs_1.default.existsSync(dir)) {
43
+ fs_1.default.mkdirSync(dir, { recursive: true });
44
+ }
45
+ fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
46
+ }
@@ -0,0 +1,15 @@
1
+ import { IncomingMessage, ServerResponse } from "http";
2
+ import { AccountPool } from "./account_pool";
3
+ import { GatewayConfig } from "./gateway_config";
4
+ export declare class OpenAiGateway {
5
+ private readonly pool;
6
+ private readonly config;
7
+ private readonly inFlightRefresh;
8
+ constructor(pool: AccountPool, config: GatewayConfig);
9
+ handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void>;
10
+ private selectAccount;
11
+ private forwardRequest;
12
+ private fetchOnce;
13
+ private ensureAccessToken;
14
+ private refreshToken;
15
+ }