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
1161 lines
41 KiB
Rust
1161 lines
41 KiB
Rust
use super::*;
|
|
|
|
struct EnvGuard {
|
|
vars: Vec<(String, Option<String>)>,
|
|
}
|
|
|
|
impl EnvGuard {
|
|
fn save(keys: &[&str]) -> Self {
|
|
let vars = keys
|
|
.iter()
|
|
.map(|key| (key.to_string(), std::env::var(key).ok()))
|
|
.collect();
|
|
Self { vars }
|
|
}
|
|
}
|
|
|
|
impl Drop for EnvGuard {
|
|
fn drop(&mut self) {
|
|
for (key, value) in &self.vars {
|
|
if let Some(value) = value {
|
|
crate::env::set_var(key, value);
|
|
} else {
|
|
crate::env::remove_var(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_profiles_have_unique_ids_and_safe_metadata() {
|
|
let mut ids = HashSet::new();
|
|
for profile in openai_compatible_profiles() {
|
|
assert!(
|
|
ids.insert(profile.id),
|
|
"duplicate provider profile id: {}",
|
|
profile.id
|
|
);
|
|
assert!(is_safe_env_key_name(profile.api_key_env));
|
|
assert!(is_safe_env_file_name(profile.env_file));
|
|
assert_eq!(
|
|
normalize_api_base(profile.api_base).as_deref(),
|
|
Some(profile.api_base)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_login_provider_aliases_resolve_to_canonical_ids() {
|
|
assert_eq!(
|
|
resolve_login_provider("subscription").map(|provider| provider.id),
|
|
Some("jcode")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("anthropic").map(|provider| provider.id),
|
|
Some("claude")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("opencodego").map(|provider| provider.id),
|
|
Some("opencode-go")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("z.ai").map(|provider| provider.id),
|
|
Some("zai")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("compat").map(|provider| provider.id),
|
|
Some("openai-compatible")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("aoai").map(|provider| provider.id),
|
|
Some("azure")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("cerberascode").map(|provider| provider.id),
|
|
Some("cerebras")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("bailian").map(|provider| provider.id),
|
|
Some("alibaba-coding-plan")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_provider("gmail").map(|provider| provider.id),
|
|
Some("google")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_issue_profile_metadata_matches_direct_provider_endpoints() {
|
|
assert_eq!(ZAI_PROFILE.api_base, "https://api.z.ai/api/coding/paas/v4");
|
|
assert_eq!(ZAI_PROFILE.default_model, Some("glm-4.5"));
|
|
assert_eq!(DEEPSEEK_PROFILE.api_base, "https://api.deepseek.com");
|
|
assert_eq!(DEEPSEEK_PROFILE.default_model, Some("deepseek-v4-flash"));
|
|
assert_eq!(DEEPSEEK_PROFILE.setup_url, "https://api-docs.deepseek.com/");
|
|
assert_eq!(MINIMAX_PROFILE.api_base, "https://api.minimax.io/v1");
|
|
assert_eq!(MINIMAX_PROFILE.api_key_env, "OPENAI_API_KEY");
|
|
assert_eq!(
|
|
ALIBABA_CODING_PLAN_PROFILE.api_base,
|
|
"https://coding-intl.dashscope.aliyuncs.com/v1"
|
|
);
|
|
assert_eq!(COMTEGRA_PROFILE.api_base, "https://llm.comtegra.cloud/v1");
|
|
assert_eq!(COMTEGRA_PROFILE.default_model, Some("glm-51-nvfp4"));
|
|
assert_eq!(COMTEGRA_PROFILE.api_key_env, "COMTEGRA_API_KEY");
|
|
assert_eq!(CEREBRAS_PROFILE.api_base, "https://api.cerebras.ai/v1");
|
|
assert_eq!(CEREBRAS_PROFILE.default_model, Some("gpt-oss-120b"));
|
|
assert!(!OPENAI_COMPAT_PROFILE.setup_url.contains("opencode.ai"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_named_profile_suggests_newest_cached_live_release() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&["JCODE_HOME"]);
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
jcode_provider_openrouter::save_disk_cache_with_source_for_namespace(
|
|
"cerebras",
|
|
&[
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "older-model".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_700_000_000),
|
|
},
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "newer-model".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_800_000_000),
|
|
},
|
|
],
|
|
Some(CEREBRAS_PROFILE.api_base),
|
|
);
|
|
|
|
let resolved = resolve_openai_compatible_profile(CEREBRAS_PROFILE);
|
|
|
|
assert_eq!(resolved.default_model.as_deref(), Some("newer-model"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_named_profile_skips_non_chat_models_when_picking_newest_default() {
|
|
// Regression: a profile's auto-selected default must never be a non-chat
|
|
// model (TTS/speech/embeddings/image/etc.). Catalogs such as Groq expose
|
|
// their entire model list, and the newest-released entry is frequently a
|
|
// non-chat model (e.g. `canopylabs/orpheus-*` TTS) which previously won the
|
|
// newest-by-created tiebreak and became the chat default.
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&["JCODE_HOME"]);
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
jcode_provider_openrouter::save_disk_cache_with_source_for_namespace(
|
|
"cerebras",
|
|
&[
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "older-chat-model".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_700_000_000),
|
|
},
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "newer-chat-model".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_800_000_000),
|
|
},
|
|
// Newest of all, but a non-chat (TTS) model that must be skipped.
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "canopylabs/orpheus-v1-english".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_900_000_000),
|
|
},
|
|
jcode_provider_openrouter::ModelInfo {
|
|
id: "whisper-large-v3".to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(1_950_000_000),
|
|
},
|
|
],
|
|
Some(CEREBRAS_PROFILE.api_base),
|
|
);
|
|
|
|
let resolved = resolve_openai_compatible_profile(CEREBRAS_PROFILE);
|
|
|
|
assert_eq!(
|
|
resolved.default_model.as_deref(),
|
|
Some("newer-chat-model"),
|
|
"newest *chat* model must win; non-chat models (orpheus TTS, whisper STT) are skipped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn minimax_token_plan_keys_resolve_to_china_endpoint_without_changing_international_default() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&["OPENAI_API_KEY"]);
|
|
crate::env::remove_var("OPENAI_API_KEY");
|
|
|
|
let international = resolve_openai_compatible_profile(MINIMAX_PROFILE);
|
|
assert_eq!(international.api_base, "https://api.minimax.io/v1");
|
|
assert_eq!(
|
|
international.setup_url,
|
|
"https://platform.minimax.io/docs/guides/text-generation"
|
|
);
|
|
|
|
let china = resolve_openai_compatible_profile_with_api_key_hint(
|
|
MINIMAX_PROFILE,
|
|
Some("sk-cp-test-token"),
|
|
);
|
|
assert_eq!(china.api_base, MINIMAX_CHINA_API_BASE);
|
|
assert_eq!(china.setup_url, MINIMAX_CHINA_SETUP_URL);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_issue_lan_openai_compatible_bases_are_valid_for_local_model_servers() {
|
|
assert_eq!(
|
|
normalize_api_base("http://100.103.78.84:11434/v1").as_deref(),
|
|
Some("http://100.103.78.84:11434/v1")
|
|
);
|
|
assert_eq!(
|
|
normalize_api_base("http://hsv.local:11434/v1").as_deref(),
|
|
Some("http://hsv.local:11434/v1")
|
|
);
|
|
assert_eq!(normalize_api_base("http://example.com/v1"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn auth_issue_runtime_display_name_tracks_direct_compatible_profiles() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_OPENROUTER_PROVIDER_FEATURES",
|
|
"JCODE_OPENROUTER_TRANSPORT_STATE",
|
|
"JCODE_OPENROUTER_ALLOW_NO_AUTH",
|
|
"JCODE_RUNTIME_PROVIDER",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
"JCODE_PROVIDER_PROFILE_ACTIVE",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_RUNTIME_PROVIDER", "azure-openai");
|
|
assert_eq!(runtime_provider_display_name("openrouter"), "Azure OpenAI");
|
|
crate::env::remove_var("JCODE_RUNTIME_PROVIDER");
|
|
|
|
apply_openai_compatible_profile_env(Some(DEEPSEEK_PROFILE));
|
|
assert_eq!(runtime_provider_display_name("openrouter"), "DeepSeek");
|
|
|
|
apply_openai_compatible_profile_env(Some(ZAI_PROFILE));
|
|
assert_eq!(runtime_provider_display_name("openrouter"), "Z.AI");
|
|
}
|
|
|
|
#[test]
|
|
fn auth_profile_env_application_flushes_stale_openrouter_catalog_state() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_OPENROUTER_PROVIDER_FEATURES",
|
|
"JCODE_OPENROUTER_TRANSPORT_STATE",
|
|
"JCODE_OPENROUTER_ALLOW_NO_AUTH",
|
|
"JCODE_OPENROUTER_MODEL_CATALOG",
|
|
"JCODE_OPENROUTER_MODEL",
|
|
"JCODE_OPENROUTER_STATIC_MODELS",
|
|
"JCODE_OPENROUTER_AUTH_HEADER",
|
|
"JCODE_OPENROUTER_AUTH_HEADER_NAME",
|
|
"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER",
|
|
"JCODE_OPENROUTER_PROVIDER",
|
|
"JCODE_OPENROUTER_NO_FALLBACK",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
"JCODE_PROVIDER_PROFILE_ACTIVE",
|
|
"JCODE_PROVIDER_PROFILE_NAME",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_OPENROUTER_API_BASE", "https://openrouter.ai/api/v1");
|
|
crate::env::set_var("JCODE_OPENROUTER_API_KEY_NAME", "OPENROUTER_API_KEY");
|
|
crate::env::set_var("JCODE_OPENROUTER_ENV_FILE", "openrouter.env");
|
|
crate::env::set_var("JCODE_OPENROUTER_CACHE_NAMESPACE", "openrouter");
|
|
crate::env::set_var("JCODE_OPENROUTER_PROVIDER_FEATURES", "1");
|
|
crate::env::set_var("JCODE_OPENROUTER_TRANSPORT_STATE", "stale");
|
|
crate::env::set_var("JCODE_OPENROUTER_ALLOW_NO_AUTH", "1");
|
|
crate::env::set_var(
|
|
"JCODE_OPENROUTER_MODEL_CATALOG",
|
|
"stale-openrouter-catalog.json",
|
|
);
|
|
crate::env::set_var("JCODE_OPENROUTER_MODEL", "gpt-5.5");
|
|
crate::env::set_var(
|
|
"JCODE_OPENROUTER_STATIC_MODELS",
|
|
"stale-openrouter-only-model",
|
|
);
|
|
crate::env::set_var("JCODE_OPENROUTER_AUTH_HEADER", "Bearer stale");
|
|
crate::env::set_var("JCODE_OPENROUTER_AUTH_HEADER_NAME", "Authorization");
|
|
crate::env::set_var("JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER", "openrouter");
|
|
crate::env::set_var("JCODE_OPENROUTER_PROVIDER", "openrouter");
|
|
crate::env::set_var("JCODE_OPENROUTER_NO_FALLBACK", "1");
|
|
crate::env::set_var("JCODE_NAMED_PROVIDER_PROFILE", "openrouter");
|
|
crate::env::set_var("JCODE_PROVIDER_PROFILE_ACTIVE", "1");
|
|
crate::env::set_var("JCODE_PROVIDER_PROFILE_NAME", "openrouter");
|
|
|
|
force_apply_openai_compatible_profile_env(Some(CEREBRAS_PROFILE));
|
|
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_BASE").as_deref(),
|
|
Ok("https://api.cerebras.ai/v1")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_KEY_NAME").as_deref(),
|
|
Ok("CEREBRAS_API_KEY")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_ENV_FILE").as_deref(),
|
|
Ok("cerebras.env")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_CACHE_NAMESPACE").as_deref(),
|
|
Ok("cerebras")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_PROVIDER_FEATURES").as_deref(),
|
|
Ok("0")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_TRANSPORT_STATE").as_deref(),
|
|
Ok("direct-api-key")
|
|
);
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_ALLOW_NO_AUTH").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_MODEL_CATALOG").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_MODEL").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_AUTH_HEADER").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_AUTH_HEADER_NAME").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_PROVIDER").is_none());
|
|
assert!(std::env::var_os("JCODE_OPENROUTER_NO_FALLBACK").is_none());
|
|
assert!(std::env::var_os("JCODE_NAMED_PROVIDER_PROFILE").is_none());
|
|
assert!(std::env::var_os("JCODE_PROVIDER_PROFILE_ACTIVE").is_none());
|
|
assert!(std::env::var_os("JCODE_PROVIDER_PROFILE_NAME").is_none());
|
|
assert_ne!(
|
|
std::env::var("JCODE_OPENROUTER_STATIC_MODELS")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("stale-openrouter-only-model")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_login_provider_ids_and_aliases_are_unique() {
|
|
let mut seen = std::collections::HashSet::new();
|
|
for provider in login_providers() {
|
|
assert!(
|
|
seen.insert(provider.id),
|
|
"duplicate login provider identifier: {}",
|
|
provider.id
|
|
);
|
|
for alias in provider.aliases {
|
|
assert!(
|
|
seen.insert(*alias),
|
|
"duplicate login provider alias: {}",
|
|
alias
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_tui_login_selection_supports_numbers_and_names() {
|
|
let providers = tui_login_providers();
|
|
assert_eq!(
|
|
resolve_login_selection("1", &providers).map(|provider| provider.id),
|
|
Some("auto-import")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("2", &providers).map(|provider| provider.id),
|
|
Some("claude")
|
|
);
|
|
// `anthropic-api` sits at 3 (between claude and openai), shifting the
|
|
// rest of the list down one slot relative to the pre-May-2026 order.
|
|
assert_eq!(
|
|
resolve_login_selection("3", &providers).map(|provider| provider.id),
|
|
Some("anthropic-api")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("7", &providers).map(|provider| provider.id),
|
|
Some("bedrock")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("compat", &providers).map(|provider| provider.id),
|
|
Some("openai-compatible")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("cgc", &providers).map(|provider| provider.id),
|
|
Some("comtegra")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("bedrock", &providers).map(|provider| provider.id),
|
|
Some("bedrock")
|
|
);
|
|
assert!(
|
|
providers
|
|
.iter()
|
|
.take(7)
|
|
.any(|provider| provider.id == "bedrock")
|
|
);
|
|
assert!(resolve_login_selection("google", &providers).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_cli_login_selection_preserves_existing_order() {
|
|
let providers = cli_login_providers();
|
|
assert_eq!(
|
|
resolve_login_selection("1", &providers).map(|provider| provider.id),
|
|
Some("auto-import")
|
|
);
|
|
// `anthropic-api` at 3 shifted everything after it down one slot.
|
|
assert_eq!(
|
|
resolve_login_selection("3", &providers).map(|provider| provider.id),
|
|
Some("anthropic-api")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("5", &providers).map(|provider| provider.id),
|
|
Some("jcode")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("6", &providers).map(|provider| provider.id),
|
|
Some("copilot")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("7", &providers).map(|provider| provider.id),
|
|
Some("openrouter")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("8", &providers).map(|provider| provider.id),
|
|
Some("bedrock")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("9", &providers).map(|provider| provider.id),
|
|
Some("azure")
|
|
);
|
|
assert_eq!(
|
|
resolve_login_selection("bedrock", &providers).map(|provider| provider.id),
|
|
Some("bedrock")
|
|
);
|
|
assert!(
|
|
providers
|
|
.iter()
|
|
.position(|provider| provider.id == "bedrock")
|
|
< providers.iter().position(|provider| provider.id == "azure")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openrouter_like_sources_include_all_static_profiles() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
]);
|
|
crate::env::remove_var("JCODE_OPENROUTER_API_KEY_NAME");
|
|
crate::env::remove_var("JCODE_OPENROUTER_ENV_FILE");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_API_KEY_NAME");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_ENV_FILE");
|
|
|
|
let sources = openrouter_like_api_key_sources();
|
|
drop(guard);
|
|
|
|
assert!(sources.contains(&(
|
|
"OPENROUTER_API_KEY".to_string(),
|
|
"openrouter.env".to_string()
|
|
)));
|
|
for profile in openai_compatible_profiles() {
|
|
if profile.requires_api_key {
|
|
assert!(sources.contains(&(
|
|
profile.api_key_env.to_string(),
|
|
profile.env_file.to_string()
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openrouter_like_sources_accept_valid_overrides() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_OPENROUTER_API_KEY_NAME", "ALT_OPENROUTER_KEY");
|
|
crate::env::set_var("JCODE_OPENROUTER_ENV_FILE", "alt-openrouter.env");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_KEY_NAME", "ALT_COMPAT_KEY");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_ENV_FILE", "alt-compat.env");
|
|
|
|
let sources = openrouter_like_api_key_sources();
|
|
assert!(sources.contains(&(
|
|
"ALT_OPENROUTER_KEY".to_string(),
|
|
"alt-openrouter.env".to_string()
|
|
)));
|
|
assert!(sources.contains(&("ALT_COMPAT_KEY".to_string(), "alt-compat.env".to_string())));
|
|
}
|
|
|
|
#[test]
|
|
fn named_provider_config_accepts_openai_compatible_spelling() {
|
|
let cfg: crate::config::Config = toml::from_str(
|
|
r#"
|
|
[providers.my-gateway]
|
|
type = "openai-compatible"
|
|
base_url = "https://llm.example.com/v1"
|
|
auth = "bearer"
|
|
api_key_env = "MY_GATEWAY_API_KEY"
|
|
default_model = "opaque/model@id"
|
|
|
|
[[providers.my-gateway.models]]
|
|
id = "opaque/model@id"
|
|
input = ["text"]
|
|
"#,
|
|
)
|
|
.expect("config should parse");
|
|
|
|
let profile = cfg.providers.get("my-gateway").expect("profile");
|
|
assert_eq!(
|
|
profile.provider_type,
|
|
crate::config::NamedProviderType::OpenAiCompatible
|
|
);
|
|
assert_eq!(profile.base_url, "https://llm.example.com/v1");
|
|
assert_eq!(profile.default_model.as_deref(), Some("opaque/model@id"));
|
|
assert_eq!(profile.models[0].id, "opaque/model@id");
|
|
}
|
|
|
|
#[test]
|
|
fn named_provider_profile_reports_malformed_config_instead_of_unknown_profile() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let previous_home = std::env::var_os("JCODE_HOME");
|
|
let temp = tempfile::TempDir::new().expect("tempdir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
crate::config::Config::invalidate_cache();
|
|
|
|
let config_path = crate::config::Config::path().expect("config path");
|
|
std::fs::create_dir_all(config_path.parent().expect("config parent"))
|
|
.expect("create config dir");
|
|
std::fs::write(
|
|
&config_path,
|
|
r#"
|
|
[providers.antigravity]
|
|
type = "anthropic-compatible"
|
|
base_url = "http://192.168.1.202:8080"
|
|
api_key_env = "ANTIGRAVITY_API_KEY"
|
|
default_model = "gemini-3.1-pro-low"
|
|
|
|
[[providers.antigravity.models]]
|
|
id = "gemini-3.1-pro-low"
|
|
context_window = 128000
|
|
"#,
|
|
)
|
|
.expect("write config");
|
|
|
|
let err = apply_named_provider_profile_env("antigravity").expect_err("malformed config");
|
|
let message = err.to_string();
|
|
assert!(
|
|
message.contains("Failed to parse config file"),
|
|
"unexpected error: {message}"
|
|
);
|
|
assert!(
|
|
message.contains("anthropic-compatible"),
|
|
"unexpected error: {message}"
|
|
);
|
|
assert!(
|
|
!message.contains("Unknown provider profile"),
|
|
"unexpected error: {message}"
|
|
);
|
|
|
|
if let Some(previous_home) = previous_home {
|
|
crate::env::set_var("JCODE_HOME", previous_home);
|
|
} else {
|
|
crate::env::remove_var("JCODE_HOME");
|
|
}
|
|
crate::config::Config::invalidate_cache();
|
|
}
|
|
|
|
#[test]
|
|
fn named_provider_profile_maps_to_openai_compatible_runtime_env() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_OPENROUTER_PROVIDER_FEATURES",
|
|
"JCODE_OPENROUTER_TRANSPORT_STATE",
|
|
"JCODE_OPENROUTER_ALLOW_NO_AUTH",
|
|
"JCODE_OPENROUTER_MODEL_CATALOG",
|
|
"JCODE_OPENROUTER_MODEL",
|
|
"JCODE_OPENROUTER_STATIC_MODELS",
|
|
"JCODE_OPENROUTER_AUTH_HEADER",
|
|
"JCODE_OPENROUTER_AUTH_HEADER_NAME",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
"MY_GATEWAY_API_KEY",
|
|
]);
|
|
|
|
let cfg: crate::config::Config = toml::from_str(
|
|
r#"
|
|
[providers.my-gateway]
|
|
type = "openai-compatible"
|
|
base_url = "https://llm.example.com/v1/"
|
|
auth = "header"
|
|
auth_header = "x-api-key"
|
|
api_key_env = "MY_GATEWAY_API_KEY"
|
|
default_model = "opaque/model@id"
|
|
model_catalog = false
|
|
|
|
[[providers.my-gateway.models]]
|
|
id = "opaque/model@id"
|
|
|
|
[[providers.my-gateway.models]]
|
|
id = "another-local-id"
|
|
"#,
|
|
)
|
|
.expect("config should parse");
|
|
|
|
apply_named_provider_profile_env_from_config("my-gateway", &cfg).expect("apply profile");
|
|
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_BASE").ok().as_deref(),
|
|
Some("https://llm.example.com/v1")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_KEY_NAME")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("MY_GATEWAY_API_KEY")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_PROVIDER_FEATURES")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("0")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_TRANSPORT_STATE")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("direct-api-key")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_MODEL_CATALOG")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("0")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_MODEL").ok().as_deref(),
|
|
Some("opaque/model@id")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_STATIC_MODELS")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("opaque/model@id\nanother-local-id")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_AUTH_HEADER")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("api-key")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_AUTH_HEADER_NAME")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("x-api-key")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_NAMED_PROVIDER_PROFILE")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("my-gateway")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn named_provider_inline_api_key_is_private_runtime_fallback() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_OPENROUTER_PROVIDER_FEATURES",
|
|
"JCODE_OPENROUTER_TRANSPORT_STATE",
|
|
"JCODE_OPENROUTER_MODEL_CATALOG",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
"JCODE_PROVIDER_MY_GATEWAY_API_KEY",
|
|
]);
|
|
|
|
let cfg: crate::config::Config = toml::from_str(
|
|
r#"
|
|
[providers.my-gateway]
|
|
type = "openai-compatible"
|
|
base_url = "https://llm.example.com/v1"
|
|
api_key = "inline-secret"
|
|
"#,
|
|
)
|
|
.expect("config should parse");
|
|
|
|
apply_named_provider_profile_env_from_config("my-gateway", &cfg).expect("apply profile");
|
|
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_KEY_NAME")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("JCODE_PROVIDER_MY_GATEWAY_API_KEY")
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_PROVIDER_MY_GATEWAY_API_KEY")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("inline-secret")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openrouter_like_sources_reject_invalid_overrides() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_OPENROUTER_API_KEY_NAME", "bad-key-name");
|
|
crate::env::set_var("JCODE_OPENROUTER_ENV_FILE", "../bad.env");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_KEY_NAME", "bad key");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_ENV_FILE", "../bad-compat.env");
|
|
|
|
let sources = openrouter_like_api_key_sources();
|
|
assert!(
|
|
!sources
|
|
.iter()
|
|
.any(|(key, _)| key == "bad-key-name" || key == "bad key")
|
|
);
|
|
assert!(
|
|
!sources
|
|
.iter()
|
|
.any(|(_, file)| file == "../bad.env" || file == "../bad-compat.env")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openai_compatible_profile_overrides_apply_when_valid() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENAI_COMPAT_API_BASE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_DEFAULT_MODEL",
|
|
]);
|
|
|
|
crate::env::set_var(
|
|
"JCODE_OPENAI_COMPAT_API_BASE",
|
|
"https://api.groq.com/openai/v1/",
|
|
);
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_KEY_NAME", "GROQ_API_KEY");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_ENV_FILE", "groq.env");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_DEFAULT_MODEL", "openai/gpt-oss-120b");
|
|
|
|
let resolved = resolve_openai_compatible_profile(OPENAI_COMPAT_PROFILE);
|
|
assert_eq!(resolved.api_base, "https://api.groq.com/openai/v1");
|
|
assert_eq!(resolved.api_key_env, "GROQ_API_KEY");
|
|
assert_eq!(resolved.env_file, "groq.env");
|
|
assert_eq!(
|
|
resolved.default_model.as_deref(),
|
|
Some("openai/gpt-oss-120b")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openai_compatible_profile_overrides_reject_invalid_values() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENAI_COMPAT_API_BASE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_BASE", "http://example.com/v1");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_KEY_NAME", "bad-key-name");
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_ENV_FILE", "../bad.env");
|
|
|
|
let resolved = resolve_openai_compatible_profile(OPENAI_COMPAT_PROFILE);
|
|
assert_eq!(resolved.api_base, OPENAI_COMPAT_PROFILE.api_base);
|
|
assert_eq!(resolved.api_key_env, OPENAI_COMPAT_PROFILE.api_key_env);
|
|
assert_eq!(resolved.env_file, OPENAI_COMPAT_PROFILE.env_file);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openai_compatible_profile_overrides_read_from_env_file() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config_root = temp.path().join("config").join("jcode");
|
|
std::fs::create_dir_all(&config_root).expect("config dir");
|
|
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_HOME",
|
|
"JCODE_OPENAI_COMPAT_API_BASE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_DEFAULT_MODEL",
|
|
]);
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_API_BASE");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_API_KEY_NAME");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_ENV_FILE");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_DEFAULT_MODEL");
|
|
std::fs::write(
|
|
config_root.join(OPENAI_COMPAT_PROFILE.env_file),
|
|
concat!(
|
|
"JCODE_OPENAI_COMPAT_API_BASE=https://api.example.com/v1\n",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME=EXAMPLE_API_KEY\n",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE=example.env\n",
|
|
"JCODE_OPENAI_COMPAT_DEFAULT_MODEL=example/model\n",
|
|
),
|
|
)
|
|
.expect("env file");
|
|
|
|
let resolved = resolve_openai_compatible_profile(OPENAI_COMPAT_PROFILE);
|
|
assert_eq!(resolved.api_base, "https://api.example.com/v1");
|
|
assert_eq!(resolved.api_key_env, "EXAMPLE_API_KEY");
|
|
assert_eq!(resolved.env_file, "example.env");
|
|
assert_eq!(resolved.default_model.as_deref(), Some("example/model"));
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_openai_compatible_localhost_override_allows_no_auth() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENAI_COMPAT_API_BASE",
|
|
"JCODE_OPENAI_COMPAT_API_KEY_NAME",
|
|
"JCODE_OPENAI_COMPAT_ENV_FILE",
|
|
"JCODE_OPENAI_COMPAT_LOCAL_ENABLED",
|
|
]);
|
|
|
|
crate::env::set_var("JCODE_OPENAI_COMPAT_API_BASE", "http://localhost:11434/v1");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_API_KEY_NAME");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_ENV_FILE");
|
|
crate::env::remove_var("JCODE_OPENAI_COMPAT_LOCAL_ENABLED");
|
|
|
|
let resolved = resolve_openai_compatible_profile(OPENAI_COMPAT_PROFILE);
|
|
assert_eq!(resolved.api_base, "http://localhost:11434/v1");
|
|
assert!(!resolved.requires_api_key);
|
|
assert!(openai_compatible_profile_is_configured(
|
|
OPENAI_COMPAT_PROFILE
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_load_api_key_from_env_or_config_prefers_env() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config_root = temp.path().join("config").join("jcode");
|
|
std::fs::create_dir_all(&config_root).expect("config dir");
|
|
|
|
let _guard = EnvGuard::save(&["JCODE_HOME", "OPENCODE_API_KEY"]);
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
crate::env::set_var("OPENCODE_API_KEY", "env-secret");
|
|
std::fs::write(
|
|
config_root.join("opencode.env"),
|
|
"OPENCODE_API_KEY=file-secret\n",
|
|
)
|
|
.expect("env file");
|
|
|
|
assert_eq!(
|
|
load_api_key_from_env_or_config("OPENCODE_API_KEY", "opencode.env").as_deref(),
|
|
Some("env-secret")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_load_api_key_from_env_or_config_reads_config_file() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config_root = temp.path().join("config").join("jcode");
|
|
std::fs::create_dir_all(&config_root).expect("config dir");
|
|
|
|
let _guard = EnvGuard::save(&["JCODE_HOME", "OPENCODE_API_KEY"]);
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
crate::env::remove_var("OPENCODE_API_KEY");
|
|
std::fs::write(
|
|
config_root.join("opencode.env"),
|
|
"OPENCODE_API_KEY=file-secret\n",
|
|
)
|
|
.expect("env file");
|
|
|
|
assert_eq!(
|
|
load_api_key_from_env_or_config("OPENCODE_API_KEY", "opencode.env").as_deref(),
|
|
Some("file-secret")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn load_api_key_accepts_legacy_zai_key_name() {
|
|
let _lock = crate::storage::lock_test_env();
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config_root = temp.path().join("config").join("jcode");
|
|
std::fs::create_dir_all(&config_root).expect("config dir");
|
|
|
|
let _guard = EnvGuard::save(&["JCODE_HOME", "ZHIPU_API_KEY", "ZAI_API_KEY"]);
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
crate::env::remove_var("ZHIPU_API_KEY");
|
|
crate::env::remove_var("ZAI_API_KEY");
|
|
std::fs::write(config_root.join("zai.env"), "ZAI_API_KEY=legacy-secret\n").expect("env file");
|
|
|
|
assert_eq!(
|
|
load_api_key_from_env_or_config("ZHIPU_API_KEY", "zai.env").as_deref(),
|
|
Some("legacy-secret")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quality_tier_ranks_flagship_above_bare_above_cheap() {
|
|
// Flagship-marked ids (max/pro/opus/coder/large/huge-param) -> tier 2.
|
|
assert_eq!(openai_compatible_model_quality_tier("qwen3-max"), 2);
|
|
assert_eq!(openai_compatible_model_quality_tier("claude-opus-4-8"), 2);
|
|
assert_eq!(openai_compatible_model_quality_tier("qwen3-coder-480b"), 2);
|
|
assert_eq!(openai_compatible_model_quality_tier("glm-4.6-pro"), 2);
|
|
assert_eq!(
|
|
openai_compatible_model_quality_tier("llama-3.1-405b-instruct"),
|
|
2
|
|
);
|
|
|
|
// Bare frontier ids (no tier marker) -> tier 1.
|
|
assert_eq!(openai_compatible_model_quality_tier("gpt-5.5"), 1);
|
|
assert_eq!(openai_compatible_model_quality_tier("minimax-m2.7"), 1);
|
|
assert_eq!(openai_compatible_model_quality_tier("kimi-k2.5"), 1);
|
|
assert_eq!(openai_compatible_model_quality_tier("glm-4.6"), 1);
|
|
|
|
// Cheap/small/fast-marked ids -> tier 0.
|
|
assert_eq!(openai_compatible_model_quality_tier("gpt-5.5-mini"), 0);
|
|
assert_eq!(openai_compatible_model_quality_tier("deepseek-v4-flash"), 0);
|
|
assert_eq!(openai_compatible_model_quality_tier("glm-4.6-air"), 0);
|
|
assert_eq!(
|
|
openai_compatible_model_quality_tier("llama-3.1-8b-instant"),
|
|
0
|
|
);
|
|
assert_eq!(openai_compatible_model_quality_tier("claude-haiku-4-5"), 0);
|
|
|
|
// Brand names that merely *contain* a marker substring must NOT trip the
|
|
// whole-token matcher: `minimax` is not `mini`/`max`.
|
|
assert_eq!(openai_compatible_model_quality_tier("minimax-m2.7"), 1);
|
|
|
|
// Flagship marker beats a co-occurring size token.
|
|
assert_eq!(
|
|
openai_compatible_model_quality_tier("qwen3-coder-30b-a3b"),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn newest_release_picker_prefers_strongest_tier_over_newest_cheap() {
|
|
use jcode_provider_openrouter::ModelInfo;
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _env = EnvGuard::save(&["JCODE_HOME"]);
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
|
|
let mk = |id: &str, created: u64| ModelInfo {
|
|
id: id.to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(created),
|
|
};
|
|
|
|
// A heterogeneous proxy catalog (like OpenCode Zen): the NEWEST model is a
|
|
// cheap `*-flash`, but a slightly older flagship-marked model exists. The
|
|
// picker must choose the flagship, not the newest-cheap.
|
|
jcode_provider_openrouter::save_disk_cache_with_source_for_namespace(
|
|
"deepseek",
|
|
&[
|
|
mk("deepseek-v4-flash", 1_900_000_000), // newest, but cheap tier
|
|
mk("deepseek-v4", 1_850_000_000), // bare frontier
|
|
mk("deepseek-v4-coder", 1_800_000_000), // flagship tier, oldest
|
|
],
|
|
Some("https://api.deepseek.com"),
|
|
);
|
|
|
|
assert_eq!(
|
|
newest_released_model_for_openai_compatible_profile("deepseek").as_deref(),
|
|
Some("deepseek-v4-coder"),
|
|
"a flagship-marked model must win over a newer cheap/flash sibling"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn newest_release_picker_uses_recency_within_a_tier() {
|
|
use jcode_provider_openrouter::ModelInfo;
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _env = EnvGuard::save(&["JCODE_HOME"]);
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
crate::env::set_var("JCODE_HOME", temp.path());
|
|
|
|
let mk = |id: &str, created: u64| ModelInfo {
|
|
id: id.to_string(),
|
|
name: String::new(),
|
|
context_length: None,
|
|
pricing: Default::default(),
|
|
created: Some(created),
|
|
};
|
|
|
|
// All same (bare frontier) tier: recency decides.
|
|
jcode_provider_openrouter::save_disk_cache_with_source_for_namespace(
|
|
"deepseek",
|
|
&[
|
|
mk("deepseek-v3", 1_700_000_000),
|
|
mk("deepseek-v4", 1_900_000_000), // newest within the same tier
|
|
mk("deepseek-v3.1", 1_800_000_000),
|
|
],
|
|
Some("https://api.deepseek.com"),
|
|
);
|
|
|
|
assert_eq!(
|
|
newest_released_model_for_openai_compatible_profile("deepseek").as_deref(),
|
|
Some("deepseek-v4"),
|
|
"within one quality tier the newest release should win"
|
|
);
|
|
}
|
|
|
|
/// Exhaustiveness guard: every model shipped in a profile's static catalog must
|
|
/// resolve to a concrete context window. Open-weight gateways frequently omit
|
|
/// `context_length` from `/v1/models`, so a missing entry here means that model
|
|
/// would silently fall back to the generic 200K default. First-party
|
|
/// OpenAI/Claude/Gemini ids are resolved by their own providers (not this static
|
|
/// table) and are exempted.
|
|
#[test]
|
|
fn every_static_profile_model_has_a_known_context_limit() {
|
|
use jcode_provider_core::models::context_limit_for_model_with_provider;
|
|
|
|
// Ids handled by dedicated first-party providers rather than the
|
|
// OpenAI-compatible static table.
|
|
fn is_first_party(model: &str) -> bool {
|
|
let m = model.to_ascii_lowercase();
|
|
m.starts_with("claude-")
|
|
|| m.starts_with("gpt-")
|
|
|| m.starts_with("gemini-")
|
|
|| m.starts_with("o3")
|
|
|| m.starts_with("o4")
|
|
}
|
|
|
|
let mut missing: Vec<(String, String)> = Vec::new();
|
|
for profile in jcode_provider_metadata::openai_compatible_profiles()
|
|
.iter()
|
|
.copied()
|
|
{
|
|
for model in openai_compatible_profile_static_models(profile) {
|
|
if is_first_party(&model) {
|
|
continue;
|
|
}
|
|
|
|
let via_profile = openai_compatible_profile_context_limit(profile.id, &model);
|
|
let via_global = context_limit_for_model_with_provider(&model, Some("openrouter"));
|
|
|
|
if via_profile.is_none() && via_global.is_none() {
|
|
missing.push((profile.id.to_string(), model));
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
missing.is_empty(),
|
|
"static profile models without a known context limit (would fall back to the \
|
|
generic default); add them to open_weight_family_context_limit: {missing:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn open_weight_family_context_limits_match_published_windows() {
|
|
use jcode_provider_core::models::open_weight_family_context_limit as f;
|
|
|
|
// GLM family spelling variants across gateways.
|
|
assert_eq!(f("glm-4.5"), Some(128_000));
|
|
assert_eq!(f("glm-4.7"), Some(200_000));
|
|
assert_eq!(f("zai-org/glm-4.7"), Some(200_000));
|
|
assert_eq!(f("accounts/fireworks/models/glm-4p7"), Some(200_000));
|
|
assert_eq!(f("glm-5"), Some(200_000));
|
|
assert_eq!(f("glm-5.1"), Some(200_000));
|
|
assert_eq!(f("zai-glm-5-1"), Some(200_000));
|
|
assert_eq!(f("glm-5.2"), Some(1_000_000));
|
|
|
|
// Other open-weight families.
|
|
assert_eq!(f("kimi-k2.5"), Some(262_144));
|
|
assert_eq!(f("minimax-m2.7"), Some(204_800));
|
|
assert_eq!(f("mimo-v2.5"), Some(262_144));
|
|
assert_eq!(f("deepseek-v3.2"), Some(163_840));
|
|
assert_eq!(f("deepseek-v4-pro"), Some(1_000_000));
|
|
assert_eq!(f("qwen3-235b-a22b-instruct-2507"), Some(262_144));
|
|
assert_eq!(f("gpt-oss-120b"), Some(131_072));
|
|
assert_eq!(f("llama-3.3-70b-instruct"), Some(131_072));
|
|
assert_eq!(f("sonar-pro"), Some(128_000));
|
|
|
|
// Unknown families stay unresolved so the dynamic cache/default can act.
|
|
assert_eq!(f("some-unknown-model"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn minimax_default_provider_applies_openai_api_key_env_not_openrouter() {
|
|
// Regression for #407: `default_provider = "minimax"` (the built-in MiniMax
|
|
// profile) must resolve credentials from the profile's documented
|
|
// OPENAI_API_KEY / minimax.env, not the generic OPENROUTER_API_KEY /
|
|
// openrouter.env. The earlier bug surfaced as
|
|
// "OPENROUTER_API_KEY not found ..." when applying the configured
|
|
// default_model.
|
|
let _lock = crate::storage::lock_test_env();
|
|
let _guard = EnvGuard::save(&[
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_PROVIDER_PROFILE_ACTIVE",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
]);
|
|
for v in [
|
|
"JCODE_OPENROUTER_API_KEY_NAME",
|
|
"JCODE_OPENROUTER_ENV_FILE",
|
|
"JCODE_OPENROUTER_API_BASE",
|
|
"JCODE_OPENROUTER_CACHE_NAMESPACE",
|
|
"JCODE_PROVIDER_PROFILE_ACTIVE",
|
|
"JCODE_NAMED_PROVIDER_PROFILE",
|
|
] {
|
|
crate::env::remove_var(v);
|
|
}
|
|
|
|
let selection = resolve_openai_compatible_profile_selection("minimax");
|
|
assert_eq!(
|
|
selection.map(|profile| profile.id),
|
|
Some("minimax"),
|
|
"default_provider=minimax must resolve the built-in MiniMax profile"
|
|
);
|
|
|
|
apply_openai_compatible_profile_env(selection);
|
|
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_API_KEY_NAME")
|
|
.ok()
|
|
.as_deref(),
|
|
Some("OPENAI_API_KEY"),
|
|
"MiniMax profile must use OPENAI_API_KEY, not OPENROUTER_API_KEY"
|
|
);
|
|
assert_eq!(
|
|
std::env::var("JCODE_OPENROUTER_ENV_FILE").ok().as_deref(),
|
|
Some("minimax.env"),
|
|
"MiniMax profile must use minimax.env, not openrouter.env"
|
|
);
|
|
}
|