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();
}