{"record":{"id":"36848f335303dc81","repo":"BigPizzaV3/CodexPlusPlus","slug":"api-auth-json-openai-api-key","errorCode":null,"errorMessage":"官方混合 API 不应在 auth.json 中保存 OPENAI_API_KEY。请清理此供应商的 auth.json 后再切换。","messagePattern":"官方混合 API 不应在 auth\\.json 中保存 OPENAI_API_KEY。请清理此供应商的 auth\\.json 后再切换。","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/codex-plus-core/src/relay_switch.rs","lineNumber":190,"sourceCode":"                profile.id.as_str()\n            } else {\n                profile.name.as_str()\n            }\n        );\n    }\n    if profile.relay_mode == RelayMode::Official\n        && serde_json::from_str::<serde_json::Value>(&profile.auth_contents)\n            .ok()\n            .and_then(|value| {\n                value\n                    .get(\"OPENAI_API_KEY\")\n                    .and_then(serde_json::Value::as_str)\n                    .map(str::trim)\n                    .map(str::is_empty)\n            })\n            == Some(false)\n    {\n        anyhow::bail!(\n            \"官方混合 API 不应在 auth.json 中保存 OPENAI_API_KEY。请清理此供应商的 auth.json 后再切换。\"\n        );\n    }\n    Ok(())\n}\n\nfn relay_combined_common_config(settings: &BackendSettings) -> String {\n    let sections = [\n        settings.relay_common_config_contents.trim(),\n        settings.relay_context_config_contents.trim(),\n    ]\n    .into_iter()\n    .filter(|section| !section.is_empty())\n    .collect::<Vec<_>>();\n    if sections.is_empty() {\n        String::new()\n    } else {\n        crate::relay_config::normalize_config_text(&format!(\"{}\\n\", sections.join(\"\\n\\n\")))","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/BigPizzaV3/CodexPlusPlus/blob/1f431ae49b57b3055e0e6845ba6156c6b4232b4d/crates/codex-plus-core/src/relay_switch.rs#L172-L208","documentation":"Thrown by validate_switch_profile_files during apply_selected_relay_profile (called from switch_relay_profile_in_home). It fires when a provider profile with relay_mode == Official and official_mix_api_key == true carries an auth_contents JSON object whose OPENAI_API_KEY is a non-empty string. The official 'hybrid' mode is expected to authenticate with ChatGPT OAuth tokens, not an API key, so the switch is aborted before any config.toml or auth.json is written to keep the previous provider active.","triggerScenarios":"Calling switch_relay_profile_in_home (or the bridge '/settings/switch' style flow that reaches apply_selected_relay_profile) for a RelayProfile where relay_mode == RelayMode::Official, official_mix_api_key == true, and serde_json::from_str(profile.auth_contents) yields an object with a string field OPENAI_API_KEY whose trimmed value is non-empty. Note: Official profiles with official_mix_api_key == false never reach this check (they take the clear-config branch at relay_switch.rs:137).","commonSituations":"Importing a provider from ccs_import/provider_import, which generates auth_contents as {\"OPENAI_API_KEY\": \"sk-...\"}, and then flipping that profile to Official hybrid mode; hand-editing the profile's auth.json in the manager UI and pasting an API key; migrating an old Pure API profile to Official mode without clearing the key.","solutions":["Open the profile in the manager and remove the OPENAI_API_KEY entry from its auth.json (auth_contents), leaving {} or the OAuth tokens, then switch again","If the profile really is a Pure API provider, set its relay_mode to PureApi instead of Official","If you intended key-based official access, set official_mix_api_key = true is already required; the key must live in config.toml (model_provider apiKey), not auth.json — move it there and clear auth_contents","Programmatically strip the field before switching: parse auth_contents, remove OPENAI_API_KEY, save the profile"],"exampleFix":"// before (profile.auth_contents)\n{\"OPENAI_API_KEY\":\"sk-abc123\"}\n// after\n{}","handlingStrategy":"validation","validationCode":"fn can_switch_official_mix(profile: &RelayProfile) -> bool {\n    if profile.relay_mode != RelayMode::Official || !profile.official_mix_api_key {\n        return true;\n    }\n    serde_json::from_str::<serde_json::Value>(&profile.auth_contents)\n        .ok()\n        .and_then(|v| v.get(\"OPENAI_API_KEY\").and_then(Value::as_str))\n        .map(|k| k.trim().is_empty())\n        .unwrap_or(true)\n}\n\nif !can_switch_official_mix(&profile) {\n    // surface a form error instead of calling switch_relay_profile_in_home\n}","typeGuard":null,"tryCatchPattern":"match switch_relay_profile_in_home(home, &mut settings) {\n    Err(e) if e.to_string().contains(\"OPENAI_API_KEY\") => {\n        // prompt user to clean the profile's auth.json; keep previous provider active\n    }\n    other => other?,\n}","preventionTips":["Never let provider_import/ccs_import output feed an Official hybrid profile unchanged — strip OPENAI_API_KEY on import","When toggling a profile to Official mode in the UI, auto-clear auth_contents or block the toggle until cleaned","Run the same JSON check in a pre-save validation hook so the profile is never persisted in a state that cannot switch"],"tags":["relay-switch","auth-json","official-mode","config-validation","codex-plus-core"],"backgroundTag":"auth-config-conflict","analyzedSha":"1f431ae49b57b3055e0e6845ba6156c6b4232b4d","analyzedAt":"2026-08-16T20:54:18.598Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}