@bitkyc08/opencodex 2.6.12 → 2.6.14-preview.20260701

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/src/doctor.ts ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `ocx doctor` - read-only environment diagnostics.
3
+ *
4
+ * Explains WHY ChatGPT quota may never populate (and thus why account
5
+ * auto-switch can appear stuck), especially on WSL2 where outbound fetch to
6
+ * chatgpt.com can be blocked by NAT/DNS/VPN/proxy differences. Observe-only:
7
+ * it never sets proxy env, relocates state dirs, mutates quota, or changes
8
+ * networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
9
+ */
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join, resolve } from "node:path";
13
+ import { getConfigDir, getConfigPath } from "./config";
14
+ import { readCodexTokens } from "./codex-auth-collision";
15
+
16
+ const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
17
+ const PROBE_TIMEOUT_MS = 8000;
18
+
19
+ export type PathRow = { label: string; path: string; exists: boolean };
20
+
21
+ export function resolveCodexHomeDir(): string {
22
+ const raw = process.env["CODEX_HOME"]?.trim();
23
+ return raw ? resolve(raw) : join(homedir(), ".codex");
24
+ }
25
+
26
+ export function collectPaths(): PathRow[] {
27
+ const codexHome = resolveCodexHomeDir();
28
+ const opencodexHome = getConfigDir();
29
+ return [
30
+ { label: "CODEX_HOME", path: codexHome, exists: existsSync(codexHome) },
31
+ { label: "CODEX_HOME/auth.json", path: join(codexHome, "auth.json"), exists: existsSync(join(codexHome, "auth.json")) },
32
+ { label: "OPENCODEX_HOME", path: opencodexHome, exists: existsSync(opencodexHome) },
33
+ { label: "OPENCODEX_HOME/config.json", path: getConfigPath(), exists: existsSync(getConfigPath()) },
34
+ ];
35
+ }
36
+
37
+ export type FsTypeInfo = { fstype: string; mount: string; isDrvfs: boolean; isMntDrive: boolean };
38
+
39
+ /**
40
+ * Parse `/proc/mounts`-shaped content and return the longest mount-point prefix
41
+ * covering `path`. `mountsContent` is injectable for testing; in production the
42
+ * caller passes the real file (or null off-Linux -> "n/a").
43
+ */
44
+ export function detectFsType(path: string, mountsContent: string | null): FsTypeInfo {
45
+ const isMntDrive = /^\/mnt\/[a-z]\//i.test(path) || /^\/mnt\/[a-z]$/i.test(path);
46
+ if (!mountsContent) {
47
+ return { fstype: "n/a", mount: "", isDrvfs: false, isMntDrive };
48
+ }
49
+ let best: { mount: string; fstype: string } | null = null;
50
+ for (const line of mountsContent.split("\n")) {
51
+ const parts = line.split(/\s+/);
52
+ if (parts.length < 3) continue;
53
+ const mount = parts[1]!;
54
+ const fstype = parts[2]!;
55
+ if (path === mount || path.startsWith(mount.endsWith("/") ? mount : `${mount}/`) || mount === "/") {
56
+ if (!best || mount.length > best.mount.length) best = { mount, fstype };
57
+ }
58
+ }
59
+ const fstype = best?.fstype ?? "unknown";
60
+ return {
61
+ fstype,
62
+ mount: best?.mount ?? "",
63
+ isDrvfs: fstype === "drvfs" || fstype === "9p",
64
+ isMntDrive,
65
+ };
66
+ }
67
+
68
+ function readMounts(): string | null {
69
+ try {
70
+ return process.platform === "linux" ? readFileSync("/proc/mounts", "utf-8") : null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"] as const;
77
+
78
+ export type ProxyEnvRow = { key: string; present: boolean };
79
+
80
+ /** Report only presence/absence of proxy env vars - never the value (it may
81
+ * embed credentials). Checks both upper- and lower-case forms. */
82
+ export function collectProxyEnv(): ProxyEnvRow[] {
83
+ return PROXY_KEYS.map(key => ({
84
+ key,
85
+ present: !!(process.env[key]?.trim() || process.env[key.toLowerCase()]?.trim()),
86
+ }));
87
+ }
88
+
89
+ export type WhamProbeResult = {
90
+ ok: boolean;
91
+ status: number | null;
92
+ durationMs: number;
93
+ classification: "ok" | "timeout" | "connect_error" | string;
94
+ authenticated: boolean;
95
+ };
96
+
97
+ /**
98
+ * Replicate the runtime WHAM fetch shape (same URL, 8s timeout, main-token
99
+ * headers when present) so the probe fails exactly where the real path fails.
100
+ * `fetchImpl` is injectable for testing.
101
+ */
102
+ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamProbeResult> {
103
+ const tokens = readCodexTokens();
104
+ const headers: Record<string, string> = {};
105
+ if (tokens) {
106
+ headers.Authorization = `Bearer ${tokens.access_token}`;
107
+ headers["ChatGPT-Account-Id"] = tokens.account_id;
108
+ }
109
+ const start = performance.now();
110
+ try {
111
+ const resp = await fetchImpl(WHAM_USAGE_URL, { headers, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
112
+ const durationMs = Math.round(performance.now() - start);
113
+ return {
114
+ ok: resp.ok,
115
+ status: resp.status,
116
+ durationMs,
117
+ classification: resp.ok ? "ok" : `http_${resp.status}`,
118
+ authenticated: !!tokens,
119
+ };
120
+ } catch (err) {
121
+ const durationMs = Math.round(performance.now() - start);
122
+ const name = err instanceof Error ? err.name : String(err);
123
+ const classification = name === "TimeoutError" || name === "AbortError"
124
+ ? "timeout"
125
+ : "connect_error";
126
+ return { ok: false, status: null, durationMs, classification, authenticated: !!tokens };
127
+ }
128
+ }
129
+
130
+ export async function runDoctor(): Promise<void> {
131
+ console.log("opencodex doctor\n");
132
+
133
+ const paths = collectPaths();
134
+ const mounts = readMounts();
135
+ console.log("Paths");
136
+ for (const row of paths) {
137
+ const fs = detectFsType(row.path, mounts);
138
+ const flags = [fs.fstype !== "n/a" ? `fs=${fs.fstype}` : null, fs.isDrvfs || fs.isMntDrive ? "WSL /mnt drive" : null]
139
+ .filter(Boolean).join(", ");
140
+ console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
141
+ }
142
+
143
+ console.log("\nProxy env (presence only)");
144
+ for (const row of collectProxyEnv()) {
145
+ console.log(` ${row.present ? "set " : "unset "} ${row.key}`);
146
+ }
147
+
148
+ console.log("\nWHAM reachability");
149
+ const probe = await probeWham();
150
+ const detail = probe.status !== null ? `status=${probe.status}` : `error=${probe.classification}`;
151
+ console.log(` ${probe.ok ? "ok " : "-- "} ${WHAM_USAGE_URL}`);
152
+ console.log(` ${detail}, ${probe.durationMs}ms, ${probe.authenticated ? "authenticated" : "unauthenticated"}`);
153
+
154
+ // Hints, not fixes.
155
+ const hints: string[] = [];
156
+ const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
157
+ const noProxy = collectProxyEnv().every(p => !p.present);
158
+ if (anyDrvfs) {
159
+ hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.");
160
+ }
161
+ if (!probe.ok) {
162
+ if (probe.classification === "timeout" || probe.classification === "connect_error") {
163
+ hints.push("WHAM probe could not reach chatgpt.com. On WSL2 this is often NAT/DNS/VPN. Quota cannot prime, so auto-switch stays on unknown scores.");
164
+ if (noProxy) {
165
+ hints.push("No *_PROXY env is set in this WSL process. If Windows uses a proxy/VPN, set HTTP(S)_PROXY here or enable WSL autoProxy so Bun fetch can reach the network.");
166
+ }
167
+ }
168
+ }
169
+ if (hints.length > 0) {
170
+ console.log("\nHints");
171
+ for (const h of hints) console.log(` - ${h}`);
172
+ }
173
+ }
@@ -13,7 +13,7 @@ const SCOPES = "org:create_api_key user:profile user:inference";
13
13
  // ── OAuth-request requirements applied by the anthropic adapter when authMode==="oauth" ──
14
14
  export const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20";
15
15
  export const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
16
- const CLAUDE_TOOL_PREFIX = "proxy_";
16
+ const CLAUDE_TOOL_PREFIX = "custom_";
17
17
  const ANTHROPIC_BUILTIN_TOOLS = new Set(["web_search", "code_execution", "text_editor", "computer"]);
18
18
 
19
19
  /** OAuth tokens reject arbitrary tool names; prefix custom tools (Anthropic builtins are exempt). */
@@ -22,7 +22,7 @@ export function applyClaudeToolPrefix(name: string): string {
22
22
  return CLAUDE_TOOL_PREFIX + name;
23
23
  }
24
24
 
25
- /** Strip the proxy_ prefix from a returned tool_use name so the caller (Codex) sees the original. */
25
+ /** Strip the custom_ prefix from a returned tool_use name so the caller (Codex) sees the original. */
26
26
  export function stripClaudeToolPrefix(name: string): string {
27
27
  return name.startsWith(CLAUDE_TOOL_PREFIX) ? name.slice(CLAUDE_TOOL_PREFIX.length) : name;
28
28
  }
@@ -12,6 +12,7 @@
12
12
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
13
13
  import { generatePKCE } from "./pkce";
14
14
  import type { OAuthController, OAuthCredentials } from "./types";
15
+ import { antigravityUserAgent, ANTIGRAVITY_GOOG_API_CLIENT_UA } from "../adapters/client-fingerprint";
15
16
 
16
17
  const CLIENT_ID = process.env.GOOGLE_ANTIGRAVITY_CLIENT_ID
17
18
  || "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
@@ -94,7 +95,7 @@ function extractProjectId(data: Record<string, unknown> | undefined): string | u
94
95
  async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
95
96
  const response = await fetch(`${PROD_API}/${API_VERSION}:loadCodeAssist`, {
96
97
  method: "POST",
97
- headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json" },
98
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent() },
98
99
  body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } }),
99
100
  signal: requestSignal(signal),
100
101
  });
@@ -107,8 +108,8 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
107
108
  if (signal?.aborted) throw signal.reason ?? new Error("Antigravity onboarding aborted");
108
109
  const response = await fetch(`${DAILY_API}/${API_VERSION}:onboardUser`, {
109
110
  method: "POST",
110
- headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json" },
111
- body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity" } }),
111
+ headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent(), "x-goog-api-client": ANTIGRAVITY_GOOG_API_CLIENT_UA },
112
+ body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: antigravityUserAgent() } }),
112
113
  signal: requestSignal(signal),
113
114
  });
114
115
  if (!response.ok) {
package/src/oauth/kimi.ts CHANGED
@@ -12,7 +12,7 @@ const DEVICE_ID_FILENAME = "kimi-device-id";
12
12
  const DEFAULT_POLL_INTERVAL_MS = 5000;
13
13
  const DEFAULT_DEVICE_FLOW_TTL_MS = 15 * 60 * 1000;
14
14
  const OAUTH_EXPIRY_SKEW_MS = 5 * 60 * 1000;
15
- const KIMI_CLI_VERSION = "1.0.0";
15
+ const KIMI_CLI_VERSION = "0.14.0";
16
16
 
17
17
  interface DeviceAuthorizationResponse {
18
18
  user_code?: string;
@@ -70,7 +70,7 @@ function getDeviceId(): string {
70
70
  function getKimiCommonHeaders(): Record<string, string> {
71
71
  return {
72
72
  "User-Agent": `KimiCLI/${KIMI_CLI_VERSION}`,
73
- "X-Msh-Platform": "kimi_cli",
73
+ "X-Msh-Platform": "kimi_code_cli",
74
74
  "X-Msh-Version": KIMI_CLI_VERSION,
75
75
  "X-Msh-Device-Name": os.hostname(),
76
76
  "X-Msh-Device-Model": getDeviceModel(),
@@ -0,0 +1,43 @@
1
+ import { createAnthropicAdapter } from "../adapters/anthropic";
2
+ import { createAzureAdapter } from "../adapters/azure";
3
+ import { createGoogleAdapter } from "../adapters/google";
4
+ import { createKiroAdapter } from "../adapters/kiro";
5
+ import { createOpenAIChatAdapter } from "../adapters/openai-chat";
6
+ import { createResponsesPassthroughAdapter } from "../adapters/openai-responses";
7
+ import type { OcxProviderConfig } from "../types";
8
+
9
+ /** Providers whose listed model ids must be driven over the Anthropic wire even if the provider's
10
+ * configured adapter is something else (the upstream only speaks Anthropic for these models). */
11
+ const ANTHROPIC_WIRE_MODELS: Record<string, Set<string>> = {
12
+ "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3", "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]),
13
+ };
14
+
15
+ /** Return a provider config whose adapter is forced to "anthropic" when the model id is wire-pinned. */
16
+ export function resolveWireProtocolOverride(providerName: string, modelId: string, providerConfig: OcxProviderConfig): OcxProviderConfig {
17
+ const overrideSet = ANTHROPIC_WIRE_MODELS[providerName];
18
+ if (overrideSet?.has(modelId) && providerConfig.adapter !== "anthropic") {
19
+ return { ...providerConfig, adapter: "anthropic" };
20
+ }
21
+ return providerConfig;
22
+ }
23
+
24
+ /** Build the provider adapter for a resolved provider config. */
25
+ export function resolveAdapter(providerConfig: OcxProviderConfig) {
26
+ switch (providerConfig.adapter) {
27
+ case "openai-chat":
28
+ return createOpenAIChatAdapter(providerConfig);
29
+ case "anthropic":
30
+ return createAnthropicAdapter(providerConfig);
31
+ case "openai-responses":
32
+ return createResponsesPassthroughAdapter(providerConfig);
33
+ case "google":
34
+ return createGoogleAdapter(providerConfig);
35
+ case "kiro":
36
+ return createKiroAdapter(providerConfig);
37
+ case "azure":
38
+ case "azure-openai":
39
+ return createAzureAdapter(providerConfig);
40
+ default:
41
+ throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
42
+ }
43
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { extname, isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ /** opencodex version, read from the packaged package.json (same source as the server bootstrap). */
5
+ const VERSION = (() => {
6
+ try {
7
+ return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string;
8
+ } catch {
9
+ return "0.0.0";
10
+ }
11
+ })();
12
+
13
+ const MIME_TYPES: Record<string, string> = {
14
+ ".html": "text/html", ".js": "application/javascript", ".css": "text/css",
15
+ ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
16
+ ".ico": "image/x-icon",
17
+ };
18
+
19
+ function findGuiDist(): string | null {
20
+ const candidates = [
21
+ join(import.meta.dir, "..", "..", "gui", "dist"),
22
+ join(import.meta.dir, "..", "..", "..", "gui", "dist"),
23
+ ];
24
+ for (const c of candidates) {
25
+ if (existsSync(join(c, "index.html"))) return c;
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export function resolveGuiFilePath(guiDist: string, pathname: string): string | null {
31
+ let decodedPath: string;
32
+ try {
33
+ decodedPath = decodeURIComponent(pathname);
34
+ } catch {
35
+ return null;
36
+ }
37
+ if (decodedPath.includes("\0")) return null;
38
+
39
+ const relativePath = decodedPath === "/" || decodedPath === ""
40
+ ? "index.html"
41
+ : decodedPath.replace(/\\/g, "/").replace(/^\/+/, "");
42
+ const root = resolve(guiDist);
43
+ const filePath = resolve(root, relativePath);
44
+ const rel = relative(root, filePath);
45
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
46
+ return filePath;
47
+ }
48
+
49
+ function isFile(path: string): boolean {
50
+ try {
51
+ return statSync(path).isFile();
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ export function serveGuiFile(pathname: string): Response | null {
58
+ const guiDist = findGuiDist();
59
+ if (!guiDist) return null;
60
+ const filePath = resolveGuiFilePath(guiDist, pathname);
61
+ if (!filePath) return null;
62
+
63
+ if (!isFile(filePath)) {
64
+ if (!extname(pathname)) {
65
+ const indexPath = join(guiDist, "index.html");
66
+ if (isFile(indexPath)) {
67
+ return new Response(Bun.file(indexPath), {
68
+ headers: { "Content-Type": "text/html" },
69
+ });
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ const ext = extname(filePath);
76
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
77
+ return new Response(Bun.file(filePath), {
78
+ headers: { "Content-Type": contentType },
79
+ });
80
+ }
81
+
82
+ export function rootFallbackPayload() {
83
+ return {
84
+ status: "ok",
85
+ service: "opencodex",
86
+ version: VERSION,
87
+ dashboard: {
88
+ available: false,
89
+ reason: "GUI build not found. Run `bun run build:gui` from the opencodex repo, or use `ocx gui` from a packaged install.",
90
+ },
91
+ endpoints: {
92
+ health: "/healthz",
93
+ models: "/v1/models",
94
+ responses: "/v1/responses",
95
+ management: "/api/*",
96
+ },
97
+ };
98
+ }
package/src/server.ts CHANGED
@@ -1,12 +1,5 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { timingSafeEqual } from "node:crypto";
3
- import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { createAnthropicAdapter } from "./adapters/anthropic";
5
- import { createAzureAdapter } from "./adapters/azure";
6
- import { createGoogleAdapter } from "./adapters/google";
7
- import { createKiroAdapter } from "./adapters/kiro";
8
- import { createOpenAIChatAdapter } from "./adapters/openai-chat";
9
- import { createResponsesPassthroughAdapter } from "./adapters/openai-responses";
10
3
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "./bridge";
11
4
  import { markActivity } from "./sidecar-tracker";
12
5
  import {
@@ -87,6 +80,10 @@ import {
87
80
  type CodexUpstreamOutcome,
88
81
  } from "./codex-routing";
89
82
  import { registerCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "./codex-websocket-registry";
83
+ import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./server/gui-static";
84
+ export { resolveGuiFilePath, rootFallbackPayload } from "./server/gui-static";
85
+ import { resolveAdapter, resolveWireProtocolOverride } from "./server/adapter-resolve";
86
+ export { resolveAdapter } from "./server/adapter-resolve";
90
87
 
91
88
  // ---------------------------------------------------------------------------
92
89
  // Active turn tracking + graceful shutdown drain
@@ -188,124 +185,10 @@ const VERSION = (() => {
188
185
  }
189
186
  })();
190
187
 
191
- const MIME_TYPES: Record<string, string> = {
192
- ".html": "text/html", ".js": "application/javascript", ".css": "text/css",
193
- ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
194
- ".ico": "image/x-icon",
195
- };
188
+ // GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
189
+ // "../src/server" import surface stable for tests/callers.
196
190
 
197
- function findGuiDist(): string | null {
198
- const candidates = [
199
- join(import.meta.dir, "..", "gui", "dist"),
200
- join(import.meta.dir, "..", "..", "gui", "dist"),
201
- ];
202
- for (const c of candidates) {
203
- if (existsSync(join(c, "index.html"))) return c;
204
- }
205
- return null;
206
- }
207
-
208
- export function resolveGuiFilePath(guiDist: string, pathname: string): string | null {
209
- let decodedPath: string;
210
- try {
211
- decodedPath = decodeURIComponent(pathname);
212
- } catch {
213
- return null;
214
- }
215
- if (decodedPath.includes("\0")) return null;
216
-
217
- const relativePath = decodedPath === "/" || decodedPath === ""
218
- ? "index.html"
219
- : decodedPath.replace(/\\/g, "/").replace(/^\/+/, "");
220
- const root = resolve(guiDist);
221
- const filePath = resolve(root, relativePath);
222
- const rel = relative(root, filePath);
223
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
224
- return filePath;
225
- }
226
-
227
- function isFile(path: string): boolean {
228
- try {
229
- return statSync(path).isFile();
230
- } catch {
231
- return false;
232
- }
233
- }
234
-
235
- function serveGuiFile(pathname: string): Response | null {
236
- const guiDist = findGuiDist();
237
- if (!guiDist) return null;
238
- const filePath = resolveGuiFilePath(guiDist, pathname);
239
- if (!filePath) return null;
240
-
241
- if (!isFile(filePath)) {
242
- if (!extname(pathname)) {
243
- const indexPath = join(guiDist, "index.html");
244
- if (isFile(indexPath)) {
245
- return new Response(Bun.file(indexPath), {
246
- headers: { "Content-Type": "text/html" },
247
- });
248
- }
249
- }
250
- return null;
251
- }
252
-
253
- const ext = extname(filePath);
254
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
255
- return new Response(Bun.file(filePath), {
256
- headers: { "Content-Type": contentType },
257
- });
258
- }
259
-
260
- export function rootFallbackPayload() {
261
- return {
262
- status: "ok",
263
- service: "opencodex",
264
- version: VERSION,
265
- dashboard: {
266
- available: false,
267
- reason: "GUI build not found. Run `bun run build:gui` from the opencodex repo, or use `ocx gui` from a packaged install.",
268
- },
269
- endpoints: {
270
- health: "/healthz",
271
- models: "/v1/models",
272
- responses: "/v1/responses",
273
- management: "/api/*",
274
- },
275
- };
276
- }
277
-
278
- const ANTHROPIC_WIRE_MODELS: Record<string, Set<string>> = {
279
- "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3", "qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]),
280
- };
281
-
282
- function resolveWireProtocolOverride(providerName: string, modelId: string, providerConfig: OcxProviderConfig): OcxProviderConfig {
283
- const overrideSet = ANTHROPIC_WIRE_MODELS[providerName];
284
- if (overrideSet?.has(modelId) && providerConfig.adapter !== "anthropic") {
285
- return { ...providerConfig, adapter: "anthropic" };
286
- }
287
- return providerConfig;
288
- }
289
-
290
- export function resolveAdapter(providerConfig: OcxProviderConfig) {
291
- switch (providerConfig.adapter) {
292
- case "openai-chat":
293
- return createOpenAIChatAdapter(providerConfig);
294
- case "anthropic":
295
- return createAnthropicAdapter(providerConfig);
296
- case "openai-responses":
297
- return createResponsesPassthroughAdapter(providerConfig);
298
- case "google":
299
- return createGoogleAdapter(providerConfig);
300
- case "kiro":
301
- return createKiroAdapter(providerConfig);
302
- case "azure":
303
- case "azure-openai":
304
- return createAzureAdapter(providerConfig);
305
- default:
306
- throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
307
- }
308
- }
191
+ // Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
309
192
 
310
193
  function sidecarOutcomeRecorder(config: OcxConfig, authCtx: CodexAuthContext): ((outcome: CodexUpstreamOutcome) => void) | undefined {
311
194
  return authCtx.kind === "pool" || authCtx.kind === "main-pool"
@@ -2310,5 +2193,13 @@ export function startServer(port?: number) {
2310
2193
  console.log(` GET /api/* → management API`);
2311
2194
  console.log(` GET / → GUI dashboard`);
2312
2195
 
2196
+ // Prime pool-account quota in the background so the rotation engine has real
2197
+ // usage scores from the first routing decision, even when the dashboard is
2198
+ // never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
2199
+ // listener, and a blocked network silently no-ops (see Phase 30 diagnostics).
2200
+ import("./codex-auth-api")
2201
+ .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
2202
+ .catch(() => {});
2203
+
2313
2204
  return server;
2314
2205
  }