Spaces:
Paused
Paused
File size: 6,612 Bytes
5d0a52f 5f8456f 4a940a5 5d0a52f d0eeb87 5d0a52f 347f81b 5d0a52f 347f81b 5d0a52f 5f8456f 5d0a52f 5416ffb 921d606 d817d67 5d0a52f 347f81b d0eeb87 347f81b 5dd5107 5d0a52f 347f81b 5d0a52f ab21b87 347f81b ab21b87 22a7de1 d85b21d 3d01305 34fceda 22a7de1 4f2665c 5d0a52f 85aec43 5d0a52f a971da6 5d0a52f 347f81b 5d0a52f d85b21d 5d0a52f 4a940a5 5d0a52f 4a940a5 5d0a52f 0d2f54c 347f81b b1107bc 347f81b 4a940a5 b1107bc 347f81b b1107bc 347f81b 4a940a5 b1107bc 347f81b 5f8456f 347f81b 5f8456f 347f81b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 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();
}
|