authorgravatar for crazywolf132@gmail.com0x1f99d <crazywolf132@gmail.com> 2026-03-11 04:14:58+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-10 10:14:58-07:00
log9becb3f40af504244f85823bb19f18bfe507ca49
treeb3f0527f7f031790be10136ff158f959ea044226
parent2c351726ae21a73ac0549500783d5a4de6394214
signature Signed by PGP key B5690EEEBB952194

fix(server): skip auth check when Codex CLI uses a custom model provider (#649)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 489 insertions(+), 148 deletions(-)

apps/server/src/provider/Layers/ProviderHealth.test.ts+404-146
......@@ -1,10 +1,15 @@
1import assert from "node:assert/strict";
2import { it } from "@effect/vitest";
3import { Effect, Layer, Sink, Stream } from "effect";
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, it, assert } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path, Sink, Stream } from "effect";
44import * as PlatformError from "effect/PlatformError";
55import { ChildProcessSpawner } from "effect/unstable/process";
66
7import { checkCodexProviderStatus, parseAuthStatusFromOutput } from "./ProviderHealth";
7import {
8 checkCodexProviderStatus,
9 hasCustomModelProvider,
10 parseAuthStatusFromOutput,
11 readCodexConfigModelProvider,
12} from "./ProviderHealth";
813
914// ── Test helpers ────────────────────────────────────────────────────
1015
......@@ -53,157 +58,410 @@ function failingSpawnerLayer(description: string) {
5358 );
5459}
5560
56// ── Tests ───────────────────────────────────────────────────────────
57
58it.effect("returns ready when codex is installed and authenticated", () =>
59 Effect.gen(function* () {
60 const status = yield* checkCodexProviderStatus;
61 assert.strictEqual(status.provider, "codex");
62 assert.strictEqual(status.status, "ready");
63 assert.strictEqual(status.available, true);
64 assert.strictEqual(status.authStatus, "authenticated");
65 }).pipe(
66 Effect.provide(
67 mockSpawnerLayer((args) => {
68 const joined = args.join(" ");
69 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
70 if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };
71 throw new Error(`Unexpected args: ${joined}`);
61/**
62 * Create a temporary CODEX_HOME scoped to the current Effect test.
63 * Cleanup is registered in the test scope rather than via Vitest hooks.
64 */
65function withTempCodexHome(configContent?: string) {
66 return Effect.gen(function* () {
67 const fileSystem = yield* FileSystem.FileSystem;
68 const path = yield* Path.Path;
69 const tmpDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-test-codex-" });
70
71 yield* Effect.acquireRelease(
72 Effect.sync(() => {
73 const originalCodexHome = process.env.CODEX_HOME;
74 process.env.CODEX_HOME = tmpDir;
75 return originalCodexHome;
7276 }),
73 ),
74 ),
75);
76
77it.effect("returns unavailable when codex is missing", () =>
78 Effect.gen(function* () {
79 const status = yield* checkCodexProviderStatus;
80 assert.strictEqual(status.provider, "codex");
81 assert.strictEqual(status.status, "error");
82 assert.strictEqual(status.available, false);
83 assert.strictEqual(status.authStatus, "unknown");
84 assert.strictEqual(status.message, "Codex CLI (`codex`) is not installed or not on PATH.");
85 }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))),
86);
87
88it.effect("returns unavailable when codex is below the minimum supported version", () =>
89 Effect.gen(function* () {
90 const status = yield* checkCodexProviderStatus;
91 assert.strictEqual(status.provider, "codex");
92 assert.strictEqual(status.status, "error");
93 assert.strictEqual(status.available, false);
94 assert.strictEqual(status.authStatus, "unknown");
95 assert.strictEqual(
96 status.message,
97 "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
98 );
99 }).pipe(
100 Effect.provide(
101 mockSpawnerLayer((args) => {
102 const joined = args.join(" ");
103 if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 };
104 throw new Error(`Unexpected args: ${joined}`);
77 (originalCodexHome) =>
78 Effect.sync(() => {
79 if (originalCodexHome !== undefined) {
80 process.env.CODEX_HOME = originalCodexHome;
81 } else {
82 delete process.env.CODEX_HOME;
83 }
84 }),
85 );
86
87 if (configContent !== undefined) {
88 yield* fileSystem.writeFileString(path.join(tmpDir, "config.toml"), configContent);
89 }
90
91 return { tmpDir } as const;
92 });
93}
94
95it.layer(NodeServices.layer)("ProviderHealth", (it) => {
96 // ── checkCodexProviderStatus tests ────────────────────────────────
97 //
98 // These tests control CODEX_HOME to ensure the custom-provider detection
99 // in hasCustomModelProvider() does not interfere with the auth-probe
100 // path being tested.
101
102 describe("checkCodexProviderStatus", () => {
103 it.effect("returns ready when codex is installed and authenticated", () =>
104 Effect.gen(function* () {
105 // Point CODEX_HOME at an empty tmp dir (no config.toml) so the
106 // default code path (OpenAI provider, auth probe runs) is exercised.
107 yield* withTempCodexHome();
108 const status = yield* checkCodexProviderStatus;
109 assert.strictEqual(status.provider, "codex");
110 assert.strictEqual(status.status, "ready");
111 assert.strictEqual(status.available, true);
112 assert.strictEqual(status.authStatus, "authenticated");
113 }).pipe(
114 Effect.provide(
115 mockSpawnerLayer((args) => {
116 const joined = args.join(" ");
117 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
118 if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };
119 throw new Error(`Unexpected args: ${joined}`);
120 }),
121 ),
122 ),
123 );
124
125 it.effect("returns unavailable when codex is missing", () =>
126 Effect.gen(function* () {
127 yield* withTempCodexHome();
128 const status = yield* checkCodexProviderStatus;
129 assert.strictEqual(status.provider, "codex");
130 assert.strictEqual(status.status, "error");
131 assert.strictEqual(status.available, false);
132 assert.strictEqual(status.authStatus, "unknown");
133 assert.strictEqual(status.message, "Codex CLI (`codex`) is not installed or not on PATH.");
134 }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))),
135 );
136
137 it.effect("returns unavailable when codex is below the minimum supported version", () =>
138 Effect.gen(function* () {
139 yield* withTempCodexHome();
140 const status = yield* checkCodexProviderStatus;
141 assert.strictEqual(status.provider, "codex");
142 assert.strictEqual(status.status, "error");
143 assert.strictEqual(status.available, false);
144 assert.strictEqual(status.authStatus, "unknown");
145 assert.strictEqual(
146 status.message,
147 "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",
148 );
149 }).pipe(
150 Effect.provide(
151 mockSpawnerLayer((args) => {
152 const joined = args.join(" ");
153 if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 };
154 throw new Error(`Unexpected args: ${joined}`);
155 }),
156 ),
157 ),
158 );
159
160 it.effect("returns unauthenticated when auth probe reports login required", () =>
161 Effect.gen(function* () {
162 yield* withTempCodexHome();
163 const status = yield* checkCodexProviderStatus;
164 assert.strictEqual(status.provider, "codex");
165 assert.strictEqual(status.status, "error");
166 assert.strictEqual(status.available, true);
167 assert.strictEqual(status.authStatus, "unauthenticated");
168 assert.strictEqual(
169 status.message,
170 "Codex CLI is not authenticated. Run `codex login` and try again.",
171 );
172 }).pipe(
173 Effect.provide(
174 mockSpawnerLayer((args) => {
175 const joined = args.join(" ");
176 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
177 if (joined === "login status") {
178 return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 };
179 }
180 throw new Error(`Unexpected args: ${joined}`);
181 }),
182 ),
183 ),
184 );
185
186 it.effect("returns unauthenticated when login status output includes 'not logged in'", () =>
187 Effect.gen(function* () {
188 yield* withTempCodexHome();
189 const status = yield* checkCodexProviderStatus;
190 assert.strictEqual(status.provider, "codex");
191 assert.strictEqual(status.status, "error");
192 assert.strictEqual(status.available, true);
193 assert.strictEqual(status.authStatus, "unauthenticated");
194 assert.strictEqual(
195 status.message,
196 "Codex CLI is not authenticated. Run `codex login` and try again.",
197 );
198 }).pipe(
199 Effect.provide(
200 mockSpawnerLayer((args) => {
201 const joined = args.join(" ");
202 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
203 if (joined === "login status")
204 return { stdout: "Not logged in\n", stderr: "", code: 1 };
205 throw new Error(`Unexpected args: ${joined}`);
206 }),
207 ),
208 ),
209 );
210
211 it.effect("returns warning when login status command is unsupported", () =>
212 Effect.gen(function* () {
213 yield* withTempCodexHome();
214 const status = yield* checkCodexProviderStatus;
215 assert.strictEqual(status.provider, "codex");
216 assert.strictEqual(status.status, "warning");
217 assert.strictEqual(status.available, true);
218 assert.strictEqual(status.authStatus, "unknown");
219 assert.strictEqual(
220 status.message,
221 "Codex CLI authentication status command is unavailable in this Codex version.",
222 );
223 }).pipe(
224 Effect.provide(
225 mockSpawnerLayer((args) => {
226 const joined = args.join(" ");
227 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
228 if (joined === "login status") {
229 return { stdout: "", stderr: "error: unknown command 'login'", code: 2 };
230 }
231 throw new Error(`Unexpected args: ${joined}`);
232 }),
233 ),
234 ),
235 );
236 });
237
238 // ── Custom model provider: checkCodexProviderStatus integration ───
239
240 describe("checkCodexProviderStatus with custom model provider", () => {
241 it.effect("skips auth probe and returns ready when a custom model provider is configured", () =>
242 Effect.gen(function* () {
243 yield* withTempCodexHome(
244 [
245 'model_provider = "portkey"',
246 "",
247 "[model_providers.portkey]",
248 'base_url = "https://api.portkey.ai/v1"',
249 'env_key = "PORTKEY_API_KEY"',
250 ].join("\n"),
251 );
252 const status = yield* checkCodexProviderStatus;
253 assert.strictEqual(status.provider, "codex");
254 assert.strictEqual(status.status, "ready");
255 assert.strictEqual(status.available, true);
256 assert.strictEqual(status.authStatus, "unknown");
257 assert.strictEqual(
258 status.message,
259 "Using a custom Codex model provider; OpenAI login check skipped.",
260 );
261 }).pipe(
262 Effect.provide(
263 // The spawner only handles --version; if the test attempts
264 // "login status" the throw proves the auth probe was NOT skipped.
265 mockSpawnerLayer((args) => {
266 const joined = args.join(" ");
267 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
268 throw new Error(`Auth probe should have been skipped but got args: ${joined}`);
269 }),
270 ),
271 ),
272 );
273
274 it.effect("still reports error when codex CLI is missing even with custom provider", () =>
275 Effect.gen(function* () {
276 yield* withTempCodexHome(
277 [
278 'model_provider = "portkey"',
279 "",
280 "[model_providers.portkey]",
281 'base_url = "https://api.portkey.ai/v1"',
282 'env_key = "PORTKEY_API_KEY"',
283 ].join("\n"),
284 );
285 const status = yield* checkCodexProviderStatus;
286 assert.strictEqual(status.status, "error");
287 assert.strictEqual(status.available, false);
288 }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))),
289 );
290 });
291
292 describe("checkCodexProviderStatus with openai model provider", () => {
293 it.effect("still runs auth probe when model_provider is openai", () =>
294 Effect.gen(function* () {
295 yield* withTempCodexHome('model_provider = "openai"\n');
296 const status = yield* checkCodexProviderStatus;
297 // The auth probe runs and sees "not logged in" → error
298 assert.strictEqual(status.status, "error");
299 assert.strictEqual(status.authStatus, "unauthenticated");
300 }).pipe(
301 Effect.provide(
302 mockSpawnerLayer((args) => {
303 const joined = args.join(" ");
304 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
305 if (joined === "login status")
306 return { stdout: "Not logged in\n", stderr: "", code: 1 };
307 throw new Error(`Unexpected args: ${joined}`);
308 }),
309 ),
310 ),
311 );
312 });
313
314 // ── parseAuthStatusFromOutput pure tests ──────────────────────────
315
316 describe("parseAuthStatusFromOutput", () => {
317 it("exit code 0 with no auth markers is ready", () => {
318 const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 });
319 assert.strictEqual(parsed.status, "ready");
320 assert.strictEqual(parsed.authStatus, "authenticated");
321 });
322
323 it("JSON with authenticated=false is unauthenticated", () => {
324 const parsed = parseAuthStatusFromOutput({
325 stdout: '[{"authenticated":false}]\n',
326 stderr: "",
327 code: 0,
328 });
329 assert.strictEqual(parsed.status, "error");
330 assert.strictEqual(parsed.authStatus, "unauthenticated");
331 });
332
333 it("JSON without auth marker is warning", () => {
334 const parsed = parseAuthStatusFromOutput({
335 stdout: '[{"ok":true}]\n',
336 stderr: "",
337 code: 0,
338 });
339 assert.strictEqual(parsed.status, "warning");
340 assert.strictEqual(parsed.authStatus, "unknown");
341 });
342 });
343
344 // ── readCodexConfigModelProvider tests ─────────────────────────────
345
346 describe("readCodexConfigModelProvider", () => {
347 it.effect("returns undefined when config file does not exist", () =>
348 Effect.gen(function* () {
349 yield* withTempCodexHome();
350 assert.strictEqual(yield* readCodexConfigModelProvider, undefined);
105351 }),
106 ),
107 ),
108);
109
110it.effect("returns unauthenticated when auth probe reports login required", () =>
111 Effect.gen(function* () {
112 const status = yield* checkCodexProviderStatus;
113 assert.strictEqual(status.provider, "codex");
114 assert.strictEqual(status.status, "error");
115 assert.strictEqual(status.available, true);
116 assert.strictEqual(status.authStatus, "unauthenticated");
117 assert.strictEqual(
118 status.message,
119 "Codex CLI is not authenticated. Run `codex login` and try again.",
120 );
121 }).pipe(
122 Effect.provide(
123 mockSpawnerLayer((args) => {
124 const joined = args.join(" ");
125 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
126 if (joined === "login status") {
127 return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 };
128 }
129 throw new Error(`Unexpected args: ${joined}`);
352 );
353
354 it.effect("returns undefined when config has no model_provider key", () =>
355 Effect.gen(function* () {
356 yield* withTempCodexHome('model = "gpt-5-codex"\n');
357 assert.strictEqual(yield* readCodexConfigModelProvider, undefined);
130358 }),
131 ),
132 ),
133);
134
135it.effect("returns unauthenticated when login status output includes 'not logged in'", () =>
136 Effect.gen(function* () {
137 const status = yield* checkCodexProviderStatus;
138 assert.strictEqual(status.provider, "codex");
139 assert.strictEqual(status.status, "error");
140 assert.strictEqual(status.available, true);
141 assert.strictEqual(status.authStatus, "unauthenticated");
142 assert.strictEqual(
143 status.message,
144 "Codex CLI is not authenticated. Run `codex login` and try again.",
145 );
146 }).pipe(
147 Effect.provide(
148 mockSpawnerLayer((args) => {
149 const joined = args.join(" ");
150 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
151 if (joined === "login status") return { stdout: "Not logged in\n", stderr: "", code: 1 };
152 throw new Error(`Unexpected args: ${joined}`);
359 );
360
361 it.effect("returns the provider when model_provider is set at top level", () =>
362 Effect.gen(function* () {
363 yield* withTempCodexHome('model = "gpt-5-codex"\nmodel_provider = "portkey"\n');
364 assert.strictEqual(yield* readCodexConfigModelProvider, "portkey");
153365 }),
154 ),
155 ),
156);
157
158it.effect("returns warning when login status command is unsupported", () =>
159 Effect.gen(function* () {
160 const status = yield* checkCodexProviderStatus;
161 assert.strictEqual(status.provider, "codex");
162 assert.strictEqual(status.status, "warning");
163 assert.strictEqual(status.available, true);
164 assert.strictEqual(status.authStatus, "unknown");
165 assert.strictEqual(
166 status.message,
167 "Codex CLI authentication status command is unavailable in this Codex version.",
168 );
169 }).pipe(
170 Effect.provide(
171 mockSpawnerLayer((args) => {
172 const joined = args.join(" ");
173 if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
174 if (joined === "login status") {
175 return { stdout: "", stderr: "error: unknown command 'login'", code: 2 };
176 }
177 throw new Error(`Unexpected args: ${joined}`);
366 );
367
368 it.effect("returns openai when model_provider is openai", () =>
369 Effect.gen(function* () {
370 yield* withTempCodexHome('model_provider = "openai"\n');
371 assert.strictEqual(yield* readCodexConfigModelProvider, "openai");
178372 }),
179 ),
180 ),
181);
373 );
182374
183// ── Pure function tests ─────────────────────────────────────────────
375 it.effect("ignores model_provider inside section headers", () =>
376 Effect.gen(function* () {
377 yield* withTempCodexHome(
378 [
379 'model = "gpt-5-codex"',
380 "",
381 "[model_providers.portkey]",
382 'base_url = "https://api.portkey.ai/v1"',
383 'model_provider = "should-be-ignored"',
384 "",
385 ].join("\n"),
386 );
387 assert.strictEqual(yield* readCodexConfigModelProvider, undefined);
388 }),
389 );
184390
185it("parseAuthStatusFromOutput: exit code 0 with no auth markers is ready", () => {
186 const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 });
187 assert.strictEqual(parsed.status, "ready");
188 assert.strictEqual(parsed.authStatus, "authenticated");
189});
391 it.effect("handles comments and whitespace", () =>
392 Effect.gen(function* () {
393 yield* withTempCodexHome(
394 [
395 "# This is a comment",
396 "",
397 ' model_provider = "azure" ',
398 "",
399 "[profiles.deep-review]",
400 'model = "gpt-5-pro"',
401 ].join("\n"),
402 );
403 assert.strictEqual(yield* readCodexConfigModelProvider, "azure");
404 }),
405 );
190406
191it("parseAuthStatusFromOutput: JSON with authenticated=false is unauthenticated", () => {
192 const parsed = parseAuthStatusFromOutput({
193 stdout: '[{"authenticated":false}]\n',
194 stderr: "",
195 code: 0,
407 it.effect("handles single-quoted values in TOML", () =>
408 Effect.gen(function* () {
409 yield* withTempCodexHome("model_provider = 'mistral'\n");
410 assert.strictEqual(yield* readCodexConfigModelProvider, "mistral");
411 }),
412 );
196413 });
197 assert.strictEqual(parsed.status, "error");
198 assert.strictEqual(parsed.authStatus, "unauthenticated");
199});
200414
201it("parseAuthStatusFromOutput: JSON without auth marker is warning", () => {
202 const parsed = parseAuthStatusFromOutput({
203 stdout: '[{"ok":true}]\n',
204 stderr: "",
205 code: 0,
415 // ── hasCustomModelProvider tests ───────────────────────────────────
416
417 describe("hasCustomModelProvider", () => {
418 it.effect("returns false when no config file exists", () =>
419 Effect.gen(function* () {
420 yield* withTempCodexHome();
421 assert.strictEqual(yield* hasCustomModelProvider, false);
422 }),
423 );
424
425 it.effect("returns false when model_provider is not set", () =>
426 Effect.gen(function* () {
427 yield* withTempCodexHome('model = "gpt-5-codex"\n');
428 assert.strictEqual(yield* hasCustomModelProvider, false);
429 }),
430 );
431
432 it.effect("returns false when model_provider is openai", () =>
433 Effect.gen(function* () {
434 yield* withTempCodexHome('model_provider = "openai"\n');
435 assert.strictEqual(yield* hasCustomModelProvider, false);
436 }),
437 );
438
439 it.effect("returns true when model_provider is portkey", () =>
440 Effect.gen(function* () {
441 yield* withTempCodexHome('model_provider = "portkey"\n');
442 assert.strictEqual(yield* hasCustomModelProvider, true);
443 }),
444 );
445
446 it.effect("returns true when model_provider is azure", () =>
447 Effect.gen(function* () {
448 yield* withTempCodexHome('model_provider = "azure"\n');
449 assert.strictEqual(yield* hasCustomModelProvider, true);
450 }),
451 );
452
453 it.effect("returns true when model_provider is ollama", () =>
454 Effect.gen(function* () {
455 yield* withTempCodexHome('model_provider = "ollama"\n');
456 assert.strictEqual(yield* hasCustomModelProvider, true);
457 }),
458 );
459
460 it.effect("returns true when model_provider is a custom proxy", () =>
461 Effect.gen(function* () {
462 yield* withTempCodexHome('model_provider = "my-company-proxy"\n');
463 assert.strictEqual(yield* hasCustomModelProvider, true);
464 }),
465 );
206466 });
207 assert.strictEqual(parsed.status, "warning");
208 assert.strictEqual(parsed.authStatus, "unknown");
209467});
apps/server/src/provider/Layers/ProviderHealth.ts+85-2
......@@ -8,12 +8,13 @@
88 *
99 * @module ProviderHealthLive
1010 */
11import * as OS from "node:os";
1112import type {
1213 ServerProviderAuthStatus,
1314 ServerProviderStatus,
1415 ServerProviderStatusState,
1516} from "@t3tools/contracts";
16import { Array, Effect, Fiber, Layer, Option, Result, Stream } from "effect";
17import { Array, Effect, Fiber, FileSystem, Layer, Option, Path, Result, Stream } from "effect";
1718import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1819
1920import {
......@@ -167,6 +168,72 @@ export function parseAuthStatusFromOutput(result: CommandResult): {
167168 };
168169}
169170
171// ── Codex CLI config detection ──────────────────────────────────────
172
173/**
174 * Providers that use OpenAI-native authentication via `codex login`.
175 * When the configured `model_provider` is one of these, the `codex login
176 * status` probe still runs. For any other provider value the auth probe
177 * is skipped because authentication is handled externally (e.g. via
178 * environment variables like `PORTKEY_API_KEY` or `AZURE_API_KEY`).
179 */
180const OPENAI_AUTH_PROVIDERS = new Set(["openai"]);
181
182/**
183 * Read the `model_provider` value from the Codex CLI config file.
184 *
185 * Looks for the file at `$CODEX_HOME/config.toml` (falls back to
186 * `~/.codex/config.toml`). Uses a simple line-by-line scan rather than
187 * a full TOML parser to avoid adding a dependency for a single key.
188 *
189 * Returns `undefined` when the file does not exist or does not set
190 * `model_provider`.
191 */
192export const readCodexConfigModelProvider = Effect.gen(function* () {
193 const fileSystem = yield* FileSystem.FileSystem;
194 const path = yield* Path.Path;
195 const codexHome = process.env.CODEX_HOME || path.join(OS.homedir(), ".codex");
196 const configPath = path.join(codexHome, "config.toml");
197
198 const content = yield* fileSystem
199 .readFileString(configPath)
200 .pipe(Effect.orElseSucceed(() => undefined));
201 if (content === undefined) {
202 return undefined;
203 }
204
205 // We need to find `model_provider = "..."` at the top level of the
206 // TOML file (i.e. before any `[section]` header). Lines inside
207 // `[profiles.*]`, `[model_providers.*]`, etc. are ignored.
208 let inTopLevel = true;
209 for (const line of content.split("\n")) {
210 const trimmed = line.trim();
211 // Skip comments and empty lines.
212 if (!trimmed || trimmed.startsWith("#")) continue;
213 // Detect section headers — once we leave the top level, stop.
214 if (trimmed.startsWith("[")) {
215 inTopLevel = false;
216 continue;
217 }
218 if (!inTopLevel) continue;
219
220 const match = trimmed.match(/^model_provider\s*=\s*["']([^"']+)["']/);
221 if (match) return match[1];
222 }
223 return undefined;
224});
225
226/**
227 * Returns `true` when the Codex CLI is configured with a custom
228 * (non-OpenAI) model provider, meaning `codex login` auth is not
229 * required because authentication is handled through provider-specific
230 * environment variables.
231 */
232export const hasCustomModelProvider = Effect.map(
233 readCodexConfigModelProvider,
234 (provider) => provider !== undefined && !OPENAI_AUTH_PROVIDERS.has(provider),
235);
236
170237// ── Effect-native command execution ─────────────────────────────────
171238
172239const collectStreamAsString = <E>(stream: Stream.Stream<Uint8Array, E>): Effect.Effect<string, E> =>
......@@ -202,7 +269,7 @@ const runCodexCommand = (args: ReadonlyArray<string>) =>
202269export const checkCodexProviderStatus: Effect.Effect<
203270 ServerProviderStatus,
204271 never,
205 ChildProcessSpawner.ChildProcessSpawner
272 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
206273> = Effect.gen(function* () {
207274 const checkedAt = new Date().toISOString();
208275
......@@ -265,6 +332,22 @@ export const checkCodexProviderStatus: Effect.Effect<
265332 }
266333
267334 // Probe 2: `codex login status` — is the user authenticated?
335 //
336 // Custom model providers (e.g. Portkey, Azure OpenAI proxy) handle
337 // authentication through their own environment variables, so `codex
338 // login status` will report "not logged in" even when the CLI works
339 // fine. Skip the auth probe entirely for non-OpenAI providers.
340 if (yield* hasCustomModelProvider) {
341 return {
342 provider: CODEX_PROVIDER,
343 status: "ready" as const,
344 available: true,
345 authStatus: "unknown" as const,
346 checkedAt,
347 message: "Using a custom Codex model provider; OpenAI login check skipped.",
348 } satisfies ServerProviderStatus;
349 }
350
268351 const authProbe = yield* runCodexCommand(["login", "status"]).pipe(
269352 Effect.timeoutOption(DEFAULT_TIMEOUT_MS),
270353 Effect.result,