Spaces:
Paused
Paused
| import { readFileSync } from "fs"; | |
| import { resolve } from "path"; | |
| import yaml from "js-yaml"; | |
| import { z } from "zod"; | |
| import { loadStaticModels } from "./models/model-store.js"; | |
| import { triggerImmediateRefresh } from "./models/model-fetcher.js"; | |
| import { getConfigDir } from "./paths.js"; | |
| export const ROTATION_STRATEGIES = ["least_used", "round_robin", "sticky"] as const; | |
| const ConfigSchema = z.object({ | |
| api: z.object({ | |
| base_url: z.string().default("https://chatgpt.com/backend-api"), | |
| timeout_seconds: z.number().min(1).default(60), | |
| }), | |
| client: z.object({ | |
| originator: z.string().default("Codex Desktop"), | |
| app_version: z.string().default("260202.0859"), | |
| build_number: z.string().default("517"), | |
| platform: z.string().default("darwin"), | |
| arch: z.string().default("arm64"), | |
| chromium_version: z.string().default("136"), | |
| }), | |
| model: z.object({ | |
| default: z.string().default("gpt-5.2-codex"), | |
| default_reasoning_effort: z.string().default("medium"), | |
| default_service_tier: z.string().nullable().default(null), | |
| inject_desktop_context: z.boolean().default(false), | |
| suppress_desktop_directives: z.boolean().default(true), | |
| }), | |
| auth: z.object({ | |
| jwt_token: z.string().nullable().default(null), | |
| chatgpt_oauth: z.boolean().default(true), | |
| refresh_margin_seconds: z.number().min(0).default(300), | |
| rotation_strategy: z.enum(ROTATION_STRATEGIES).default("least_used"), | |
| rate_limit_backoff_seconds: z.number().min(1).default(60), | |
| oauth_client_id: z.string().default("app_EMoamEEZ73f0CkXaXp7hrann"), | |
| oauth_auth_endpoint: z.string().default("https://auth.openai.com/oauth/authorize"), | |
| oauth_token_endpoint: z.string().default("https://auth.openai.com/oauth/token"), | |
| }), | |
| server: z.object({ | |
| host: z.string().default("0.0.0.0"), | |
| port: z.number().min(1).max(65535).default(8080), | |
| proxy_api_key: z.string().nullable().default(null), | |
| }), | |
| session: z.object({ | |
| ttl_minutes: z.number().min(1).default(60), | |
| cleanup_interval_minutes: z.number().min(1).default(5), | |
| }), | |
| tls: z.object({ | |
| curl_binary: z.string().default("auto"), | |
| impersonate_profile: z.string().default("chrome136"), | |
| proxy_url: z.string().nullable().default(null), | |
| transport: z.enum(["auto", "curl-cli", "libcurl-ffi"]).default("auto"), | |
| force_http11: z.boolean().default(false), | |
| }).default({}), | |
| quota: z.object({ | |
| refresh_interval_minutes: z.number().min(1).default(5), | |
| warning_thresholds: z.object({ | |
| primary: z.array(z.number().min(1).max(100)).default([80, 90]), | |
| secondary: z.array(z.number().min(1).max(100)).default([80, 90]), | |
| }).default({}), | |
| skip_exhausted: z.boolean().default(true), | |
| }).default({}), | |
| }); | |
| export type AppConfig = z.infer<typeof ConfigSchema>; | |
| const FingerprintSchema = z.object({ | |
| user_agent_template: z.string(), | |
| auth_domains: z.array(z.string()), | |
| auth_domain_exclusions: z.array(z.string()), | |
| header_order: z.array(z.string()), | |
| default_headers: z.record(z.string()).optional().default({}), | |
| }); | |
| export type FingerprintConfig = z.infer<typeof FingerprintSchema>; | |
| function loadYaml(filePath: string): unknown { | |
| const content = readFileSync(filePath, "utf-8"); | |
| return yaml.load(content); | |
| } | |
| function applyEnvOverrides(raw: Record<string, unknown>): Record<string, unknown> { | |
| const jwtEnv = process.env.CODEX_JWT_TOKEN?.trim(); | |
| if (jwtEnv && jwtEnv.startsWith("eyJ")) { | |
| (raw.auth as Record<string, unknown>).jwt_token = jwtEnv; | |
| } else if (jwtEnv) { | |
| console.warn("[Config] CODEX_JWT_TOKEN ignored: not a valid JWT (must start with 'eyJ')"); | |
| } | |
| if (process.env.CODEX_PLATFORM) { | |
| (raw.client as Record<string, unknown>).platform = process.env.CODEX_PLATFORM; | |
| } | |
| if (process.env.CODEX_ARCH) { | |
| (raw.client as Record<string, unknown>).arch = process.env.CODEX_ARCH; | |
| } | |
| if (process.env.PORT) { | |
| const parsed = parseInt(process.env.PORT, 10); | |
| if (!isNaN(parsed)) { | |
| (raw.server as Record<string, unknown>).port = parsed; | |
| } | |
| } | |
| const proxyEnv = process.env.HTTPS_PROXY || process.env.https_proxy; | |
| if (proxyEnv) { | |
| if (!raw.tls) raw.tls = {}; | |
| (raw.tls as Record<string, unknown>).proxy_url = proxyEnv; | |
| } | |
| return raw; | |
| } | |
| let _config: AppConfig | null = null; | |
| let _fingerprint: FingerprintConfig | null = null; | |
| export function loadConfig(configDir?: string): AppConfig { | |
| if (_config) return _config; | |
| const dir = configDir ?? getConfigDir(); | |
| const raw = loadYaml(resolve(dir, "default.yaml")) as Record<string, unknown>; | |
| applyEnvOverrides(raw); | |
| _config = ConfigSchema.parse(raw); | |
| return _config; | |
| } | |
| export function loadFingerprint(configDir?: string): FingerprintConfig { | |
| if (_fingerprint) return _fingerprint; | |
| const dir = configDir ?? getConfigDir(); | |
| const raw = loadYaml(resolve(dir, "fingerprint.yaml")); | |
| _fingerprint = FingerprintSchema.parse(raw); | |
| return _fingerprint; | |
| } | |
| export function getConfig(): AppConfig { | |
| if (!_config) throw new Error("Config not loaded. Call loadConfig() first."); | |
| return _config; | |
| } | |
| export function getFingerprint(): FingerprintConfig { | |
| if (!_fingerprint) throw new Error("Fingerprint not loaded. Call loadFingerprint() first."); | |
| return _fingerprint; | |
| } | |
| export function mutateClientConfig(patch: Partial<AppConfig["client"]>): void { | |
| if (!_config) throw new Error("Config not loaded"); | |
| Object.assign(_config.client, patch); | |
| } | |
| /** Reload config from disk (hot-reload after full-update). | |
| * P1-5: Load to temp first, then swap atomically to avoid null window. */ | |
| export function reloadConfig(configDir?: string): AppConfig { | |
| const dir = configDir ?? getConfigDir(); | |
| const raw = loadYaml(resolve(dir, "default.yaml")) as Record<string, unknown>; | |
| applyEnvOverrides(raw); | |
| const fresh = ConfigSchema.parse(raw); | |
| _config = fresh; | |
| return _config; | |
| } | |
| /** Reload fingerprint from disk (hot-reload after full-update). | |
| * P1-5: Load to temp first, then swap atomically. */ | |
| export function reloadFingerprint(configDir?: string): FingerprintConfig { | |
| const dir = configDir ?? getConfigDir(); | |
| const raw = loadYaml(resolve(dir, "fingerprint.yaml")); | |
| const fresh = FingerprintSchema.parse(raw); | |
| _fingerprint = fresh; | |
| return _fingerprint; | |
| } | |
| /** Reload both config and fingerprint from disk, plus static models. */ | |
| export function reloadAllConfigs(configDir?: string): void { | |
| reloadConfig(configDir); | |
| reloadFingerprint(configDir); | |
| loadStaticModels(configDir); | |
| console.log("[Config] Hot-reloaded config, fingerprint, and models from disk"); | |
| // Re-merge backend models so hot-reload doesn't wipe them for ~1h | |
| triggerImmediateRefresh(); | |
| } | |