Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion crates/jcode-base/src/provider/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const CHATGPT_API_BASE: &str = "https://chatgpt.com/backend-api/codex";
const RESPONSES_PATH: &str = "responses";
const DEFAULT_MODEL: &str = "gpt-5.5";
const ORIGINATOR: &str = "codex_cli_rs";
const OPENAI_API_BASE_ENV: &str = "OPENAI_API_BASE";
const OPENAI_BASE_URL_ENV: &str = "OPENAI_BASE_URL";
const JCODE_OPENAI_API_BASE_ENV: &str = "JCODE_OPENAI_API_BASE";
const OPENAI_ENV_FILE: &str = "openai.env";

/// Maximum number of retries for transient errors
const MAX_RETRIES: u32 = 3;
Expand Down Expand Up @@ -836,11 +840,42 @@ impl OpenAIProvider {
parsed
}

fn configured_openai_api_base() -> &'static str {
static CONFIGURED_BASE: LazyLock<String> = LazyLock::new(|| {
for env_key in [
JCODE_OPENAI_API_BASE_ENV,
OPENAI_API_BASE_ENV,
OPENAI_BASE_URL_ENV,
] {
if let Some(raw) = crate::provider_catalog::load_env_value_from_env_or_config(
env_key,
OPENAI_ENV_FILE,
) {
if let Some(normalized) = crate::provider_catalog::normalize_api_base(&raw) {
crate::logging::info(&format!(
"OpenAI API base configured via {}: {}",
env_key, normalized
));
return normalized;
}
crate::logging::warn(&format!(
"Ignoring invalid {}='{}'; use https://... or http://localhost/private-lan.",
env_key, raw
));
}
}

OPENAI_API_BASE.to_string()
});

CONFIGURED_BASE.as_str()
}

fn responses_url(credentials: &CodexCredentials) -> String {
let base = if Self::is_chatgpt_mode(credentials) {
CHATGPT_API_BASE
} else {
OPENAI_API_BASE
Self::configured_openai_api_base()
};
format!("{}/{}", base.trim_end_matches('/'), RESPONSES_PATH)
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ pub(crate) enum Command {
#[arg(long, value_enum)]
google_access_tier: Option<GoogleAccessTierArg>,

/// OpenAI-compatible API base URL. Used with --provider openai-compatible/custom profiles.
/// API base URL. Used with --provider openai-api for native Responses API or --provider openai-compatible/custom profiles.
#[arg(long)]
api_base: Option<String>,

/// OpenAI-compatible API key. If omitted, jcode prompts securely when needed.
/// API key. Used with --provider openai-api or OpenAI-compatible providers. If omitted, jcode prompts securely when needed.
#[arg(long)]
api_key: Option<String>,

Expand Down
27 changes: 21 additions & 6 deletions src/cli/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ pub async fn run_login_provider(
.await
.map(|_| LoginFlowOutcome::Completed),
LoginProviderTarget::OpenAiApiKey => {
login_openai_api_key_flow().map(|_| LoginFlowOutcome::Completed)
login_openai_api_key_flow(&options).map(|_| LoginFlowOutcome::Completed)
}
LoginProviderTarget::OpenRouter => {
login_openrouter_flow().map(|_| LoginFlowOutcome::Completed)
Expand Down Expand Up @@ -512,13 +512,16 @@ fn login_jcode_flow() -> Result<()> {
Ok(())
}

fn login_openai_api_key_flow() -> Result<()> {
fn login_openai_api_key_flow(options: &LoginOptions) -> Result<()> {
eprintln!("Setting up OpenAI API key...");
eprintln!("Get your API key from: https://platform.openai.com/api-keys\n");
eprint!("Paste your OpenAI API key: ");
io::stdout().flush()?;

let key = read_secret_line()?;
let key = if let Some(key) = options.openai_compatible_api_key.as_deref() {
key.trim().to_string()
} else {
eprint!("Paste your OpenAI API key: ");
io::stdout().flush()?;
read_secret_line()?
};
if key.is_empty() {
anyhow::bail!("No API key provided.");
}
Expand All @@ -527,6 +530,15 @@ fn login_openai_api_key_flow() -> Result<()> {
}

save_named_api_key("openai.env", "OPENAI_API_KEY", &key)?;
if let Some(api_base) = options.openai_compatible_api_base.as_deref() {
let api_base = crate::provider_catalog::normalize_api_base(api_base).ok_or_else(|| {
anyhow::anyhow!(
"Invalid OpenAI API base URL '{}'. Use https://... or http://localhost/private-lan.",
api_base
)
})?;
save_named_api_key("openai.env", "JCODE_OPENAI_API_BASE", &api_base)?;
}
eprintln!("\nSuccessfully saved OpenAI API key!");
eprintln!(
"Stored at {}",
Expand All @@ -535,6 +547,9 @@ fn login_openai_api_key_flow() -> Result<()> {
.display()
);
eprintln!("Provider: openai-api (native OpenAI Responses API)");
if options.openai_compatible_api_base.is_some() {
eprintln!("Base URL: custom native Responses API endpoint");
}
crate::telemetry::record_auth_success("openai-api", "api_key");
Ok(())
}
Expand Down