fix(cli-tools): send the API key Codex actually reads

Codex only authenticates a custom model provider from env_key,
http_headers, env_http_headers or a token command — auth.json is
read solely by the built-in openai provider. Writing OPENAI_API_KEY
there left every request unauthenticated (401 Missing API key) while
clobbering an existing ChatGPT login.

Put the key in [model_providers.9router.http_headers] instead, and
drop the auth.json write. Also move the subagent model to the
agents.default_subagent_model scalar: agents.<role> now declares a
custom role and requires a description, so the old [agents.subagent]
table was discarded with a startup warning. DELETE still clears
auth.json to repair machines configured by the previous version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
decolua
2026-08-27 18:52:32 +07:00
parent e6f5724b4b
commit 9c45b27cd7
2 changed files with 14 additions and 32 deletions

View File

@@ -57,7 +57,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
if (modelMatch) setSelectedModel(modelMatch[1]); if (modelMatch) setSelectedModel(modelMatch[1]);
// Parse subagent settings // Parse subagent settings
const subagentModelMatch = codexStatus.config.match(/\[agents\.subagent\]\s*\n\s*model\s*=\s*"([^"]+)"/m); const subagentModelMatch = codexStatus.config.match(/^default_subagent_model\s*=\s*"([^"]+)"/m);
if (subagentModelMatch) setSubagentModel(subagentModelMatch[1]); if (subagentModelMatch) setSubagentModel(subagentModelMatch[1]);
} }
}, [codexStatus]); }, [codexStatus]);
@@ -172,24 +172,18 @@ name = "9Router"
base_url = "${getEffectiveBaseUrl()}" base_url = "${getEffectiveBaseUrl()}"
wire_api = "responses" wire_api = "responses"
[agents.subagent] [model_providers.9router.http_headers]
model = "${effectiveSubagentModel}" Authorization = "Bearer ${keyToUse}"
`;
const authContent = JSON.stringify({ [agents]
auth_mode: "apikey", default_subagent_model = "${effectiveSubagentModel}"
OPENAI_API_KEY: keyToUse `;
}, null, 2);
return [ return [
{ {
filename: "~/.codex/config.toml", filename: "~/.codex/config.toml",
content: configContent, content: configContent,
}, },
{
filename: "~/.codex/auth.json",
content: authContent,
},
]; ];
}; };
@@ -254,7 +248,7 @@ model = "${effectiveSubagentModel}"
<p className="text-text-muted">After installation, run <code className="px-1 bg-black/5 dark:bg-white/5 rounded">codex</code> to verify.</p> <p className="text-text-muted">After installation, run <code className="px-1 bg-black/5 dark:bg-white/5 rounded">codex</code> to verify.</p>
<div className="pt-2 border-t border-border"> <div className="pt-2 border-t border-border">
<p className="text-text-muted text-xs"> <p className="text-text-muted text-xs">
Codex uses <code className="px-1 bg-black/5 dark:bg-white/5 rounded">~/.codex/auth.json</code> with <code className="px-1 bg-black/5 dark:bg-white/5 rounded">OPENAI_API_KEY</code>. Codex reads custom providers from <code className="px-1 bg-black/5 dark:bg-white/5 rounded">~/.codex/config.toml</code>.
Click &quot;Apply&quot; to auto-configure. Click &quot;Apply&quot; to auto-configure.
</p> </p>
</div> </div>

View File

@@ -135,35 +135,22 @@ export async function POST(request) {
// Update or create 9router provider section (no api_key - Codex reads from auth.json) // Update or create 9router provider section (no api_key - Codex reads from auth.json)
// Ensure /v1 suffix is added only once // Ensure /v1 suffix is added only once
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
// Custom providers ignore auth.json - the key must travel as a static header
setNestedSection(parsed, "model_providers.9router", { setNestedSection(parsed, "model_providers.9router", {
name: "9Router", name: "9Router",
base_url: normalizedBaseUrl, base_url: normalizedBaseUrl,
wire_api: "responses", wire_api: "responses",
http_headers: { Authorization: `Bearer ${apiKey}` },
}); });
// Add subagent configuration // Subagent model is a scalar under [agents]; agents.<role> now means a custom role
const effectiveSubagentModel = subagentModel || model; deleteNestedSection(parsed, "agents.subagent");
setNestedSection(parsed, "agents.subagent", { setNestedSection(parsed, "agents.default_subagent_model", subagentModel || model);
model: effectiveSubagentModel,
});
// Write merged config // Write merged config
const configContent = stringifyTOML(parsed); const configContent = stringifyTOML(parsed);
await fs.writeFile(configPath, configContent); await fs.writeFile(configPath, configContent);
// Update auth.json with OPENAI_API_KEY (Codex reads this first)
const authPath = getCodexAuthPath();
let authData = {};
try {
const existingAuth = await fs.readFile(authPath, "utf-8");
authData = JSON.parse(existingAuth);
} catch { /* No existing auth */ }
// Force apikey mode (keep existing tokens untouched for ChatGPT login reuse)
authData.OPENAI_API_KEY = apiKey;
authData.auth_mode = "apikey";
await fs.writeFile(authPath, JSON.stringify(authData, null, 2));
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: "Codex settings applied successfully!", message: "Codex settings applied successfully!",
@@ -204,7 +191,8 @@ export async function DELETE() {
// Remove 9router provider section // Remove 9router provider section
deleteNestedSection(parsed, "model_providers.9router"); deleteNestedSection(parsed, "model_providers.9router");
// Remove subagent configuration // Remove subagent configuration (both the current key and the legacy role form)
deleteNestedSection(parsed, "agents.default_subagent_model");
deleteNestedSection(parsed, "agents.subagent"); deleteNestedSection(parsed, "agents.subagent");
// Write updated config // Write updated config