mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 23:16:26 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcae0852df |
+13
-1
@@ -4,7 +4,7 @@ DATABASE_POOL_SIZE=10
|
||||
|
||||
# LLM Provider
|
||||
# LLM_BACKEND=nearai # default
|
||||
# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil
|
||||
# Possible values: nearai, ollama, openai_compatible, openai, anthropic, tinfoil, openai_codex
|
||||
|
||||
# === NEAR AI (Chat Completions API) ===
|
||||
# Two auth modes:
|
||||
@@ -57,6 +57,18 @@ NEARAI_AUTH_URL=https://private.near.ai
|
||||
# LLM_BASE_URL=https://api.fireworks.ai/inference/v1
|
||||
# LLM_API_KEY=fw_...
|
||||
|
||||
# === OpenAI Codex (Responses API) ===
|
||||
# Two auth modes:
|
||||
# 1. API key: Standard OpenAI billing (api.openai.com/v1/responses)
|
||||
# 2. Codex CLI OAuth: ChatGPT subscription billing (chatgpt.com)
|
||||
# Reads token from ~/.codex/auth.json (or $CODEX_HOME/auth.json)
|
||||
# OPENAI_CODEX_MODEL=gpt-5.3-codex
|
||||
# LLM_BACKEND=openai_codex
|
||||
# OPENAI_CODEX_API_KEY=sk-... # API key mode
|
||||
# CODEX_AUTH_PATH=~/.codex/auth.json # OAuth mode (default path)
|
||||
# OPENAI_CODEX_ACCOUNT_ID=... # Required for ChatGPT endpoint
|
||||
# OPENAI_CODEX_BASE_URL=... # Override base URL
|
||||
|
||||
# For full provider setup guide see docs/LLM_PROVIDERS.md
|
||||
|
||||
# Channel Configuration
|
||||
|
||||
+263
-1
@@ -25,6 +25,8 @@ pub enum LlmBackend {
|
||||
OpenAiCompatible,
|
||||
/// Tinfoil private inference
|
||||
Tinfoil,
|
||||
/// OpenAI Codex via Responses API (ChatGPT OAuth or API key)
|
||||
OpenAiCodex,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for LlmBackend {
|
||||
@@ -38,8 +40,9 @@ impl std::str::FromStr for LlmBackend {
|
||||
"ollama" => Ok(Self::Ollama),
|
||||
"openai_compatible" | "openai-compatible" | "compatible" => Ok(Self::OpenAiCompatible),
|
||||
"tinfoil" => Ok(Self::Tinfoil),
|
||||
"openai_codex" | "codex" => Ok(Self::OpenAiCodex),
|
||||
_ => Err(format!(
|
||||
"invalid LLM backend '{}', expected one of: nearai, openai, anthropic, ollama, openai_compatible, tinfoil",
|
||||
"invalid LLM backend '{}', expected one of: nearai, openai, anthropic, ollama, openai_compatible, tinfoil, openai_codex",
|
||||
s
|
||||
)),
|
||||
}
|
||||
@@ -55,6 +58,7 @@ impl std::fmt::Display for LlmBackend {
|
||||
Self::Ollama => write!(f, "ollama"),
|
||||
Self::OpenAiCompatible => write!(f, "openai_compatible"),
|
||||
Self::Tinfoil => write!(f, "tinfoil"),
|
||||
Self::OpenAiCodex => write!(f, "openai_codex"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +106,29 @@ pub struct TinfoilConfig {
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
/// Configuration for OpenAI Codex via Responses API.
|
||||
///
|
||||
/// Supports two auth modes:
|
||||
/// - **API key**: Standard OpenAI billing via `api.openai.com/v1/responses`
|
||||
/// - **OAuth**: ChatGPT subscription billing via `chatgpt.com/backend-api/codex/responses`,
|
||||
/// using tokens from the Codex CLI (`~/.codex/auth.json`)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpenAiCodexConfig {
|
||||
/// Model name (default: "gpt-5.3-codex").
|
||||
pub model: String,
|
||||
/// Base URL. Defaults based on auth mode:
|
||||
/// - API key: `https://api.openai.com/v1`
|
||||
/// - OAuth: `https://chatgpt.com/backend-api/codex`
|
||||
pub base_url: String,
|
||||
/// API key for api.openai.com (standard billing).
|
||||
pub api_key: Option<SecretString>,
|
||||
/// Path to Codex CLI auth.json for OAuth tokens.
|
||||
/// Default: `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`).
|
||||
pub auth_path: PathBuf,
|
||||
/// OpenAI account ID (required for ChatGPT endpoint).
|
||||
pub account_id: Option<String>,
|
||||
}
|
||||
|
||||
/// LLM provider configuration.
|
||||
///
|
||||
/// NEAR AI remains the default backend. Users can switch to other providers
|
||||
@@ -122,6 +149,8 @@ pub struct LlmConfig {
|
||||
pub openai_compatible: Option<OpenAiCompatibleConfig>,
|
||||
/// Tinfoil config (populated when backend=tinfoil)
|
||||
pub tinfoil: Option<TinfoilConfig>,
|
||||
/// OpenAI Codex config (populated when backend=openai_codex)
|
||||
pub openai_codex: Option<OpenAiCodexConfig>,
|
||||
}
|
||||
|
||||
/// NEAR AI configuration.
|
||||
@@ -325,6 +354,32 @@ impl LlmConfig {
|
||||
None
|
||||
};
|
||||
|
||||
let openai_codex = if backend == LlmBackend::OpenAiCodex {
|
||||
let api_key = optional_env("OPENAI_CODEX_API_KEY")?.map(SecretString::from);
|
||||
let model =
|
||||
optional_env("OPENAI_CODEX_MODEL")?.unwrap_or_else(|| "gpt-5.3-codex".to_string());
|
||||
let auth_path = optional_env("CODEX_AUTH_PATH")?
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(default_codex_auth_path);
|
||||
let account_id = optional_env("OPENAI_CODEX_ACCOUNT_ID")?;
|
||||
let base_url = optional_env("OPENAI_CODEX_BASE_URL")?.unwrap_or_else(|| {
|
||||
if api_key.is_some() {
|
||||
"https://api.openai.com/v1".to_string()
|
||||
} else {
|
||||
"https://chatgpt.com/backend-api/codex".to_string()
|
||||
}
|
||||
});
|
||||
Some(OpenAiCodexConfig {
|
||||
model,
|
||||
base_url,
|
||||
api_key,
|
||||
auth_path,
|
||||
account_id,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
nearai,
|
||||
@@ -333,6 +388,7 @@ impl LlmConfig {
|
||||
ollama,
|
||||
openai_compatible,
|
||||
tinfoil,
|
||||
openai_codex,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -371,6 +427,49 @@ fn parse_extra_headers(val: &str) -> Result<Vec<(String, String)>, ConfigError>
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Get the default Codex CLI auth.json path.
|
||||
///
|
||||
/// Respects `$CODEX_HOME` if set, otherwise defaults to `~/.codex/auth.json`.
|
||||
fn default_codex_auth_path() -> PathBuf {
|
||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||
return PathBuf::from(codex_home).join("auth.json");
|
||||
}
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".codex")
|
||||
.join("auth.json")
|
||||
}
|
||||
|
||||
/// Extract an OAuth access token from a Codex CLI `auth.json` file.
|
||||
///
|
||||
/// Tries fields in order: `tokens.access_token`, `token`, `api_key`, `access_token`.
|
||||
/// Returns `None` on any failure (file not found, parse error, no matching field).
|
||||
pub fn extract_codex_oauth_token(auth_path: &std::path::Path) -> Option<String> {
|
||||
let content = std::fs::read_to_string(auth_path).ok()?;
|
||||
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||
|
||||
// Try nested tokens.access_token first (Codex CLI format)
|
||||
if let Some(token) = json
|
||||
.get("tokens")
|
||||
.and_then(|t| t.get("access_token"))
|
||||
.and_then(|v| v.as_str())
|
||||
&& !token.is_empty()
|
||||
{
|
||||
return Some(token.to_string());
|
||||
}
|
||||
|
||||
// Try top-level fields
|
||||
for field in &["token", "api_key", "access_token"] {
|
||||
if let Some(val) = json.get(field).and_then(|v| v.as_str())
|
||||
&& !val.is_empty()
|
||||
{
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the default session file path (~/.ironclaw/session.json).
|
||||
fn default_session_path() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
@@ -508,4 +607,167 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear codex-related env vars for testing.
|
||||
fn clear_codex_env() {
|
||||
// SAFETY: Only called under ENV_MUTEX in tests.
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_BACKEND");
|
||||
std::env::remove_var("OPENAI_CODEX_API_KEY");
|
||||
std::env::remove_var("OPENAI_CODEX_MODEL");
|
||||
std::env::remove_var("OPENAI_CODEX_BASE_URL");
|
||||
std::env::remove_var("OPENAI_CODEX_ACCOUNT_ID");
|
||||
std::env::remove_var("CODEX_AUTH_PATH");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_defaults_model_and_oauth_base_url() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_codex_env();
|
||||
|
||||
let settings = Settings {
|
||||
llm_backend: Some("openai_codex".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg = LlmConfig::resolve(&settings).expect("resolve should succeed");
|
||||
let codex = cfg.openai_codex.expect("codex config should be present");
|
||||
|
||||
assert_eq!(codex.model, "gpt-5.3-codex");
|
||||
// No API key → OAuth mode → ChatGPT base URL
|
||||
assert!(codex.api_key.is_none());
|
||||
assert_eq!(codex.base_url, "https://chatgpt.com/backend-api/codex");
|
||||
assert!(codex.auth_path.to_string_lossy().contains("auth.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_api_key_sets_openai_base_url() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_codex_env();
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::set_var("OPENAI_CODEX_API_KEY", "sk-test-key");
|
||||
}
|
||||
|
||||
let settings = Settings {
|
||||
llm_backend: Some("openai_codex".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg = LlmConfig::resolve(&settings).expect("resolve should succeed");
|
||||
let codex = cfg.openai_codex.expect("codex config should be present");
|
||||
|
||||
assert!(codex.api_key.is_some());
|
||||
assert_eq!(codex.base_url, "https://api.openai.com/v1");
|
||||
|
||||
// Cleanup
|
||||
unsafe {
|
||||
std::env::remove_var("OPENAI_CODEX_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_env_vars_override_defaults() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_codex_env();
|
||||
// SAFETY: Under ENV_MUTEX.
|
||||
unsafe {
|
||||
std::env::set_var("OPENAI_CODEX_MODEL", "gpt-5.1-codex");
|
||||
std::env::set_var("OPENAI_CODEX_BASE_URL", "https://custom.example.com/v1");
|
||||
std::env::set_var("OPENAI_CODEX_ACCOUNT_ID", "acct_123");
|
||||
std::env::set_var("CODEX_AUTH_PATH", "/tmp/test-auth.json");
|
||||
}
|
||||
|
||||
let settings = Settings {
|
||||
llm_backend: Some("openai_codex".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg = LlmConfig::resolve(&settings).expect("resolve should succeed");
|
||||
let codex = cfg.openai_codex.expect("codex config should be present");
|
||||
|
||||
assert_eq!(codex.model, "gpt-5.1-codex");
|
||||
assert_eq!(codex.base_url, "https://custom.example.com/v1");
|
||||
assert_eq!(codex.account_id.as_deref(), Some("acct_123"));
|
||||
assert_eq!(
|
||||
codex.auth_path,
|
||||
std::path::PathBuf::from("/tmp/test-auth.json")
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
unsafe {
|
||||
std::env::remove_var("OPENAI_CODEX_MODEL");
|
||||
std::env::remove_var("OPENAI_CODEX_BASE_URL");
|
||||
std::env::remove_var("OPENAI_CODEX_ACCOUNT_ID");
|
||||
std::env::remove_var("CODEX_AUTH_PATH");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_not_populated_for_other_backends() {
|
||||
let _guard = ENV_MUTEX.lock().expect("env mutex poisoned");
|
||||
clear_codex_env();
|
||||
|
||||
let settings = Settings {
|
||||
llm_backend: Some("nearai".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cfg = LlmConfig::resolve(&settings).expect("resolve should succeed");
|
||||
assert!(cfg.openai_codex.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_codex_oauth_token_nested() {
|
||||
let dir = std::env::temp_dir().join("ironclaw-test-codex");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("auth-nested.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{"tokens":{"access_token":"oauth-tok-123","refresh_token":"rt_456"}}"#,
|
||||
)
|
||||
.expect("write test file");
|
||||
|
||||
let token = extract_codex_oauth_token(&path);
|
||||
assert_eq!(token, Some("oauth-tok-123".to_string()));
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_codex_oauth_token_flat() {
|
||||
let dir = std::env::temp_dir().join("ironclaw-test-codex");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("auth-flat.json");
|
||||
std::fs::write(&path, r#"{"token":"flat-tok-789"}"#).expect("write test file");
|
||||
|
||||
let token = extract_codex_oauth_token(&path);
|
||||
assert_eq!(token, Some("flat-tok-789".to_string()));
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_codex_oauth_token_missing_file() {
|
||||
let path = std::path::Path::new("/tmp/ironclaw-nonexistent-auth.json");
|
||||
assert!(extract_codex_oauth_token(path).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_codex_oauth_token_empty_fields() {
|
||||
let dir = std::env::temp_dir().join("ironclaw-test-codex");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let path = dir.join("auth-empty.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
r#"{"tokens":{"access_token":""},"token":"","api_key":""}"#,
|
||||
)
|
||||
.expect("write test file");
|
||||
|
||||
let token = extract_codex_oauth_token(&path);
|
||||
assert!(token.is_none());
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,8 +37,8 @@ pub use self::embeddings::EmbeddingsConfig;
|
||||
pub use self::heartbeat::HeartbeatConfig;
|
||||
pub use self::hygiene::HygieneConfig;
|
||||
pub use self::llm::{
|
||||
AnthropicDirectConfig, LlmBackend, LlmConfig, NearAiConfig, OllamaConfig,
|
||||
OpenAiCompatibleConfig, OpenAiDirectConfig, TinfoilConfig,
|
||||
AnthropicDirectConfig, LlmBackend, LlmConfig, NearAiConfig, OllamaConfig, OpenAiCodexConfig,
|
||||
OpenAiCompatibleConfig, OpenAiDirectConfig, TinfoilConfig, extract_codex_oauth_token,
|
||||
};
|
||||
pub use self::routines::RoutineConfig;
|
||||
pub use self::safety::SafetyConfig;
|
||||
@@ -220,6 +220,7 @@ pub async fn inject_llm_keys_from_secrets(
|
||||
("llm_anthropic_api_key", "ANTHROPIC_API_KEY"),
|
||||
("llm_compatible_api_key", "LLM_API_KEY"),
|
||||
("llm_nearai_api_key", "NEARAI_API_KEY"),
|
||||
("llm_codex_api_key", "OPENAI_CODEX_API_KEY"),
|
||||
];
|
||||
|
||||
let mut injected = HashMap::new();
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod circuit_breaker;
|
||||
pub mod costs;
|
||||
pub mod failover;
|
||||
mod nearai_chat;
|
||||
pub mod openai_codex;
|
||||
mod provider;
|
||||
mod reasoning;
|
||||
pub mod response_cache;
|
||||
@@ -43,6 +44,7 @@ use secrecy::ExposeSecret;
|
||||
|
||||
use crate::config::{LlmBackend, LlmConfig, NearAiConfig};
|
||||
use crate::error::LlmError;
|
||||
use crate::llm::openai_codex::OpenAiCodexProvider;
|
||||
|
||||
/// Create an LLM provider based on configuration.
|
||||
///
|
||||
@@ -60,6 +62,7 @@ pub fn create_llm_provider(
|
||||
LlmBackend::Ollama => create_ollama_provider(config),
|
||||
LlmBackend::OpenAiCompatible => create_openai_compatible_provider(config),
|
||||
LlmBackend::Tinfoil => create_tinfoil_provider(config),
|
||||
LlmBackend::OpenAiCodex => create_openai_codex_provider(config),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +268,28 @@ fn create_openai_compatible_provider(config: &LlmConfig) -> Result<Arc<dyn LlmPr
|
||||
Ok(Arc::new(RigAdapter::new(model, &compat.model)))
|
||||
}
|
||||
|
||||
fn create_openai_codex_provider(config: &LlmConfig) -> Result<Arc<dyn LlmProvider>, LlmError> {
|
||||
let codex = config
|
||||
.openai_codex
|
||||
.as_ref()
|
||||
.ok_or_else(|| LlmError::AuthFailed {
|
||||
provider: "openai_codex".to_string(),
|
||||
})?;
|
||||
|
||||
let auth_mode = if codex.api_key.is_some() {
|
||||
"API key"
|
||||
} else {
|
||||
"OAuth (Codex CLI)"
|
||||
};
|
||||
tracing::info!(
|
||||
model = %codex.model,
|
||||
base_url = %codex.base_url,
|
||||
auth = auth_mode,
|
||||
"Using OpenAI Codex (Responses API)"
|
||||
);
|
||||
Ok(Arc::new(OpenAiCodexProvider::new(codex.clone())?))
|
||||
}
|
||||
|
||||
/// Create a cheap/fast LLM provider for lightweight tasks (heartbeat, routing, evaluation).
|
||||
///
|
||||
/// Uses `NEARAI_CHEAP_MODEL` if set, otherwise falls back to the main provider.
|
||||
@@ -472,6 +497,7 @@ mod tests {
|
||||
ollama: None,
|
||||
openai_compatible: None,
|
||||
tinfoil: None,
|
||||
openai_codex: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+108
-1
@@ -751,6 +751,7 @@ impl SetupWizard {
|
||||
"openai" => "OpenAI",
|
||||
"ollama" => "Ollama (local)",
|
||||
"openai_compatible" => "OpenAI-compatible endpoint",
|
||||
"openai_codex" => "OpenAI Codex (Responses API)",
|
||||
other => other,
|
||||
}
|
||||
};
|
||||
@@ -759,7 +760,7 @@ impl SetupWizard {
|
||||
|
||||
let is_known = matches!(
|
||||
current.as_str(),
|
||||
"nearai" | "anthropic" | "openai" | "ollama" | "openai_compatible"
|
||||
"nearai" | "anthropic" | "openai" | "ollama" | "openai_compatible" | "openai_codex"
|
||||
);
|
||||
|
||||
if is_known && confirm("Keep current provider?", true).map_err(SetupError::Io)? {
|
||||
@@ -773,6 +774,7 @@ impl SetupWizard {
|
||||
"openai" => return self.setup_openai().await,
|
||||
"ollama" => return self.setup_ollama(),
|
||||
"openai_compatible" => return self.setup_openai_compatible().await,
|
||||
"openai_codex" => return self.setup_openai_codex().await,
|
||||
_ => {
|
||||
return Err(SetupError::Config(format!(
|
||||
"Unhandled provider: {}",
|
||||
@@ -800,6 +802,7 @@ impl SetupWizard {
|
||||
"Ollama - local models, no API key needed",
|
||||
"OpenRouter - 200+ models via single API key",
|
||||
"OpenAI-compatible - custom endpoint (vLLM, LiteLLM, etc.)",
|
||||
"OpenAI Codex - Responses API (ChatGPT OAuth or API key)",
|
||||
];
|
||||
|
||||
let choice = select_one("Provider:", options).map_err(SetupError::Io)?;
|
||||
@@ -811,6 +814,7 @@ impl SetupWizard {
|
||||
3 => self.setup_ollama()?,
|
||||
4 => self.setup_openrouter().await?,
|
||||
5 => self.setup_openai_compatible().await?,
|
||||
6 => self.setup_openai_codex().await?,
|
||||
_ => return Err(SetupError::Config("Invalid provider selection".to_string())),
|
||||
}
|
||||
|
||||
@@ -1066,6 +1070,100 @@ impl SetupWizard {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// OpenAI Codex provider setup: API key or Codex CLI OAuth.
|
||||
async fn setup_openai_codex(&mut self) -> Result<(), SetupError> {
|
||||
self.settings.llm_backend = Some("openai_codex".to_string());
|
||||
if self.settings.selected_model.is_some() {
|
||||
self.settings.selected_model = None;
|
||||
}
|
||||
|
||||
let auth_options = &[
|
||||
"API key - standard OpenAI billing (api.openai.com)",
|
||||
"Codex CLI OAuth - ChatGPT subscription billing (~/.codex/auth.json)",
|
||||
];
|
||||
|
||||
let auth_choice =
|
||||
select_one("Authentication mode:", auth_options).map_err(SetupError::Io)?;
|
||||
|
||||
match auth_choice {
|
||||
0 => {
|
||||
// API key mode — delegate to shared helper
|
||||
self.setup_api_key_provider(
|
||||
"openai_codex",
|
||||
"OPENAI_CODEX_API_KEY",
|
||||
"llm_codex_api_key",
|
||||
"OpenAI API key (for Codex)",
|
||||
"https://platform.openai.com/api-keys",
|
||||
Some("OpenAI Codex"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
1 => {
|
||||
// OAuth mode — read from Codex CLI auth.json
|
||||
let auth_path = std::env::var("CODEX_AUTH_PATH").unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".codex")
|
||||
.join("auth.json")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let path = std::path::Path::new(&auth_path);
|
||||
match crate::config::extract_codex_oauth_token(path) {
|
||||
Some(token) => {
|
||||
print_info(&format!(
|
||||
"Found Codex OAuth token: {}",
|
||||
mask_api_key(&token)
|
||||
));
|
||||
if !confirm("Use this token?", true).map_err(SetupError::Io)? {
|
||||
return Err(SetupError::Cancelled);
|
||||
}
|
||||
print_success("OpenAI Codex configured (OAuth from Codex CLI)");
|
||||
}
|
||||
None => {
|
||||
print_error(&format!("No Codex OAuth token found at {}", path.display()));
|
||||
print_info(
|
||||
"Run `npx codex --full-setup` to authenticate, then retry setup.",
|
||||
);
|
||||
if confirm("Retry after authenticating?", true).map_err(SetupError::Io)? {
|
||||
// Check again after user's action
|
||||
match crate::config::extract_codex_oauth_token(path) {
|
||||
Some(_) => {
|
||||
print_success("OpenAI Codex configured (OAuth from Codex CLI)");
|
||||
}
|
||||
None => {
|
||||
return Err(SetupError::Auth(format!(
|
||||
"Still no token found at {}. Run Codex CLI setup first.",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(SetupError::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for account ID (required for ChatGPT endpoint)
|
||||
let account_id = optional_input(
|
||||
"OpenAI account ID (for ChatGPT endpoint, optional)",
|
||||
Some("leave blank if unknown"),
|
||||
)
|
||||
.map_err(SetupError::Io)?;
|
||||
|
||||
if let Some(ref id) = account_id
|
||||
&& !id.is_empty()
|
||||
{
|
||||
print_info(&format!("Account ID: {}", id));
|
||||
}
|
||||
}
|
||||
_ => return Err(SetupError::Config("Invalid auth choice".to_string())),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Step 4: Model selection.
|
||||
///
|
||||
/// Branches on the selected LLM backend and fetches models from the
|
||||
@@ -1127,6 +1225,14 @@ impl SetupWizard {
|
||||
self.settings.selected_model = Some(model_id.clone());
|
||||
print_success(&format!("Selected {}", model_id));
|
||||
}
|
||||
"openai_codex" => {
|
||||
// OAuth tokens can't call /v1/models — use hardcoded list
|
||||
let models: Vec<(String, String)> = crate::llm::openai_codex::CODEX_MODELS
|
||||
.iter()
|
||||
.map(|(id, desc)| (id.to_string(), desc.to_string()))
|
||||
.collect();
|
||||
self.select_from_model_list(&models)?;
|
||||
}
|
||||
_ => {
|
||||
// NEAR AI: use existing provider list_models()
|
||||
let fetched = self.fetch_nearai_models().await;
|
||||
@@ -1230,6 +1336,7 @@ impl SetupWizard {
|
||||
ollama: None,
|
||||
openai_compatible: None,
|
||||
tinfoil: None,
|
||||
openai_codex: None,
|
||||
};
|
||||
|
||||
match create_llm_provider(&config, session) {
|
||||
|
||||
Reference in New Issue
Block a user