Files
1jehuang--jcode/src/cli/provider_init.rs
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

1824 lines
62 KiB
Rust

use anyhow::Result;
use std::io::{self, Write};
use std::sync::Arc;
use crate::auth;
use crate::provider;
use crate::provider::Provider;
use crate::provider_catalog::{
LoginProviderDescriptor, LoginProviderTarget, OpenAiCompatibleProfile,
apply_openai_compatible_profile_env, force_apply_openai_compatible_profile_env,
is_safe_env_file_name, is_safe_env_key_name, resolve_login_selection,
resolve_openai_compatible_profile,
};
use crate::tool;
use super::login::run_login_provider;
use super::output;
pub(crate) use crate::external_auth::maybe_run_external_auth_auto_import_flow;
use crate::external_auth::{
can_prompt_for_external_auth, external_auth_blocked_message, prompt_to_trust_external_auth,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ProviderChoice {
Jcode,
Claude,
#[value(alias = "claude-api", alias = "anthropic-key", alias = "claude-key")]
AnthropicApi,
#[deprecated(
note = "Claude Code CLI subprocess transport is deprecated; use ProviderChoice::Claude for native Anthropic OAuth/API transport"
)]
#[value(alias = "claude-subprocess", hide = true)]
ClaudeSubprocess,
Openai,
#[value(
alias = "openai-key",
alias = "openai-apikey",
alias = "openai-platform"
)]
OpenaiApi,
Openrouter,
#[value(alias = "aws-bedrock", alias = "aws_bedrock")]
Bedrock,
#[value(alias = "azure-openai", alias = "aoai")]
Azure,
#[value(alias = "opencode-zen", alias = "zen")]
Opencode,
#[value(alias = "opencodego")]
OpencodeGo,
#[value(alias = "z.ai", alias = "z-ai", alias = "zai-coding")]
Zai,
#[value(
alias = "kimi-code",
alias = "kimi-coding",
alias = "kimi-coding-plan",
alias = "kimi-for-coding",
alias = "moonshot-coding"
)]
Kimi,
#[value(alias = "302.ai")]
Ai302,
Baseten,
Cortecs,
#[value(alias = "cgc", alias = "comtegra-gpu-cloud")]
Comtegra,
Deepseek,
#[value(alias = "fpt-ai", alias = "fptcloud", alias = "fpt-cloud")]
Fpt,
Firmware,
#[value(alias = "hugging-face", alias = "hf")]
HuggingFace,
#[value(alias = "moonshot")]
MoonshotAi,
Nebius,
Scaleway,
Stackit,
Groq,
#[value(alias = "mistralai")]
Mistral,
#[value(alias = "pplx")]
Perplexity,
#[value(alias = "together", alias = "together-ai")]
TogetherAi,
#[value(alias = "deep-infra")]
Deepinfra,
#[value(alias = "fireworks-ai", alias = "fireworks.ai")]
Fireworks,
#[value(alias = "minimax-ai", alias = "minimaxi")]
Minimax,
#[value(alias = "x.ai", alias = "x-ai", alias = "grok")]
Xai,
#[value(alias = "nvidia", alias = "nim")]
NvidiaNim,
#[value(alias = "xiaomi", alias = "mimo", alias = "xiaomi-mimo-api")]
XiaomiMimo,
#[value(alias = "lm-studio")]
Lmstudio,
Ollama,
Chutes,
#[value(alias = "cerebrascode", alias = "cerberascode")]
Cerebras,
#[value(
alias = "bailian",
alias = "aliyun-bailian",
alias = "coding-plan",
alias = "alibaba-coding"
)]
AlibabaCodingPlan,
#[value(alias = "compat", alias = "custom")]
OpenaiCompatible,
Cursor,
Copilot,
Gemini,
#[value(
alias = "gemini-key",
alias = "gemini-apikey",
alias = "google-ai-studio",
alias = "ai-studio"
)]
GeminiApi,
Antigravity,
Google,
Auto,
}
impl ProviderChoice {
#[allow(deprecated)]
pub fn as_arg_value(&self) -> &'static str {
match self {
Self::Jcode => "jcode",
Self::Claude => "claude",
Self::AnthropicApi => "anthropic-api",
Self::ClaudeSubprocess => "claude-subprocess",
Self::Openai => "openai",
Self::OpenaiApi => "openai-api",
Self::Openrouter => "openrouter",
Self::Bedrock => "bedrock",
Self::Azure => "azure",
Self::Opencode => "opencode",
Self::OpencodeGo => "opencode-go",
Self::Zai => "zai",
Self::Kimi => "kimi",
Self::Ai302 => "302ai",
Self::Baseten => "baseten",
Self::Cortecs => "cortecs",
Self::Comtegra => "comtegra",
Self::Deepseek => "deepseek",
Self::Fpt => "fpt",
Self::Firmware => "firmware",
Self::HuggingFace => "huggingface",
Self::MoonshotAi => "moonshotai",
Self::Nebius => "nebius",
Self::Scaleway => "scaleway",
Self::Stackit => "stackit",
Self::Groq => "groq",
Self::Mistral => "mistral",
Self::Perplexity => "perplexity",
Self::TogetherAi => "togetherai",
Self::Deepinfra => "deepinfra",
Self::Fireworks => "fireworks",
Self::Minimax => "minimax",
Self::Xai => "xai",
Self::NvidiaNim => "nvidia-nim",
Self::XiaomiMimo => "xiaomi-mimo",
Self::Lmstudio => "lmstudio",
Self::Ollama => "ollama",
Self::Chutes => "chutes",
Self::Cerebras => "cerebras",
Self::AlibabaCodingPlan => "alibaba-coding-plan",
Self::OpenaiCompatible => "openai-compatible",
Self::Cursor => "cursor",
Self::Copilot => "copilot",
Self::Gemini => "gemini",
Self::GeminiApi => "gemini-api",
Self::Antigravity => "antigravity",
Self::Google => "google",
Self::Auto => "auto",
}
}
}
#[allow(deprecated)]
const PROVIDER_CHOICE_LOGIN_PROVIDERS: &[(ProviderChoice, LoginProviderDescriptor)] = &[
(
ProviderChoice::Jcode,
crate::provider_catalog::JCODE_LOGIN_PROVIDER,
),
(
ProviderChoice::Claude,
crate::provider_catalog::CLAUDE_LOGIN_PROVIDER,
),
(
ProviderChoice::AnthropicApi,
crate::provider_catalog::ANTHROPIC_API_LOGIN_PROVIDER,
),
(
ProviderChoice::ClaudeSubprocess,
crate::provider_catalog::CLAUDE_LOGIN_PROVIDER,
),
(
ProviderChoice::Openai,
crate::provider_catalog::OPENAI_LOGIN_PROVIDER,
),
(
ProviderChoice::OpenaiApi,
crate::provider_catalog::OPENAI_API_LOGIN_PROVIDER,
),
(
ProviderChoice::Openrouter,
crate::provider_catalog::OPENROUTER_LOGIN_PROVIDER,
),
(
ProviderChoice::Bedrock,
crate::provider_catalog::BEDROCK_LOGIN_PROVIDER,
),
(
ProviderChoice::Azure,
crate::provider_catalog::AZURE_LOGIN_PROVIDER,
),
(
ProviderChoice::Opencode,
crate::provider_catalog::OPENCODE_LOGIN_PROVIDER,
),
(
ProviderChoice::OpencodeGo,
crate::provider_catalog::OPENCODE_GO_LOGIN_PROVIDER,
),
(
ProviderChoice::Zai,
crate::provider_catalog::ZAI_LOGIN_PROVIDER,
),
(
ProviderChoice::Kimi,
crate::provider_catalog::KIMI_LOGIN_PROVIDER,
),
(
ProviderChoice::Ai302,
crate::provider_catalog::AI302_LOGIN_PROVIDER,
),
(
ProviderChoice::Baseten,
crate::provider_catalog::BASETEN_LOGIN_PROVIDER,
),
(
ProviderChoice::Cortecs,
crate::provider_catalog::CORTECS_LOGIN_PROVIDER,
),
(
ProviderChoice::Comtegra,
crate::provider_catalog::COMTEGRA_LOGIN_PROVIDER,
),
(
ProviderChoice::Deepseek,
crate::provider_catalog::DEEPSEEK_LOGIN_PROVIDER,
),
(
ProviderChoice::Fpt,
crate::provider_catalog::FPT_LOGIN_PROVIDER,
),
(
ProviderChoice::Firmware,
crate::provider_catalog::FIRMWARE_LOGIN_PROVIDER,
),
(
ProviderChoice::HuggingFace,
crate::provider_catalog::HUGGING_FACE_LOGIN_PROVIDER,
),
(
ProviderChoice::MoonshotAi,
crate::provider_catalog::MOONSHOT_LOGIN_PROVIDER,
),
(
ProviderChoice::Nebius,
crate::provider_catalog::NEBIUS_LOGIN_PROVIDER,
),
(
ProviderChoice::Scaleway,
crate::provider_catalog::SCALEWAY_LOGIN_PROVIDER,
),
(
ProviderChoice::Stackit,
crate::provider_catalog::STACKIT_LOGIN_PROVIDER,
),
(
ProviderChoice::Groq,
crate::provider_catalog::GROQ_LOGIN_PROVIDER,
),
(
ProviderChoice::Mistral,
crate::provider_catalog::MISTRAL_LOGIN_PROVIDER,
),
(
ProviderChoice::Perplexity,
crate::provider_catalog::PERPLEXITY_LOGIN_PROVIDER,
),
(
ProviderChoice::TogetherAi,
crate::provider_catalog::TOGETHER_AI_LOGIN_PROVIDER,
),
(
ProviderChoice::Deepinfra,
crate::provider_catalog::DEEPINFRA_LOGIN_PROVIDER,
),
(
ProviderChoice::Fireworks,
crate::provider_catalog::FIREWORKS_LOGIN_PROVIDER,
),
(
ProviderChoice::Minimax,
crate::provider_catalog::MINIMAX_LOGIN_PROVIDER,
),
(
ProviderChoice::Xai,
crate::provider_catalog::XAI_LOGIN_PROVIDER,
),
(
ProviderChoice::NvidiaNim,
crate::provider_catalog::NVIDIA_NIM_LOGIN_PROVIDER,
),
(
ProviderChoice::XiaomiMimo,
crate::provider_catalog::XIAOMI_MIMO_LOGIN_PROVIDER,
),
(
ProviderChoice::Lmstudio,
crate::provider_catalog::LMSTUDIO_LOGIN_PROVIDER,
),
(
ProviderChoice::Ollama,
crate::provider_catalog::OLLAMA_LOGIN_PROVIDER,
),
(
ProviderChoice::Chutes,
crate::provider_catalog::CHUTES_LOGIN_PROVIDER,
),
(
ProviderChoice::Cerebras,
crate::provider_catalog::CEREBRAS_LOGIN_PROVIDER,
),
(
ProviderChoice::AlibabaCodingPlan,
crate::provider_catalog::ALIBABA_CODING_PLAN_LOGIN_PROVIDER,
),
(
ProviderChoice::OpenaiCompatible,
crate::provider_catalog::OPENAI_COMPAT_LOGIN_PROVIDER,
),
(
ProviderChoice::Cursor,
crate::provider_catalog::CURSOR_LOGIN_PROVIDER,
),
(
ProviderChoice::Copilot,
crate::provider_catalog::COPILOT_LOGIN_PROVIDER,
),
(
ProviderChoice::Gemini,
crate::provider_catalog::GEMINI_LOGIN_PROVIDER,
),
(
ProviderChoice::GeminiApi,
crate::provider_catalog::GEMINI_API_LOGIN_PROVIDER,
),
(
ProviderChoice::Antigravity,
crate::provider_catalog::ANTIGRAVITY_LOGIN_PROVIDER,
),
(
ProviderChoice::Google,
crate::provider_catalog::GOOGLE_LOGIN_PROVIDER,
),
];
pub fn login_provider_choice_mappings() -> &'static [(ProviderChoice, LoginProviderDescriptor)] {
PROVIDER_CHOICE_LOGIN_PROVIDERS
}
pub fn profile_for_choice(choice: &ProviderChoice) -> Option<OpenAiCompatibleProfile> {
match login_provider_for_choice(choice)?.target {
LoginProviderTarget::OpenAiCompatible(profile) => Some(profile),
_ => None,
}
}
#[allow(deprecated)]
pub fn login_provider_for_choice(choice: &ProviderChoice) -> Option<LoginProviderDescriptor> {
PROVIDER_CHOICE_LOGIN_PROVIDERS
.iter()
.find(|(candidate, _)| candidate == choice)
.map(|(_, provider)| *provider)
}
#[allow(deprecated)]
pub fn choice_for_login_provider(provider: LoginProviderDescriptor) -> Option<ProviderChoice> {
PROVIDER_CHOICE_LOGIN_PROVIDERS
.iter()
.find(|(choice, candidate)| {
candidate.id == provider.id && !matches!(choice, ProviderChoice::ClaudeSubprocess)
})
.map(|(choice, _)| *choice)
}
pub fn prompt_login_provider_selection(
providers: &[LoginProviderDescriptor],
heading: &str,
) -> Result<LoginProviderDescriptor> {
prompt_login_provider_selection_optional(providers, heading)?.ok_or_else(|| {
anyhow::anyhow!("Login skipped. Run `jcode login` when you're ready to authenticate.")
})
}
pub fn prompt_login_provider_selection_optional(
providers: &[LoginProviderDescriptor],
heading: &str,
) -> Result<Option<LoginProviderDescriptor>> {
let status = auth::AuthStatus::check_fast();
eprint!(
"{}",
render_login_provider_selection_menu(heading, providers, &status)
);
eprint!(
"\nEnter 1-{}, provider name, or Enter=skip: ",
providers.len()
);
io::stderr().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
parse_login_provider_selection_input(&input, providers)
}
pub fn parse_login_provider_selection_input(
input: &str,
providers: &[LoginProviderDescriptor],
) -> Result<Option<LoginProviderDescriptor>> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(None);
}
let normalized = trimmed.to_ascii_lowercase();
if matches!(
normalized.as_str(),
"s" | "skip" | "q" | "quit" | "cancel" | "none"
) {
return Ok(None);
}
resolve_login_selection(trimmed, providers)
.map(Some)
.ok_or_else(|| {
anyhow::anyhow!(
"Invalid choice '{}'. Enter 1-{}, a provider name, or 'skip'.",
trimmed,
providers.len()
)
})
}
pub fn render_login_provider_selection_menu(
heading: &str,
providers: &[LoginProviderDescriptor],
status: &auth::AuthStatus,
) -> String {
use std::fmt::Write as _;
let mut out = String::new();
let _ = writeln!(out, "{heading}");
let _ = writeln!(out);
let detected = providers
.iter()
.copied()
.filter_map(|provider| {
let assessment = status.assessment_for_provider(provider);
(assessment.state != auth::AuthState::NotConfigured).then(|| {
format!(
" - {}: {}",
provider.display_name,
login_provider_detection_detail(provider, &assessment)
)
})
})
.collect::<Vec<_>>();
if detected.is_empty() {
let _ = writeln!(out, "Autodetected auth: none found yet.");
} else {
let _ = writeln!(out, "Autodetected auth:");
for line in detected {
let _ = writeln!(out, "{line}");
}
}
let _ = writeln!(out);
for (index, provider) in providers.iter().copied().enumerate() {
let assessment = status.assessment_for_provider(provider);
let _ = writeln!(
out,
" {}. {:<22} [{:<15}] - {}",
index + 1,
provider.display_name,
login_provider_state_badge(provider, assessment.state),
provider.menu_detail
);
}
let recommended = providers
.iter()
.filter(|provider| provider.recommended)
.map(|provider| provider.display_name)
.collect::<Vec<_>>();
if !recommended.is_empty() {
let _ = writeln!(out);
let _ = writeln!(
out,
" Recommended if you have a subscription: {}.",
recommended.join(", ")
);
}
let _ = writeln!(out);
let _ = writeln!(out, " Skip: press Enter, or type `skip`.");
out
}
fn login_provider_state_badge(
provider: LoginProviderDescriptor,
state: auth::AuthState,
) -> &'static str {
match state {
auth::AuthState::Available => {
if matches!(provider.target, LoginProviderTarget::AutoImport) {
"detected"
} else {
"configured"
}
}
auth::AuthState::Expired => "needs attention",
auth::AuthState::NotConfigured => "not configured",
}
}
fn login_provider_detection_detail(
provider: LoginProviderDescriptor,
assessment: &auth::ProviderAuthAssessment,
) -> String {
match assessment.state {
auth::AuthState::Available => {
let prefix = if matches!(provider.target, LoginProviderTarget::AutoImport) {
"detected"
} else {
"configured"
};
format!("{}: {}", prefix, assessment.method_detail)
}
auth::AuthState::Expired => format!("needs attention: {}", assessment.method_detail),
auth::AuthState::NotConfigured => "not configured".to_string(),
}
}
struct AutoProviderAvailability {
auth_status: auth::AuthStatus,
has_claude: bool,
has_openai: bool,
has_copilot: bool,
has_antigravity: bool,
has_gemini: bool,
has_cursor: bool,
has_openrouter: bool,
}
impl AutoProviderAvailability {
fn has_any_provider(&self) -> bool {
self.has_claude
|| self.has_openai
|| self.has_copilot
|| self.has_antigravity
|| self.has_gemini
|| self.has_cursor
|| self.has_openrouter
}
}
fn maybe_enable_config_default_provider_for_auto() -> Result<bool> {
let cfg = crate::config::config();
let Some(default_provider) = cfg
.provider
.default_provider
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(false);
};
if let Some(profile) =
crate::provider_catalog::resolve_openai_compatible_profile_selection(default_provider)
{
apply_openai_compatible_profile_env(Some(profile));
return Ok(provider::openrouter::has_credentials());
}
if cfg.providers.contains_key(default_provider) {
crate::provider_catalog::apply_named_provider_profile_env_from_config(
default_provider,
cfg,
)?;
return Ok(provider::openrouter::has_credentials());
}
Ok(false)
}
async fn detect_auto_provider_flags() -> AutoProviderAvailability {
let auth_status = auth::AuthStatus::check_fast();
AutoProviderAvailability {
has_claude: auth_status.anthropic.has_oauth || auth_status.anthropic.has_api_key,
has_openai: auth_status.openai_has_oauth || auth_status.openai_has_api_key,
has_copilot: auth_status.copilot_has_api_token,
has_antigravity: auth::antigravity::load_tokens().is_ok(),
has_gemini: auth_status.gemini == auth::AuthState::Available,
has_cursor: auth_status.cursor == auth::AuthState::Available,
has_openrouter: auth_status.openrouter == auth::AuthState::Available,
auth_status,
}
}
fn provider_label_for_api_key_env(env_key: &str) -> String {
if env_key == "OPENROUTER_API_KEY" {
return "OpenRouter".to_string();
}
crate::provider_catalog::openai_compatible_profiles()
.iter()
.find_map(|profile| {
let resolved = resolve_openai_compatible_profile(*profile);
(resolved.api_key_env == env_key).then_some(resolved.display_name)
})
.unwrap_or_else(|| env_key.to_string())
}
fn provider_login_hint_for_api_key_env(env_key: &str) -> String {
if env_key == "OPENROUTER_API_KEY" {
return "jcode login --provider openrouter".to_string();
}
crate::provider_catalog::openai_compatible_profiles()
.iter()
.find_map(|profile| {
let resolved = resolve_openai_compatible_profile(*profile);
(resolved.api_key_env == env_key)
.then(|| format!("jcode login --provider {}", resolved.id))
})
.unwrap_or_else(|| "jcode login".to_string())
}
fn ensure_external_api_key_auth_allowed_for_explicit_choice(env_key: &str) -> Result<()> {
if direct_api_key_configured_for_env(env_key) {
return Ok(());
}
let Some(source) = auth::external::preferred_unconsented_api_key_source_for_env(env_key) else {
return Ok(());
};
let path = source.path()?;
let provider_name = provider_label_for_api_key_env(env_key);
let login_hint = provider_login_hint_for_api_key_env(env_key);
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
&provider_name,
source.display_name(),
&path,
&login_hint,
));
}
if prompt_to_trust_external_auth(&provider_name, source.display_name(), &path)? {
auth::external::trust_external_auth_source(source)?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting external {} credentials. Run `{}` to authenticate jcode directly.",
provider_name,
login_hint
)
}
fn direct_api_key_configured_for_env(env_key: &str) -> bool {
let env_key = env_key.trim();
if env_key.is_empty() {
return false;
}
if std::env::var(env_key)
.ok()
.map(|key| !key.trim().is_empty())
.unwrap_or(false)
{
return true;
}
crate::provider_catalog::openai_compatible_profiles()
.iter()
.filter_map(|profile| {
let resolved = resolve_openai_compatible_profile(*profile);
(resolved.api_key_env == env_key).then_some(resolved.env_file)
})
.any(|env_file| direct_env_file_contains_key(env_key, &env_file))
}
fn direct_env_file_contains_key(env_key: &str, env_file: &str) -> bool {
if !crate::provider_catalog::is_safe_env_file_name(env_file) {
return false;
}
let Some(config_dir) = crate::storage::app_config_dir().ok() else {
return false;
};
let path = config_dir.join(env_file);
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
let prefix = format!("{}=", env_key);
content.lines().any(|line| {
line.strip_prefix(&prefix)
.map(|key| !key.trim().trim_matches('"').trim_matches('\'').is_empty())
.unwrap_or(false)
})
}
fn maybe_enable_external_api_key_auth_for_auto(has_other_provider: bool) -> Result<bool> {
if provider::openrouter::has_credentials() {
return Ok(true);
}
if has_other_provider {
return Ok(false);
}
for (env_key, _) in crate::provider_catalog::openrouter_like_api_key_sources() {
let Some(source) = auth::external::preferred_unconsented_api_key_source_for_env(&env_key)
else {
continue;
};
let path = source.path()?;
let provider_name = provider_label_for_api_key_env(&env_key);
let login_hint = provider_login_hint_for_api_key_env(&env_key);
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
&provider_name,
source.display_name(),
&path,
&login_hint,
));
return Ok(false);
}
if prompt_to_trust_external_auth(&provider_name, source.display_name(), &path)? {
auth::external::trust_external_auth_source(source)?;
return Ok(provider::openrouter::has_credentials());
}
return Ok(false);
}
Ok(false)
}
fn maybe_prompt_for_generic_oauth_source(
provider_name: &str,
source: Option<auth::external::ExternalAuthSource>,
login_hint: &str,
auto: bool,
validation: impl Fn() -> bool,
) -> Result<bool> {
let Some(source) = source else {
return Ok(false);
};
let path = source.path()?;
if !can_prompt_for_external_auth() {
if auto {
crate::logging::warn(&external_auth_blocked_message(
provider_name,
source.display_name(),
&path,
login_hint,
));
return Ok(false);
}
anyhow::bail!(external_auth_blocked_message(
provider_name,
source.display_name(),
&path,
login_hint,
));
}
if prompt_to_trust_external_auth(provider_name, source.display_name(), &path)? {
auth::external::trust_external_auth_source(source)?;
return Ok(if auto { validation() } else { true });
}
Ok(false)
}
fn ensure_openai_auth_allowed_for_explicit_choice() -> Result<()> {
if auth::codex::load_credentials().is_ok() {
return Ok(());
}
if maybe_prompt_for_generic_oauth_source(
"OpenAI/Codex",
auth::external::preferred_unconsented_openai_oauth_source(),
"jcode login --provider openai",
false,
|| auth::codex::load_credentials().is_ok(),
)? {
return Ok(());
}
if !auth::codex::has_unconsented_legacy_credentials() {
return Ok(());
}
let path = auth::codex::legacy_auth_file_path()?;
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
"OpenAI/Codex",
"Codex",
&path,
"jcode login --provider openai"
));
}
if prompt_to_trust_external_auth("OpenAI/Codex", "Codex", &path)? {
auth::codex::trust_legacy_auth_for_future_use()?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting existing ~/.codex/auth.json credentials. Run `jcode login --provider openai` to authenticate jcode directly."
)
}
fn maybe_enable_legacy_codex_auth_for_auto(has_other_provider: bool) -> Result<bool> {
if auth::codex::load_credentials().is_ok() {
return Ok(true);
}
if let Some(source) = auth::external::preferred_unconsented_openai_oauth_source() {
if has_other_provider {
return Ok(false);
}
return maybe_prompt_for_generic_oauth_source(
"OpenAI/Codex",
Some(source),
"jcode login --provider openai",
true,
|| auth::codex::load_credentials().is_ok(),
);
}
if !auth::codex::has_unconsented_legacy_credentials() {
return Ok(false);
}
if has_other_provider {
return Ok(false);
}
let path = auth::codex::legacy_auth_file_path()?;
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
"OpenAI/Codex",
"Codex",
&path,
"jcode login --provider openai",
));
return Ok(false);
}
if prompt_to_trust_external_auth("OpenAI/Codex", "Codex", &path)? {
auth::codex::trust_legacy_auth_for_future_use()?;
return Ok(auth::codex::load_credentials().is_ok());
}
Ok(false)
}
fn ensure_claude_auth_allowed_for_explicit_choice() -> Result<()> {
if auth::claude::load_credentials().is_ok() {
return Ok(());
}
if maybe_prompt_for_generic_oauth_source(
"Claude",
auth::external::preferred_unconsented_anthropic_oauth_source(),
"jcode login --provider claude",
false,
|| auth::claude::load_credentials().is_ok(),
)? {
return Ok(());
}
let Some(source) = auth::claude::has_unconsented_external_auth() else {
return Ok(());
};
let path = source.path()?;
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
"Claude",
source.display_name(),
&path,
"jcode login --provider claude"
));
}
if prompt_to_trust_external_auth("Claude", source.display_name(), &path)? {
auth::claude::trust_external_auth_source(source)?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting external Claude credentials. Run `jcode login --provider claude` to authenticate jcode directly."
)
}
fn maybe_enable_claude_auth_for_auto(has_other_provider: bool) -> Result<bool> {
if auth::claude::load_credentials().is_ok() {
return Ok(true);
}
if let Some(source) = auth::external::preferred_unconsented_anthropic_oauth_source() {
if has_other_provider {
return Ok(false);
}
return maybe_prompt_for_generic_oauth_source(
"Claude",
Some(source),
"jcode login --provider claude",
true,
|| auth::claude::load_credentials().is_ok(),
);
}
let Some(source) = auth::claude::has_unconsented_external_auth() else {
return Ok(false);
};
if has_other_provider {
return Ok(false);
}
let path = source.path()?;
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
"Claude",
source.display_name(),
&path,
"jcode login --provider claude",
));
return Ok(false);
}
if prompt_to_trust_external_auth("Claude", source.display_name(), &path)? {
auth::claude::trust_external_auth_source(source)?;
return Ok(auth::claude::load_credentials().is_ok());
}
Ok(false)
}
fn ensure_gemini_auth_allowed_for_explicit_choice() -> Result<()> {
// An official Gemini Developer API key (GEMINI_API_KEY) authenticates
// directly against generativelanguage.googleapis.com and needs no OAuth
// consent flow, so allow it without further prompting.
if auth::gemini::has_api_key() {
return Ok(());
}
if auth::gemini::load_tokens().is_ok() {
return Ok(());
}
if maybe_prompt_for_generic_oauth_source(
"Gemini",
auth::external::preferred_unconsented_gemini_oauth_source(),
"jcode login --provider gemini",
false,
|| auth::gemini::load_tokens().is_ok(),
)? {
return Ok(());
}
if !auth::gemini::has_unconsented_cli_auth() {
return Ok(());
}
let path = auth::gemini::gemini_cli_oauth_path()?;
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
"Gemini",
"Gemini CLI",
&path,
"jcode login --provider gemini"
));
}
if prompt_to_trust_external_auth("Gemini", "Gemini CLI", &path)? {
auth::gemini::trust_cli_auth_for_future_use()?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting Gemini CLI credentials. Run `jcode login --provider gemini` to authenticate jcode directly."
)
}
fn maybe_enable_gemini_auth_for_auto(has_other_provider: bool) -> Result<bool> {
// A configured Gemini Developer API key is sufficient on its own.
if auth::gemini::has_api_key() {
return Ok(true);
}
if auth::gemini::load_tokens().is_ok() {
return Ok(true);
}
if let Some(source) = auth::external::preferred_unconsented_gemini_oauth_source() {
if has_other_provider {
return Ok(false);
}
return maybe_prompt_for_generic_oauth_source(
"Gemini",
Some(source),
"jcode login --provider gemini",
true,
|| auth::gemini::load_tokens().is_ok(),
);
}
if !auth::gemini::has_unconsented_cli_auth() {
return Ok(false);
}
if has_other_provider {
return Ok(false);
}
let path = auth::gemini::gemini_cli_oauth_path()?;
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
"Gemini",
"Gemini CLI",
&path,
"jcode login --provider gemini",
));
return Ok(false);
}
if prompt_to_trust_external_auth("Gemini", "Gemini CLI", &path)? {
auth::gemini::trust_cli_auth_for_future_use()?;
return Ok(auth::gemini::load_tokens().is_ok());
}
Ok(false)
}
fn ensure_antigravity_auth_allowed_for_explicit_choice() -> Result<()> {
if auth::antigravity::load_tokens().is_ok() {
return Ok(());
}
if maybe_prompt_for_generic_oauth_source(
"Antigravity",
auth::external::preferred_unconsented_antigravity_oauth_source(),
"jcode login --provider antigravity",
false,
|| auth::antigravity::load_tokens().is_ok(),
)? {
return Ok(());
}
Ok(())
}
fn ensure_copilot_auth_allowed_for_explicit_choice() -> Result<()> {
if auth::copilot::load_github_token().is_ok() {
return Ok(());
}
let Some(source) = auth::copilot::has_unconsented_external_auth() else {
return Ok(());
};
let path = source.path();
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
"GitHub Copilot",
source.display_name(),
&path,
"jcode login --provider copilot"
));
}
if prompt_to_trust_external_auth("GitHub Copilot", source.display_name(), &path)? {
auth::copilot::trust_external_auth_source(source)?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting external Copilot credentials. Run `jcode login --provider copilot` to authenticate jcode directly."
)
}
fn maybe_enable_copilot_auth_for_auto(has_other_provider: bool) -> Result<bool> {
if auth::copilot::load_github_token().is_ok() {
return Ok(true);
}
let Some(source) = auth::copilot::has_unconsented_external_auth() else {
return Ok(false);
};
if has_other_provider {
return Ok(false);
}
let path = source.path();
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
"GitHub Copilot",
source.display_name(),
&path,
"jcode login --provider copilot",
));
return Ok(false);
}
if prompt_to_trust_external_auth("GitHub Copilot", source.display_name(), &path)? {
auth::copilot::trust_external_auth_source(source)?;
return Ok(auth::copilot::load_github_token().is_ok());
}
Ok(false)
}
fn ensure_cursor_auth_allowed_for_explicit_choice() -> Result<()> {
if auth::cursor::has_cursor_native_auth() || auth::cursor::has_cursor_api_key() {
return Ok(());
}
let Some(source) = auth::cursor::has_unconsented_external_auth() else {
return Ok(());
};
let path = source.path()?;
if !can_prompt_for_external_auth() {
anyhow::bail!(external_auth_blocked_message(
"Cursor",
source.display_name(),
&path,
"jcode login --provider cursor"
));
}
if prompt_to_trust_external_auth("Cursor", source.display_name(), &path)? {
auth::cursor::trust_external_auth_source(source)?;
return Ok(());
}
anyhow::bail!(
"Skipped trusting external Cursor credentials. Run `jcode login --provider cursor` to authenticate jcode directly."
)
}
fn maybe_enable_cursor_auth_for_auto(has_other_provider: bool) -> Result<bool> {
if auth::cursor::has_cursor_native_auth() || auth::cursor::has_cursor_api_key() {
return Ok(true);
}
let Some(source) = auth::cursor::has_unconsented_external_auth() else {
return Ok(false);
};
if has_other_provider {
return Ok(false);
}
let path = source.path()?;
if !can_prompt_for_external_auth() {
crate::logging::warn(&external_auth_blocked_message(
"Cursor",
source.display_name(),
&path,
"jcode login --provider cursor",
));
return Ok(false);
}
if prompt_to_trust_external_auth("Cursor", source.display_name(), &path)? {
auth::cursor::trust_external_auth_source(source)?;
return Ok(auth::cursor::has_cursor_native_auth());
}
Ok(false)
}
pub fn lock_model_provider(provider_key: &str) {
crate::provider::activation::lock_runtime_provider_key(provider_key);
}
pub fn unlock_model_provider() {
crate::provider::activation::unlock_runtime_provider();
}
/// A CLI provider choice for a dual-auth backend is also a credential choice.
/// Pin it through the provider's credential-mode API
/// so `--provider anthropic-api` cannot remain in Auto mode and prefer a stored
/// Claude OAuth credential over `ANTHROPIC_API_KEY` (and likewise for OpenAI).
fn explicit_credential_mode(choice: &ProviderChoice) -> Option<provider::CredentialMode> {
match choice {
ProviderChoice::AnthropicApi | ProviderChoice::OpenaiApi => {
Some(provider::CredentialMode::ApiKey)
}
_ => None,
}
}
fn disable_subscription_runtime_mode() {
crate::subscription_catalog::clear_runtime_env();
}
fn disable_subscription_runtime_mode_preserving_active_provider_profile() {
if std::env::var_os("JCODE_PROVIDER_PROFILE_ACTIVE").is_some()
|| std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_some()
{
crate::env::remove_var(crate::subscription_catalog::JCODE_SUBSCRIPTION_ACTIVE_ENV);
} else {
disable_subscription_runtime_mode();
}
}
pub fn apply_login_provider_profile_env(provider: LoginProviderDescriptor) {
match provider.target {
LoginProviderTarget::OpenAiCompatible(profile) => {
force_apply_openai_compatible_profile_env(Some(profile));
// Bootstrap login still spawns the daemon with `--provider auto`. Mark the
// just-selected compatible provider as active so the child process does
// not clear these inherited runtime vars before credential detection.
crate::env::set_var("JCODE_PROVIDER_PROFILE_ACTIVE", "1");
}
LoginProviderTarget::AutoImport | LoginProviderTarget::Google => {}
_ => {
// A later non-compatible login selection must not inherit a stale
// compatible-provider profile from an earlier bootstrap/login path.
force_apply_openai_compatible_profile_env(None);
}
}
}
fn resolved_profile_default_model(profile: OpenAiCompatibleProfile) -> Option<String> {
resolve_openai_compatible_profile(profile).default_model
}
pub async fn login_and_bootstrap_provider(
provider: LoginProviderDescriptor,
account_label: Option<&str>,
) -> Result<Arc<dyn provider::Provider>> {
run_login_provider(
provider,
account_label,
crate::cli::login::LoginOptions::default(),
)
.await?;
eprintln!();
let runtime: Arc<dyn provider::Provider> = match provider.target {
LoginProviderTarget::AutoImport => {
disable_subscription_runtime_mode();
Arc::new(provider::MultiProvider::new())
}
LoginProviderTarget::Jcode => Arc::new(provider::jcode::JcodeProvider::new()),
LoginProviderTarget::Claude | LoginProviderTarget::ClaudeApiKey => {
disable_subscription_runtime_mode();
Arc::new(provider::MultiProvider::new())
}
LoginProviderTarget::OpenAi => {
disable_subscription_runtime_mode();
Arc::new(provider::MultiProvider::with_preference(true))
}
LoginProviderTarget::OpenAiApiKey => {
disable_subscription_runtime_mode();
lock_model_provider("openai");
Arc::new(provider::MultiProvider::with_preference(true))
}
LoginProviderTarget::OpenRouter => {
disable_subscription_runtime_mode();
Arc::new(provider::MultiProvider::new())
}
LoginProviderTarget::Bedrock => {
disable_subscription_runtime_mode();
lock_model_provider("bedrock");
Arc::new(provider::MultiProvider::new())
}
LoginProviderTarget::Azure => {
disable_subscription_runtime_mode();
let model = crate::provider::activation::apply_azure_openai_runtime()?;
let multi = provider::MultiProvider::new();
if let Some(model) = model {
let _ = multi.set_model(&model);
}
Arc::new(multi)
}
LoginProviderTarget::OpenAiCompatible(profile) => {
disable_subscription_runtime_mode();
apply_openai_compatible_profile_env(Some(profile));
let multi = provider::MultiProvider::new();
let resolved = resolve_openai_compatible_profile(profile);
crate::provider::activation::apply_openai_compatible_runtime(
resolved.default_model.clone(),
)?;
if let Some(model) = resolved.default_model.as_deref() {
let _ = multi.set_model(model);
}
Arc::new(multi)
}
LoginProviderTarget::Cursor => {
disable_subscription_runtime_mode();
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "cursor");
Arc::new(jcode_provider_cursor_runtime::CursorCliProvider::new())
}
LoginProviderTarget::Copilot => {
disable_subscription_runtime_mode();
Arc::new(provider::MultiProvider::new())
}
LoginProviderTarget::Gemini => {
disable_subscription_runtime_mode();
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "gemini");
Arc::new(jcode_provider_gemini_runtime::GeminiProvider::new())
}
LoginProviderTarget::Antigravity => {
disable_subscription_runtime_mode();
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "antigravity");
Arc::new(jcode_provider_antigravity_runtime::AntigravityProvider::new())
}
LoginProviderTarget::Google => {
anyhow::bail!("Google login cannot be used as a model provider bootstrap");
}
};
Ok(runtime)
}
pub fn save_named_api_key(env_file: &str, key_name: &str, key: &str) -> Result<()> {
if !is_safe_env_key_name(key_name) {
anyhow::bail!("Invalid API key variable name: {}", key_name);
}
if !is_safe_env_file_name(env_file) {
anyhow::bail!("Invalid env file name: {}", env_file);
}
let config_dir = crate::storage::app_config_dir()?;
let file_path = config_dir.join(env_file);
crate::storage::upsert_env_file_value(&file_path, key_name, Some(key))?;
crate::env::set_var(key_name, key);
Ok(())
}
pub async fn init_provider(
choice: &ProviderChoice,
model: Option<&str>,
) -> Result<Arc<dyn provider::Provider>> {
init_provider_with_options(choice, model, true, true).await
}
pub async fn init_provider_quiet(
choice: &ProviderChoice,
model: Option<&str>,
) -> Result<Arc<dyn provider::Provider>> {
init_provider_with_options(choice, model, false, true).await
}
pub async fn init_provider_for_validation(
choice: &ProviderChoice,
model: Option<&str>,
) -> Result<Arc<dyn provider::Provider>> {
init_provider_with_options(choice, model, false, false).await
}
#[allow(deprecated)]
async fn init_provider_with_options(
choice: &ProviderChoice,
model: Option<&str>,
show_init_messages: bool,
allow_login_bootstrap: bool,
) -> Result<Arc<dyn provider::Provider>> {
// Provider construction resolves concrete runtimes through the base
// crate's external-runtime registry (composition-root pattern). The
// binary's normal path registers them in `startup::run()`, but this
// function is also entered directly by validation/login/test flows that
// never run startup. Registration is idempotent, so do it here too;
// otherwise Auto-init silently loses registry-backed runtimes (e.g. the
// OpenRouter/OpenAI-compatible factory) and their model-picker routes.
super::startup::register_external_provider_runtimes();
if let Ok(profile_name) = std::env::var("JCODE_PROVIDER_PROFILE_NAME")
&& !profile_name.trim().is_empty()
{
crate::provider_catalog::apply_named_provider_profile_env(profile_name.trim())?;
crate::env::set_var("JCODE_PROVIDER_PROFILE_ACTIVE", "1");
}
if std::env::var_os("JCODE_PROVIDER_PROFILE_ACTIVE").is_none()
&& std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_none()
{
if let Some(profile) = profile_for_choice(choice) {
apply_openai_compatible_profile_env(Some(profile));
} else {
apply_openai_compatible_profile_env(None);
}
}
let init_notice = |message: &str| {
if show_init_messages {
output::stderr_info(message);
}
};
let provider: Arc<dyn provider::Provider> = match choice {
ProviderChoice::Jcode => {
init_notice("Using Jcode subscription provider (provider locked)");
Arc::new(provider::jcode::JcodeProvider::new())
}
ProviderChoice::Claude => {
disable_subscription_runtime_mode();
ensure_claude_auth_allowed_for_explicit_choice()?;
init_notice("Using Claude (provider locked)");
lock_model_provider("claude");
Arc::new(provider::MultiProvider::with_preference_fast(false))
}
ProviderChoice::AnthropicApi => {
disable_subscription_runtime_mode();
ensure_external_api_key_auth_allowed_for_explicit_choice("ANTHROPIC_API_KEY")?;
init_notice("Using Anthropic API key provider (provider locked)");
lock_model_provider("claude");
Arc::new(provider::MultiProvider::with_preference_fast(false))
}
ProviderChoice::ClaudeSubprocess => {
disable_subscription_runtime_mode();
ensure_claude_auth_allowed_for_explicit_choice()?;
crate::logging::warn(
"Using --provider claude-subprocess is deprecated and will be removed. Prefer `--provider claude`.",
);
crate::env::set_var("JCODE_USE_CLAUDE_CLI", "1");
init_notice(
"Using deprecated Claude subprocess transport (legacy compatibility mode; provider locked)",
);
lock_model_provider("claude");
Arc::new(provider::MultiProvider::with_preference_fast(false))
}
ProviderChoice::Openai => {
disable_subscription_runtime_mode();
ensure_openai_auth_allowed_for_explicit_choice()?;
init_notice("Using OpenAI (provider locked)");
lock_model_provider("openai");
Arc::new(provider::MultiProvider::with_preference_fast(true))
}
ProviderChoice::OpenaiApi => {
disable_subscription_runtime_mode();
ensure_external_api_key_auth_allowed_for_explicit_choice("OPENAI_API_KEY")?;
init_notice("Using OpenAI API key provider (provider locked)");
lock_model_provider("openai");
Arc::new(provider::MultiProvider::with_preference_fast(true))
}
ProviderChoice::Cursor => {
disable_subscription_runtime_mode();
ensure_cursor_auth_allowed_for_explicit_choice()?;
init_notice("Using Cursor native HTTPS provider (experimental)");
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "cursor");
Arc::new(jcode_provider_cursor_runtime::CursorCliProvider::new())
}
ProviderChoice::Copilot => {
disable_subscription_runtime_mode();
ensure_copilot_auth_allowed_for_explicit_choice()?;
init_notice("Using GitHub Copilot API provider (provider locked)");
lock_model_provider("copilot");
Arc::new(provider::MultiProvider::new_fast())
}
ProviderChoice::Gemini => {
disable_subscription_runtime_mode();
ensure_gemini_auth_allowed_for_explicit_choice()?;
if auth::gemini::has_api_key() {
init_notice(
"Using Gemini provider (official Gemini Developer API key, generativelanguage.googleapis.com)",
);
} else {
init_notice("Using Gemini provider (native Google Code Assist OAuth)");
}
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "gemini");
Arc::new(jcode_provider_gemini_runtime::GeminiProvider::new())
}
ProviderChoice::Openrouter => {
disable_subscription_runtime_mode();
ensure_external_api_key_auth_allowed_for_explicit_choice("OPENROUTER_API_KEY")?;
init_notice("Using OpenRouter provider (provider locked)");
lock_model_provider("openrouter");
Arc::new(provider::MultiProvider::new_fast())
}
ProviderChoice::Bedrock => {
disable_subscription_runtime_mode();
init_notice("Using AWS Bedrock provider (provider locked)");
lock_model_provider("bedrock");
Arc::new(provider::MultiProvider::new_fast())
}
ProviderChoice::Azure => {
disable_subscription_runtime_mode();
let model = crate::provider::activation::apply_azure_openai_runtime()?;
init_notice("Using Azure OpenAI provider (provider locked)");
let multi = provider::MultiProvider::new_fast();
if let Some(model) = model {
let _ = multi.set_model(&model);
}
Arc::new(multi)
}
ProviderChoice::Opencode
| ProviderChoice::OpencodeGo
| ProviderChoice::Zai
| ProviderChoice::Ai302
| ProviderChoice::Baseten
| ProviderChoice::Cortecs
| ProviderChoice::Comtegra
| ProviderChoice::Deepseek
| ProviderChoice::Fpt
| ProviderChoice::Firmware
| ProviderChoice::HuggingFace
| ProviderChoice::MoonshotAi
| ProviderChoice::Kimi
| ProviderChoice::Nebius
| ProviderChoice::Scaleway
| ProviderChoice::Stackit
| ProviderChoice::Groq
| ProviderChoice::Mistral
| ProviderChoice::Perplexity
| ProviderChoice::TogetherAi
| ProviderChoice::Deepinfra
| ProviderChoice::Fireworks
| ProviderChoice::Minimax
| ProviderChoice::Xai
| ProviderChoice::NvidiaNim
| ProviderChoice::XiaomiMimo
| ProviderChoice::Lmstudio
| ProviderChoice::Ollama
| ProviderChoice::Chutes
| ProviderChoice::Cerebras
| ProviderChoice::AlibabaCodingPlan
| ProviderChoice::GeminiApi
| ProviderChoice::OpenaiCompatible => {
disable_subscription_runtime_mode();
let profile = profile_for_choice(choice)
.ok_or_else(|| anyhow::anyhow!("missing provider profile for choice"))?;
if std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_none() {
// An explicit `--provider <compatible>` selection should win over
// any stale active-profile marker inherited from a previous
// bootstrap/login flow. Named provider profiles still take
// precedence when explicitly configured.
force_apply_openai_compatible_profile_env(Some(profile));
}
let mut runtime_model_hint = None;
let display_name = if let Ok(named) = std::env::var("JCODE_NAMED_PROVIDER_PROFILE") {
if let Some(profile) = crate::config::config().providers.get(&named) {
runtime_model_hint = profile.default_model.clone();
}
named
} else {
let resolved = resolve_openai_compatible_profile(profile);
if resolved.requires_api_key {
ensure_external_api_key_auth_allowed_for_explicit_choice(
&resolved.api_key_env,
)?;
}
runtime_model_hint = resolved.default_model.clone();
resolved.display_name
};
init_notice(&format!(
"Using {} via OpenAI-compatible API (provider locked)",
display_name
));
crate::provider::activation::apply_openai_compatible_runtime(runtime_model_hint)?;
if std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_some() {
let profile_name = std::env::var("JCODE_NAMED_PROVIDER_PROFILE")?;
let cfg = crate::config::config();
let profile = cfg.providers.get(&profile_name).ok_or_else(|| {
anyhow::anyhow!("Unknown provider profile '{}'", profile_name)
})?;
Arc::new(
jcode_provider_openrouter_runtime::OpenRouterProvider::new_named_openai_compatible(
&profile_name,
profile,
)?,
)
} else {
Arc::new(jcode_provider_openrouter_runtime::OpenRouterProvider::new()?)
}
}
ProviderChoice::Antigravity => {
disable_subscription_runtime_mode();
ensure_antigravity_auth_allowed_for_explicit_choice()?;
init_notice("Using Antigravity provider (experimental)");
unlock_model_provider();
crate::env::set_var("JCODE_ACTIVE_PROVIDER", "antigravity");
Arc::new(jcode_provider_antigravity_runtime::AntigravityProvider::new())
}
ProviderChoice::Google => {
disable_subscription_runtime_mode();
init_notice(
"Note: Google/Gmail is not a model provider. Using auto-detect for model provider.",
);
init_notice(
"Gmail credentials can be configured with `jcode login google`; the gmail tool is enabled by default in the full tool profile.",
);
unlock_model_provider();
Arc::new(provider::MultiProvider::new_fast())
}
ProviderChoice::Auto => {
disable_subscription_runtime_mode_preserving_active_provider_profile();
unlock_model_provider();
let auto_detect_start = std::time::Instant::now();
let mut availability = detect_auto_provider_flags().await;
let reviewed_external_auth = if !availability.has_any_provider() {
maybe_run_external_auth_auto_import_flow().await?.is_some()
} else {
false
};
if reviewed_external_auth {
availability = detect_auto_provider_flags().await;
}
let auto_detect_ms = auto_detect_start.elapsed().as_millis();
if !availability.has_any_provider() {
let supplemental_start = std::time::Instant::now();
let mut has_claude = availability.has_claude;
let mut has_openai = availability.has_openai;
let mut has_copilot = availability.has_copilot;
let has_antigravity = availability.has_antigravity;
let mut has_gemini = availability.has_gemini;
let mut has_cursor = availability.has_cursor;
let mut has_openrouter = availability.has_openrouter;
let mut has_other_provider = has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_openai {
has_openai = maybe_enable_legacy_codex_auth_for_auto(has_other_provider)?;
}
has_other_provider = has_openai
|| has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_claude {
has_claude =
maybe_enable_claude_auth_for_auto(has_other_provider && !has_claude)?;
}
has_other_provider = has_openai
|| has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_copilot {
has_copilot =
maybe_enable_copilot_auth_for_auto(has_other_provider && !has_copilot)?;
}
has_other_provider = has_openai
|| has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_gemini {
has_gemini =
maybe_enable_gemini_auth_for_auto(has_other_provider && !has_gemini)?;
}
has_other_provider = has_openai
|| has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_cursor {
has_cursor =
maybe_enable_cursor_auth_for_auto(has_other_provider && !has_cursor)?;
}
if !has_openrouter {
has_openrouter = maybe_enable_config_default_provider_for_auto()?;
}
has_other_provider = has_openai
|| has_claude
|| has_copilot
|| has_antigravity
|| has_gemini
|| has_cursor
|| has_openrouter;
if !has_openrouter {
has_openrouter = maybe_enable_external_api_key_auth_for_auto(
has_other_provider && !has_openrouter,
)?;
}
availability = AutoProviderAvailability {
auth_status: auth::AuthStatus::check_fast(),
has_claude,
has_openai,
has_copilot,
has_antigravity,
has_gemini,
has_cursor,
has_openrouter,
};
crate::logging::info(&format!(
"[TIMING] auto_provider_bootstrap: detect={}ms, external_import={}, supplemental={}ms, final_has_any={}",
auto_detect_ms,
reviewed_external_auth,
supplemental_start.elapsed().as_millis(),
availability.has_any_provider()
));
} else {
crate::logging::info(&format!(
"[TIMING] auto_provider_bootstrap: detect={}ms, external_import={}, supplemental=skipped, final_has_any=true",
auto_detect_ms, reviewed_external_auth
));
}
if availability.has_any_provider() {
let multi = provider::MultiProvider::from_auth_status(availability.auth_status);
init_notice(&format!(
"Using {} (use /model to switch models)",
multi.name()
));
crate::env::set_var("JCODE_ACTIVE_PROVIDER", multi.name().to_lowercase());
Arc::new(multi)
} else {
let non_interactive = std::env::var("JCODE_NON_INTERACTIVE").is_ok();
// Deferred-auth bootstrap: the interactive TUI server is spawned
// headless (JCODE_NON_INTERACTIVE) but the user logs in *inside*
// the TUI on a fresh install. Rather than bail, boot an empty
// MultiProvider with no configured credentials yet. The TUI's
// `/login` flow then activates a provider via the normal
// auth-changed path (MultiProvider::on_auth_changed hot-inits the
// newly logged-in provider). Only the actual TUI server opts in
// via JCODE_DEFERRED_AUTH_BOOTSTRAP, so `jcode run` and other
// genuinely headless callers still fail loudly.
if std::env::var_os("JCODE_DEFERRED_AUTH_BOOTSTRAP").is_some() {
crate::logging::info(
"No credentials configured; booting deferred-auth MultiProvider for in-TUI onboarding login",
);
let multi = provider::MultiProvider::from_auth_status(availability.auth_status);
crate::env::set_var("JCODE_ACTIVE_PROVIDER", multi.name().to_lowercase());
Arc::new(multi)
} else if non_interactive {
anyhow::bail!(
"No credentials configured. Run 'jcode login' or set ANTHROPIC_API_KEY to authenticate."
);
} else if !allow_login_bootstrap {
anyhow::bail!(
"No credentials configured for provider auto-detection; automatic login/bootstrap is disabled during validation."
);
} else {
let provider_desc = prompt_login_provider_selection(
&crate::provider_catalog::auto_init_login_providers(),
"No credentials found. Let's log in!\n\nChoose a provider:",
)?;
Box::pin(login_and_bootstrap_provider(provider_desc, None)).await?
}
}
}
};
if let Some(mode) = explicit_credential_mode(choice) {
provider.set_credential_mode(mode).map_err(|err| {
anyhow::anyhow!(
"Failed to select the credential route for --provider {}: {err}",
choice.as_arg_value()
)
})?;
}
if std::env::var_os("JCODE_PROVIDER_PROFILE_ACTIVE").is_none()
&& std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_none()
&& model.is_none()
&& let Some(profile) = profile_for_choice(choice)
&& let Some(default_model) = resolved_profile_default_model(profile)
&& provider.set_model(&default_model).is_ok()
{
let resolved = resolve_openai_compatible_profile(profile);
init_notice(&format!(
"Using default model for {}: {}",
resolved.display_name, default_model
));
}
if let Some(model_name) = model {
if let Err(e) = provider.set_model(model_name) {
init_notice(&format!(
"Warning: failed to set model '{}': {}",
model_name, e
));
} else {
init_notice(&format!("Using model: {}", model_name));
}
}
Ok(provider)
}
pub async fn init_provider_and_registry(
choice: &ProviderChoice,
model: Option<&str>,
) -> Result<(Arc<dyn provider::Provider>, tool::Registry)> {
let provider = init_provider(choice, model).await?;
let registry = tool::Registry::new(provider.clone()).await;
Ok((provider, registry))
}
pub async fn init_provider_and_registry_for_validation(
choice: &ProviderChoice,
model: Option<&str>,
) -> Result<(Arc<dyn provider::Provider>, tool::Registry)> {
let provider = init_provider_for_validation(choice, model).await?;
let registry = tool::Registry::new(provider.clone()).await;
Ok((provider, registry))
}
#[cfg(test)]
#[path = "provider_init_tests.rs"]
mod tests;