1 line
1.2 MiB
Plaintext
1 line
1.2 MiB
Plaintext
{"repo": "atulpokharel-gp/jcode-jarvise", "files": {"crates/jcode-base/src/provider/openrouter_tests.rs": {"eebf0738f9bd665b2212169deb41328531eff83f": {"t": 1785745846.9419308, "n": 99543, "h": "4cc1a537e2f8f54f", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"not supported\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn named_profile_set_model_strips_own_session_routing_prefix() {\n // Session restore persists `<profile>:<model>`; the standalone provider\n // must normalize its own profile prefix back to the bare model id so the\n // upstream API never sees `tokenrouter:MiniMax-M3` (issues #382/#383/#363).\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"tokenrouter:MiniMax-M3\").unwrap();\n assert_eq!(provider.model(), \"MiniMax-M3\");\n\n // Bare ids still work unchanged.\n provider.set_model(\"MiniMax-M3\").unwrap();\n assert_eq!(provider.model(), \"MiniMax-M3\");\n}\n\n#[test]\nfn named_profile_set_model_strips_other_known_profile_prefix() {\n // A session saved under one built-in OpenAI-compatible profile and\n // reattached under another must still normalize to the bare model id.\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"kimi:kimi-for-coding\").unwrap();\n assert_eq!(provider.model(), \"kimi-for-coding\");\n}\n\n#[test]\nfn named_profile_set_model_keeps_builtin_routing_prefixes() {\n // Built-in provider routing prefixes must round-trip verbatim so a user can\n // switch the active provider from a saved session.\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n for spec in [\n \"claude-oauth:claude-opus-4-8\",\n \"openai-api:gpt-5.4\",\n \"copilot:gpt-5.4\",\n ] {\n provider.set_model(spec).unwrap();\n assert_eq!(provider.model(), spec, \"spec {spec} must be preserved\");\n }\n}\n\n#[test]\nfn named_profile_set_model_keeps_unknown_prefix_with_colon() {\n // A `:`-bearing id whose prefix is neither this profile nor a known\n // built-in profile must be preserved verbatim (it may be a real model id).\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"some-vendor:weird-model\").unwrap();\n assert_eq!(provider.model(), \"some-vendor:weird-model\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let base = ProviderRouting {\n order: Some(vec![\"fireworks\".to_string()]),\n ..Default::default()\n };\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n// ============================================================================\n// Mid-stream retry rollback (issue #338 gap #3)\n// ============================================================================\n\n/// Fake SSE server: the first connection streams partial output then drops the\n/// socket mid-stream (transport fault); the second connection streams a clean,\n/// complete response.\nfn spawn_midstream_fault_then_complete_server() -> String {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n\n std::thread::spawn(move || {\n // Connection 1: partial output, then abrupt close (no [DONE]).\n {\n let (mut stream, _) = listener.accept().expect(\"accept first request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"partial answer that must not duplicate\\\"}}]}\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n{:x}\\r\\n{}\\r\\n\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write partial response\");\n stream.flush().expect(\"flush partial response\");\n // Drop without terminating the chunked encoding: the client sees\n // an unexpected EOF mid-stream (transient transport fault).\n drop(stream);\n }\n\n // Connection 2 (the retry): clean complete response.\n {\n let (mut stream, _) = listener.accept().expect(\"accept retry request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = concat!(\n \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"final answer\\\"},\\\"finish_reason\\\":\\\"stop\\\"}]}\\n\\n\",\n \"data: [DONE]\\n\\n\",\n );\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write retry response\");\n }\n });\n\n format!(\"http://{addr}/v1\")\n}\n\n/// Regression for issue #338 gap #3: a transient transport fault that hits\n/// mid-stream, after partial output has already been emitted, must surface a\n/// `RetryRollback` before the replayed response so consumers can discard the\n/// partial attempt instead of rendering duplicated output.\n#[test]\nfn midstream_transport_fault_emits_retry_rollback_before_replay() {\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n\n rt.block_on(async {\n let api_base = spawn_midstream_fault_then_complete_server();\n let client = reqwest::Client::new();\n let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<StreamEvent>>(64);\n\n let request = serde_json::json!({\n \"model\": \"test-model\",\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"stream\": true,\n });\n\n super::openrouter_sse_stream::run_stream_with_retries(\n client,\n api_base,\n ProviderAuth::None {\n label: \"test\".to_string(),\n },\n false,\n request,\n tx,\n Arc::new(Mutex::new(None)),\n \"test-model\".to_string(),\n )\n .await;\n\n let mut events = Vec::new();\n while let Some(item) = rx.recv().await {\n events.push(item);\n }\n\n let mut saw_partial = false;\n let mut rollback_after_partial = false;\n let mut final_after_rollback = false;\n let mut duplicate_partial_without_rollback = false;\n for item in &events {\n let Ok(event) = item else {\n panic!(\"stream surfaced an error instead of retrying: {item:?}\");\n };\n match event {\n StreamEvent::TextDelta(text) => {\n if text.contains(\"partial answer\") {\n if saw_partial && !rollback_after_partial {\n duplicate_partial_without_rollback = true;\n }\n saw_partial = true;\n }\n if text.contains(\"final answer\") {\n assert!(\n rollback_after_partial,\n \"replayed response arrived without a RetryRollback after partial output\"\n );\n final_after_rollback = true;\n }\n }\n StreamEvent::RetryRollback { .. } => {\n assert!(\n saw_partial,\n \"RetryRollback must only be emitted after partial output was streamed\"\n );\n rollback_after_partial = true;\n }\n _ => {}\n }\n }\n\n assert!(saw_partial, \"first attempt's partial output never arrived\");\n assert!(\n rollback_after_partial,\n \"no RetryRollback emitted for the mid-stream fault\"\n );\n assert!(\n final_after_rollback,\n \"retry never delivered the complete response\"\n );\n assert!(\n !duplicate_partial_without_rollback,\n \"partial output duplicated without an interleaved rollback\"\n );\n });\n}\n\n/// Issue #352: reasoning effort must follow the *model family*, not just the\n/// dedicated `deepseek` profile id. A custom compat endpoint (named profile or\n/// generic openai-compatible) serving a DeepSeek model supports `/effort`.\n#[test]\nfn compat_profile_serving_deepseek_model_supports_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n // Non-DeepSeek model on a custom endpoint: no effort support.\n provider.set_model(\"some-random-model\").unwrap();\n assert!(provider.available_efforts().is_empty());\n assert!(provider.set_reasoning_effort(\"high\").is_err());\n assert_eq!(provider.reasoning_effort(), None);\n\n // DeepSeek-family model: DeepSeek-style efforts become available.\n provider.set_model(\"deepseek-v4-flash\").unwrap();\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"deepseek model on compat endpoint accepts effort\");\n assert_eq!(provider.reasoning_effort(), Some(\"high\".to_string()));\n}\n\n/// Issue #352: named-profile config can override effort support explicitly in\n/// both directions.\n#[test]\nfn named_profile_supports_reasoning_effort_config_override() {\n let force_on = OpenRouterProvider {\n reasoning_effort_support: Some(true),\n ..make_custom_compatible_provider()\n };\n force_on.set_model(\"not-a-deepseek-model\").unwrap();\n assert_eq!(\n force_on.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n force_on\n .set_reasoning_effort(\"medium\")\n .expect(\"explicit supports_reasoning_effort=true enables effort\");\n assert_eq!(force_on.reasoning_effort(), Some(\"medium\".to_string()));\n\n let force_off = OpenRouterProvider {\n reasoning_effort_support: Some(false),\n ..make_custom_compatible_provider()\n };\n force_off.set_model(\"deepseek-v4-flash\").unwrap();\n assert!(force_off.available_efforts().is_empty());\n assert!(\n force_off.set_reasoning_effort(\"high\").is_err(),\n \"explicit supports_reasoning_effort=false suppresses model auto-detection\"\n );\n}\n\n/// Issue #352: named profiles construct with the user's configured\n/// `openai_reasoning_effort` when the profile supports effort, instead of\n/// silently ignoring the config.\n#[test]\nfn named_profile_construction_reads_openai_reasoning_effort_config() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"deepseek-v4\".to_string()),\n supports_reasoning_effort: Some(true),\n ..Default::default()\n };\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n // The config default is only applied when openai_reasoning_effort is set;\n // with no config value the provider starts with no effort but still\n // supports setting one.\n let initial = provider.reasoning_effort();\n let configured = crate::config::config()\n .provider\n .openai_reasoning_effort\n .clone();\n match configured {\n Some(_) => assert!(initial.is_some(), \"configured effort must be honored\"),\n None => assert_eq!(initial, None),\n }\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"explicitly-enabled profile accepts effort\");\n}\n"}, "0cd825d5e2a919c2a5b832a582a0c8980a3aed97": {"t": 1785757203.7474475, "n": 99543, "h": "4cc1a537e2f8f54f", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"not supported\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn named_profile_set_model_strips_own_session_routing_prefix() {\n // Session restore persists `<profile>:<model>`; the standalone provider\n // must normalize its own profile prefix back to the bare model id so the\n // upstream API never sees `tokenrouter:MiniMax-M3` (issues #382/#383/#363).\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"tokenrouter:MiniMax-M3\").unwrap();\n assert_eq!(provider.model(), \"MiniMax-M3\");\n\n // Bare ids still work unchanged.\n provider.set_model(\"MiniMax-M3\").unwrap();\n assert_eq!(provider.model(), \"MiniMax-M3\");\n}\n\n#[test]\nfn named_profile_set_model_strips_other_known_profile_prefix() {\n // A session saved under one built-in OpenAI-compatible profile and\n // reattached under another must still normalize to the bare model id.\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"kimi:kimi-for-coding\").unwrap();\n assert_eq!(provider.model(), \"kimi-for-coding\");\n}\n\n#[test]\nfn named_profile_set_model_keeps_builtin_routing_prefixes() {\n // Built-in provider routing prefixes must round-trip verbatim so a user can\n // switch the active provider from a saved session.\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n for spec in [\n \"claude-oauth:claude-opus-4-8\",\n \"openai-api:gpt-5.4\",\n \"copilot:gpt-5.4\",\n ] {\n provider.set_model(spec).unwrap();\n assert_eq!(provider.model(), spec, \"spec {spec} must be preserved\");\n }\n}\n\n#[test]\nfn named_profile_set_model_keeps_unknown_prefix_with_colon() {\n // A `:`-bearing id whose prefix is neither this profile nor a known\n // built-in profile must be preserved verbatim (it may be a real model id).\n let provider = OpenRouterProvider {\n profile_id: Some(\"tokenrouter\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n provider.set_model(\"some-vendor:weird-model\").unwrap();\n assert_eq!(provider.model(), \"some-vendor:weird-model\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let base = ProviderRouting {\n order: Some(vec![\"fireworks\".to_string()]),\n ..Default::default()\n };\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n// ============================================================================\n// Mid-stream retry rollback (issue #338 gap #3)\n// ============================================================================\n\n/// Fake SSE server: the first connection streams partial output then drops the\n/// socket mid-stream (transport fault); the second connection streams a clean,\n/// complete response.\nfn spawn_midstream_fault_then_complete_server() -> String {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n\n std::thread::spawn(move || {\n // Connection 1: partial output, then abrupt close (no [DONE]).\n {\n let (mut stream, _) = listener.accept().expect(\"accept first request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"partial answer that must not duplicate\\\"}}]}\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n{:x}\\r\\n{}\\r\\n\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write partial response\");\n stream.flush().expect(\"flush partial response\");\n // Drop without terminating the chunked encoding: the client sees\n // an unexpected EOF mid-stream (transient transport fault).\n drop(stream);\n }\n\n // Connection 2 (the retry): clean complete response.\n {\n let (mut stream, _) = listener.accept().expect(\"accept retry request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = concat!(\n \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"final answer\\\"},\\\"finish_reason\\\":\\\"stop\\\"}]}\\n\\n\",\n \"data: [DONE]\\n\\n\",\n );\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write retry response\");\n }\n });\n\n format!(\"http://{addr}/v1\")\n}\n\n/// Regression for issue #338 gap #3: a transient transport fault that hits\n/// mid-stream, after partial output has already been emitted, must surface a\n/// `RetryRollback` before the replayed response so consumers can discard the\n/// partial attempt instead of rendering duplicated output.\n#[test]\nfn midstream_transport_fault_emits_retry_rollback_before_replay() {\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n\n rt.block_on(async {\n let api_base = spawn_midstream_fault_then_complete_server();\n let client = reqwest::Client::new();\n let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<StreamEvent>>(64);\n\n let request = serde_json::json!({\n \"model\": \"test-model\",\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"stream\": true,\n });\n\n super::openrouter_sse_stream::run_stream_with_retries(\n client,\n api_base,\n ProviderAuth::None {\n label: \"test\".to_string(),\n },\n false,\n request,\n tx,\n Arc::new(Mutex::new(None)),\n \"test-model\".to_string(),\n )\n .await;\n\n let mut events = Vec::new();\n while let Some(item) = rx.recv().await {\n events.push(item);\n }\n\n let mut saw_partial = false;\n let mut rollback_after_partial = false;\n let mut final_after_rollback = false;\n let mut duplicate_partial_without_rollback = false;\n for item in &events {\n let Ok(event) = item else {\n panic!(\"stream surfaced an error instead of retrying: {item:?}\");\n };\n match event {\n StreamEvent::TextDelta(text) => {\n if text.contains(\"partial answer\") {\n if saw_partial && !rollback_after_partial {\n duplicate_partial_without_rollback = true;\n }\n saw_partial = true;\n }\n if text.contains(\"final answer\") {\n assert!(\n rollback_after_partial,\n \"replayed response arrived without a RetryRollback after partial output\"\n );\n final_after_rollback = true;\n }\n }\n StreamEvent::RetryRollback { .. } => {\n assert!(\n saw_partial,\n \"RetryRollback must only be emitted after partial output was streamed\"\n );\n rollback_after_partial = true;\n }\n _ => {}\n }\n }\n\n assert!(saw_partial, \"first attempt's partial output never arrived\");\n assert!(\n rollback_after_partial,\n \"no RetryRollback emitted for the mid-stream fault\"\n );\n assert!(\n final_after_rollback,\n \"retry never delivered the complete response\"\n );\n assert!(\n !duplicate_partial_without_rollback,\n \"partial output duplicated without an interleaved rollback\"\n );\n });\n}\n\n/// Issue #352: reasoning effort must follow the *model family*, not just the\n/// dedicated `deepseek` profile id. A custom compat endpoint (named profile or\n/// generic openai-compatible) serving a DeepSeek model supports `/effort`.\n#[test]\nfn compat_profile_serving_deepseek_model_supports_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n // Non-DeepSeek model on a custom endpoint: no effort support.\n provider.set_model(\"some-random-model\").unwrap();\n assert!(provider.available_efforts().is_empty());\n assert!(provider.set_reasoning_effort(\"high\").is_err());\n assert_eq!(provider.reasoning_effort(), None);\n\n // DeepSeek-family model: DeepSeek-style efforts become available.\n provider.set_model(\"deepseek-v4-flash\").unwrap();\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"deepseek model on compat endpoint accepts effort\");\n assert_eq!(provider.reasoning_effort(), Some(\"high\".to_string()));\n}\n\n/// Issue #352: named-profile config can override effort support explicitly in\n/// both directions.\n#[test]\nfn named_profile_supports_reasoning_effort_config_override() {\n let force_on = OpenRouterProvider {\n reasoning_effort_support: Some(true),\n ..make_custom_compatible_provider()\n };\n force_on.set_model(\"not-a-deepseek-model\").unwrap();\n assert_eq!(\n force_on.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n force_on\n .set_reasoning_effort(\"medium\")\n .expect(\"explicit supports_reasoning_effort=true enables effort\");\n assert_eq!(force_on.reasoning_effort(), Some(\"medium\".to_string()));\n\n let force_off = OpenRouterProvider {\n reasoning_effort_support: Some(false),\n ..make_custom_compatible_provider()\n };\n force_off.set_model(\"deepseek-v4-flash\").unwrap();\n assert!(force_off.available_efforts().is_empty());\n assert!(\n force_off.set_reasoning_effort(\"high\").is_err(),\n \"explicit supports_reasoning_effort=false suppresses model auto-detection\"\n );\n}\n\n/// Issue #352: named profiles construct with the user's configured\n/// `openai_reasoning_effort` when the profile supports effort, instead of\n/// silently ignoring the config.\n#[test]\nfn named_profile_construction_reads_openai_reasoning_effort_config() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"deepseek-v4\".to_string()),\n supports_reasoning_effort: Some(true),\n ..Default::default()\n };\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n // The config default is only applied when openai_reasoning_effort is set;\n // with no config value the provider starts with no effort but still\n // supports setting one.\n let initial = provider.reasoning_effort();\n let configured = crate::config::config()\n .provider\n .openai_reasoning_effort\n .clone();\n match configured {\n Some(_) => assert!(initial.is_some(), \"configured effort must be honored\"),\n None => assert_eq!(initial, None),\n }\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"explicitly-enabled profile accepts effort\");\n}\n"}, "6266a379cc754ed5662dd4337e9403b907ff8d1e": {"t": 1785757206.6000113, "n": 96942, "h": "fe090b5f14861580", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"not supported\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let base = ProviderRouting {\n order: Some(vec![\"fireworks\".to_string()]),\n ..Default::default()\n };\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n// ============================================================================\n// Mid-stream retry rollback (issue #338 gap #3)\n// ============================================================================\n\n/// Fake SSE server: the first connection streams partial output then drops the\n/// socket mid-stream (transport fault); the second connection streams a clean,\n/// complete response.\nfn spawn_midstream_fault_then_complete_server() -> String {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n\n std::thread::spawn(move || {\n // Connection 1: partial output, then abrupt close (no [DONE]).\n {\n let (mut stream, _) = listener.accept().expect(\"accept first request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"partial answer that must not duplicate\\\"}}]}\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n{:x}\\r\\n{}\\r\\n\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write partial response\");\n stream.flush().expect(\"flush partial response\");\n // Drop without terminating the chunked encoding: the client sees\n // an unexpected EOF mid-stream (transient transport fault).\n drop(stream);\n }\n\n // Connection 2 (the retry): clean complete response.\n {\n let (mut stream, _) = listener.accept().expect(\"accept retry request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = concat!(\n \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"final answer\\\"},\\\"finish_reason\\\":\\\"stop\\\"}]}\\n\\n\",\n \"data: [DONE]\\n\\n\",\n );\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write retry response\");\n }\n });\n\n format!(\"http://{addr}/v1\")\n}\n\n/// Regression for issue #338 gap #3: a transient transport fault that hits\n/// mid-stream, after partial output has already been emitted, must surface a\n/// `RetryRollback` before the replayed response so consumers can discard the\n/// partial attempt instead of rendering duplicated output.\n#[test]\nfn midstream_transport_fault_emits_retry_rollback_before_replay() {\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n\n rt.block_on(async {\n let api_base = spawn_midstream_fault_then_complete_server();\n let client = reqwest::Client::new();\n let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<StreamEvent>>(64);\n\n let request = serde_json::json!({\n \"model\": \"test-model\",\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"stream\": true,\n });\n\n super::openrouter_sse_stream::run_stream_with_retries(\n client,\n api_base,\n ProviderAuth::None {\n label: \"test\".to_string(),\n },\n false,\n request,\n tx,\n Arc::new(Mutex::new(None)),\n \"test-model\".to_string(),\n )\n .await;\n\n let mut events = Vec::new();\n while let Some(item) = rx.recv().await {\n events.push(item);\n }\n\n let mut saw_partial = false;\n let mut rollback_after_partial = false;\n let mut final_after_rollback = false;\n let mut duplicate_partial_without_rollback = false;\n for item in &events {\n let Ok(event) = item else {\n panic!(\"stream surfaced an error instead of retrying: {item:?}\");\n };\n match event {\n StreamEvent::TextDelta(text) => {\n if text.contains(\"partial answer\") {\n if saw_partial && !rollback_after_partial {\n duplicate_partial_without_rollback = true;\n }\n saw_partial = true;\n }\n if text.contains(\"final answer\") {\n assert!(\n rollback_after_partial,\n \"replayed response arrived without a RetryRollback after partial output\"\n );\n final_after_rollback = true;\n }\n }\n StreamEvent::RetryRollback { .. } => {\n assert!(\n saw_partial,\n \"RetryRollback must only be emitted after partial output was streamed\"\n );\n rollback_after_partial = true;\n }\n _ => {}\n }\n }\n\n assert!(saw_partial, \"first attempt's partial output never arrived\");\n assert!(\n rollback_after_partial,\n \"no RetryRollback emitted for the mid-stream fault\"\n );\n assert!(\n final_after_rollback,\n \"retry never delivered the complete response\"\n );\n assert!(\n !duplicate_partial_without_rollback,\n \"partial output duplicated without an interleaved rollback\"\n );\n });\n}\n\n/// Issue #352: reasoning effort must follow the *model family*, not just the\n/// dedicated `deepseek` profile id. A custom compat endpoint (named profile or\n/// generic openai-compatible) serving a DeepSeek model supports `/effort`.\n#[test]\nfn compat_profile_serving_deepseek_model_supports_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n // Non-DeepSeek model on a custom endpoint: no effort support.\n provider.set_model(\"some-random-model\").unwrap();\n assert!(provider.available_efforts().is_empty());\n assert!(provider.set_reasoning_effort(\"high\").is_err());\n assert_eq!(provider.reasoning_effort(), None);\n\n // DeepSeek-family model: DeepSeek-style efforts become available.\n provider.set_model(\"deepseek-v4-flash\").unwrap();\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"deepseek model on compat endpoint accepts effort\");\n assert_eq!(provider.reasoning_effort(), Some(\"high\".to_string()));\n}\n\n/// Issue #352: named-profile config can override effort support explicitly in\n/// both directions.\n#[test]\nfn named_profile_supports_reasoning_effort_config_override() {\n let force_on = OpenRouterProvider {\n reasoning_effort_support: Some(true),\n ..make_custom_compatible_provider()\n };\n force_on.set_model(\"not-a-deepseek-model\").unwrap();\n assert_eq!(\n force_on.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n force_on\n .set_reasoning_effort(\"medium\")\n .expect(\"explicit supports_reasoning_effort=true enables effort\");\n assert_eq!(force_on.reasoning_effort(), Some(\"medium\".to_string()));\n\n let force_off = OpenRouterProvider {\n reasoning_effort_support: Some(false),\n ..make_custom_compatible_provider()\n };\n force_off.set_model(\"deepseek-v4-flash\").unwrap();\n assert!(force_off.available_efforts().is_empty());\n assert!(\n force_off.set_reasoning_effort(\"high\").is_err(),\n \"explicit supports_reasoning_effort=false suppresses model auto-detection\"\n );\n}\n\n/// Issue #352: named profiles construct with the user's configured\n/// `openai_reasoning_effort` when the profile supports effort, instead of\n/// silently ignoring the config.\n#[test]\nfn named_profile_construction_reads_openai_reasoning_effort_config() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"deepseek-v4\".to_string()),\n supports_reasoning_effort: Some(true),\n ..Default::default()\n };\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n // The config default is only applied when openai_reasoning_effort is set;\n // with no config value the provider starts with no effort but still\n // supports setting one.\n let initial = provider.reasoning_effort();\n let configured = crate::config::config()\n .provider\n .openai_reasoning_effort\n .clone();\n match configured {\n Some(_) => assert!(initial.is_some(), \"configured effort must be honored\"),\n None => assert_eq!(initial, None),\n }\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"explicitly-enabled profile accepts effort\");\n}\n"}, "54103f849c1fc52b98ab06e27ad493c46ac380fc": {"t": 1785757208.913509, "n": 96922, "h": "1fe231846f22efc9", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"not supported\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n reasoning_effort_support: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n// ============================================================================\n// Mid-stream retry rollback (issue #338 gap #3)\n// ============================================================================\n\n/// Fake SSE server: the first connection streams partial output then drops the\n/// socket mid-stream (transport fault); the second connection streams a clean,\n/// complete response.\nfn spawn_midstream_fault_then_complete_server() -> String {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n\n std::thread::spawn(move || {\n // Connection 1: partial output, then abrupt close (no [DONE]).\n {\n let (mut stream, _) = listener.accept().expect(\"accept first request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"partial answer that must not duplicate\\\"}}]}\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n{:x}\\r\\n{}\\r\\n\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write partial response\");\n stream.flush().expect(\"flush partial response\");\n // Drop without terminating the chunked encoding: the client sees\n // an unexpected EOF mid-stream (transient transport fault).\n drop(stream);\n }\n\n // Connection 2 (the retry): clean complete response.\n {\n let (mut stream, _) = listener.accept().expect(\"accept retry request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = concat!(\n \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"final answer\\\"},\\\"finish_reason\\\":\\\"stop\\\"}]}\\n\\n\",\n \"data: [DONE]\\n\\n\",\n );\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write retry response\");\n }\n });\n\n format!(\"http://{addr}/v1\")\n}\n\n/// Regression for issue #338 gap #3: a transient transport fault that hits\n/// mid-stream, after partial output has already been emitted, must surface a\n/// `RetryRollback` before the replayed response so consumers can discard the\n/// partial attempt instead of rendering duplicated output.\n#[test]\nfn midstream_transport_fault_emits_retry_rollback_before_replay() {\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n\n rt.block_on(async {\n let api_base = spawn_midstream_fault_then_complete_server();\n let client = reqwest::Client::new();\n let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<StreamEvent>>(64);\n\n let request = serde_json::json!({\n \"model\": \"test-model\",\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"stream\": true,\n });\n\n super::openrouter_sse_stream::run_stream_with_retries(\n client,\n api_base,\n ProviderAuth::None {\n label: \"test\".to_string(),\n },\n false,\n request,\n tx,\n Arc::new(Mutex::new(None)),\n \"test-model\".to_string(),\n )\n .await;\n\n let mut events = Vec::new();\n while let Some(item) = rx.recv().await {\n events.push(item);\n }\n\n let mut saw_partial = false;\n let mut rollback_after_partial = false;\n let mut final_after_rollback = false;\n let mut duplicate_partial_without_rollback = false;\n for item in &events {\n let Ok(event) = item else {\n panic!(\"stream surfaced an error instead of retrying: {item:?}\");\n };\n match event {\n StreamEvent::TextDelta(text) => {\n if text.contains(\"partial answer\") {\n if saw_partial && !rollback_after_partial {\n duplicate_partial_without_rollback = true;\n }\n saw_partial = true;\n }\n if text.contains(\"final answer\") {\n assert!(\n rollback_after_partial,\n \"replayed response arrived without a RetryRollback after partial output\"\n );\n final_after_rollback = true;\n }\n }\n StreamEvent::RetryRollback { .. } => {\n assert!(\n saw_partial,\n \"RetryRollback must only be emitted after partial output was streamed\"\n );\n rollback_after_partial = true;\n }\n _ => {}\n }\n }\n\n assert!(saw_partial, \"first attempt's partial output never arrived\");\n assert!(\n rollback_after_partial,\n \"no RetryRollback emitted for the mid-stream fault\"\n );\n assert!(\n final_after_rollback,\n \"retry never delivered the complete response\"\n );\n assert!(\n !duplicate_partial_without_rollback,\n \"partial output duplicated without an interleaved rollback\"\n );\n });\n}\n\n/// Issue #352: reasoning effort must follow the *model family*, not just the\n/// dedicated `deepseek` profile id. A custom compat endpoint (named profile or\n/// generic openai-compatible) serving a DeepSeek model supports `/effort`.\n#[test]\nfn compat_profile_serving_deepseek_model_supports_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n // Non-DeepSeek model on a custom endpoint: no effort support.\n provider.set_model(\"some-random-model\").unwrap();\n assert!(provider.available_efforts().is_empty());\n assert!(provider.set_reasoning_effort(\"high\").is_err());\n assert_eq!(provider.reasoning_effort(), None);\n\n // DeepSeek-family model: DeepSeek-style efforts become available.\n provider.set_model(\"deepseek-v4-flash\").unwrap();\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"deepseek model on compat endpoint accepts effort\");\n assert_eq!(provider.reasoning_effort(), Some(\"high\".to_string()));\n}\n\n/// Issue #352: named-profile config can override effort support explicitly in\n/// both directions.\n#[test]\nfn named_profile_supports_reasoning_effort_config_override() {\n let force_on = OpenRouterProvider {\n reasoning_effort_support: Some(true),\n ..make_custom_compatible_provider()\n };\n force_on.set_model(\"not-a-deepseek-model\").unwrap();\n assert_eq!(\n force_on.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n force_on\n .set_reasoning_effort(\"medium\")\n .expect(\"explicit supports_reasoning_effort=true enables effort\");\n assert_eq!(force_on.reasoning_effort(), Some(\"medium\".to_string()));\n\n let force_off = OpenRouterProvider {\n reasoning_effort_support: Some(false),\n ..make_custom_compatible_provider()\n };\n force_off.set_model(\"deepseek-v4-flash\").unwrap();\n assert!(force_off.available_efforts().is_empty());\n assert!(\n force_off.set_reasoning_effort(\"high\").is_err(),\n \"explicit supports_reasoning_effort=false suppresses model auto-detection\"\n );\n}\n\n/// Issue #352: named profiles construct with the user's configured\n/// `openai_reasoning_effort` when the profile supports effort, instead of\n/// silently ignoring the config.\n#[test]\nfn named_profile_construction_reads_openai_reasoning_effort_config() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"deepseek-v4\".to_string()),\n supports_reasoning_effort: Some(true),\n ..Default::default()\n };\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n // The config default is only applied when openai_reasoning_effort is set;\n // with no config value the provider starts with no effort but still\n // supports setting one.\n let initial = provider.reasoning_effort();\n let configured = crate::config::config()\n .provider\n .openai_reasoning_effort\n .clone();\n match configured {\n Some(_) => assert!(initial.is_some(), \"configured effort must be honored\"),\n None => assert_eq!(initial, None),\n }\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"explicitly-enabled profile accepts effort\");\n}\n"}, "ec2f3f84a3a0bfc892e84d5f7b905d56576f99c4": {"t": 1785757210.5662146, "n": 93194, "h": "e64e898e0d0c1943", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n// ============================================================================\n// Mid-stream retry rollback (issue #338 gap #3)\n// ============================================================================\n\n/// Fake SSE server: the first connection streams partial output then drops the\n/// socket mid-stream (transport fault); the second connection streams a clean,\n/// complete response.\nfn spawn_midstream_fault_then_complete_server() -> String {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n\n std::thread::spawn(move || {\n // Connection 1: partial output, then abrupt close (no [DONE]).\n {\n let (mut stream, _) = listener.accept().expect(\"accept first request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"partial answer that must not duplicate\\\"}}]}\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n{:x}\\r\\n{}\\r\\n\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write partial response\");\n stream.flush().expect(\"flush partial response\");\n // Drop without terminating the chunked encoding: the client sees\n // an unexpected EOF mid-stream (transient transport fault).\n drop(stream);\n }\n\n // Connection 2 (the retry): clean complete response.\n {\n let (mut stream, _) = listener.accept().expect(\"accept retry request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(5)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 65536];\n let _ = stream.read(&mut request);\n let body = concat!(\n \"data: {\\\"choices\\\":[{\\\"delta\\\":{\\\"content\\\":\\\"final answer\\\"},\\\"finish_reason\\\":\\\"stop\\\"}]}\\n\\n\",\n \"data: [DONE]\\n\\n\",\n );\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write retry response\");\n }\n });\n\n format!(\"http://{addr}/v1\")\n}\n\n/// Regression for issue #338 gap #3: a transient transport fault that hits\n/// mid-stream, after partial output has already been emitted, must surface a\n/// `RetryRollback` before the replayed response so consumers can discard the\n/// partial attempt instead of rendering duplicated output.\n#[test]\nfn midstream_transport_fault_emits_retry_rollback_before_replay() {\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n\n rt.block_on(async {\n let api_base = spawn_midstream_fault_then_complete_server();\n let client = reqwest::Client::new();\n let (tx, mut rx) = tokio::sync::mpsc::channel::<anyhow::Result<StreamEvent>>(64);\n\n let request = serde_json::json!({\n \"model\": \"test-model\",\n \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}],\n \"stream\": true,\n });\n\n super::openrouter_sse_stream::run_stream_with_retries(\n client,\n api_base,\n ProviderAuth::None {\n label: \"test\".to_string(),\n },\n false,\n request,\n tx,\n Arc::new(Mutex::new(None)),\n \"test-model\".to_string(),\n )\n .await;\n\n let mut events = Vec::new();\n while let Some(item) = rx.recv().await {\n events.push(item);\n }\n\n let mut saw_partial = false;\n let mut rollback_after_partial = false;\n let mut final_after_rollback = false;\n let mut duplicate_partial_without_rollback = false;\n for item in &events {\n let Ok(event) = item else {\n panic!(\"stream surfaced an error instead of retrying: {item:?}\");\n };\n match event {\n StreamEvent::TextDelta(text) => {\n if text.contains(\"partial answer\") {\n if saw_partial && !rollback_after_partial {\n duplicate_partial_without_rollback = true;\n }\n saw_partial = true;\n }\n if text.contains(\"final answer\") {\n assert!(\n rollback_after_partial,\n \"replayed response arrived without a RetryRollback after partial output\"\n );\n final_after_rollback = true;\n }\n }\n StreamEvent::RetryRollback { .. } => {\n assert!(\n saw_partial,\n \"RetryRollback must only be emitted after partial output was streamed\"\n );\n rollback_after_partial = true;\n }\n _ => {}\n }\n }\n\n assert!(saw_partial, \"first attempt's partial output never arrived\");\n assert!(\n rollback_after_partial,\n \"no RetryRollback emitted for the mid-stream fault\"\n );\n assert!(\n final_after_rollback,\n \"retry never delivered the complete response\"\n );\n assert!(\n !duplicate_partial_without_rollback,\n \"partial output duplicated without an interleaved rollback\"\n );\n });\n}\n"}, "4135660d28a1e79f608d2f5c1277307d97a592ca": {"t": 1785757212.3762245, "n": 87168, "h": "7fcc794ba0bd088f", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n extra_body: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n\n#[test]\nfn resolve_extra_body_returns_none_when_unset() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_parses_env_json_object() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true,\"reasoning_effort\":\"high\"}}\"#,\n );\n let extra =\n OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").expect(\"extra body\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn resolve_extra_body_ignores_invalid_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"not-json\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_env_json() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::set(\"JCODE_OPENAI_EXTRA_BODY\", \"[1,2,3]\");\n assert!(OpenRouterProvider::resolve_extra_body(None, \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn resolve_extra_body_merges_config_and_env_with_env_override() {\n let _lock = ENV_LOCK.lock();\n let config = serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": false},\n \"config_only\": 1,\n });\n let _guard = EnvVarGuard::set(\n \"JCODE_OPENAI_EXTRA_BODY\",\n r#\"{\"chat_template_kwargs\":{\"thinking\":true},\"env_only\":2}\"#,\n );\n let extra = OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\")\n .expect(\"merged extra body\");\n // Env overrides the colliding key.\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"thinking\")),\n Some(&serde_json::json!(true))\n );\n // Non-colliding keys from both sources survive.\n assert_eq!(extra.get(\"config_only\"), Some(&serde_json::json!(1)));\n assert_eq!(extra.get(\"env_only\"), Some(&serde_json::json!(2)));\n}\n\n#[test]\nfn resolve_extra_body_ignores_non_object_config() {\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let config = serde_json::json!(\"not an object\");\n assert!(OpenRouterProvider::resolve_extra_body(Some(&config), \"nonexistent.env\").is_none());\n}\n\n#[test]\nfn named_profile_extra_body_threads_into_provider() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _extra_guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n\n let mut profile = crate::config::NamedProviderConfig {\n base_url: \"https://integrate.api.nvidia.com/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n requires_api_key: Some(false),\n ..Default::default()\n };\n profile.extra_body = Some(serde_json::json!({\n \"chat_template_kwargs\": {\"thinking\": true, \"reasoning_effort\": \"high\"}\n }));\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"my-nim\", &profile)\n .expect(\"build named provider\");\n let extra = provider.extra_body.as_ref().expect(\"extra body present\");\n assert_eq!(\n extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n\n#[test]\nfn named_provider_config_deserializes_nested_extra_body_toml() {\n // Verifies the exact `config.toml` shape documented in the README:\n // a nested `[providers.<name>.extra_body.chat_template_kwargs]` table\n // round-trips into the `serde_json::Value` field correctly.\n let toml_str = r#\"\ntype = \"openai-compatible\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\napi_key_env = \"NVIDIA_API_KEY\"\ndefault_model = \"deepseek-ai/deepseek-v4-flash\"\n\n[extra_body.chat_template_kwargs]\nthinking = true\nreasoning_effort = \"high\"\n\"#;\n let profile: crate::config::NamedProviderConfig =\n toml::from_str(toml_str).expect(\"parse named provider toml\");\n let extra = profile.extra_body.as_ref().expect(\"extra_body present\");\n let kwargs = extra\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.as_object())\n .expect(\"chat_template_kwargs object\");\n assert_eq!(kwargs.get(\"thinking\"), Some(&serde_json::json!(true)));\n assert_eq!(\n kwargs.get(\"reasoning_effort\"),\n Some(&serde_json::json!(\"high\"))\n );\n\n // And the resolver hands it back unchanged when no env override is set.\n let _lock = ENV_LOCK.lock();\n let _guard = EnvVarGuard::remove(\"JCODE_OPENAI_EXTRA_BODY\");\n let resolved =\n OpenRouterProvider::resolve_extra_body(profile.extra_body.as_ref(), \"nonexistent.env\")\n .expect(\"resolved extra body\");\n assert_eq!(\n resolved\n .get(\"chat_template_kwargs\")\n .and_then(|v| v.get(\"reasoning_effort\")),\n Some(&serde_json::json!(\"high\"))\n );\n}\n"}, "1da0416b888cf4fcd005f84415da06b296571770": {"t": 1785757214.1248353, "n": 81559, "h": "40906de6908c8475", "c": "use super::*;\nuse bytes::Bytes;\nuse futures::StreamExt;\nuse jcode_provider_openrouter::stream::OpenRouterStream;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let bytes = Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\",\n );\n let mut stream = OpenRouterStream::new(\n futures::stream::once(async move { Ok::<Bytes, reqwest::Error>(bytes) }),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let chunks = vec![\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\",\n )),\n Ok::<Bytes, reqwest::Error>(Bytes::from_static(\n b\"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\",\n )),\n ];\n let mut stream = OpenRouterStream::new(\n futures::stream::iter(chunks),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n match futures::executor::block_on(stream.next()) {\n Some(Ok(StreamEvent::ThinkingDelta(text))) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n"}, "6c855cc978ecaa86055ca4a334f075de8aa95b3a": {"t": 1785757215.823352, "n": 81352, "h": "ed72c6a86224a362", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n\n#[test]\nfn runtime_display_name_tracks_active_openai_compatible_profile() {\n // Regression for issue #329: switching to a direct OpenAI-compatible\n // profile (NVIDIA NIM) at runtime must surface that profile's display\n // name, not the fixed \"OpenRouter\" aggregator label. The machine-facing\n // `name()` stays \"openrouter\" because billing/routing logic keys off it.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n // Configure both the OpenRouter aggregator and NVIDIA NIM credentials so\n // the slot can host either runtime. Set after the isolate guard, which\n // clears every profile api-key env var.\n let _or_key = EnvVarGuard::set(\"OPENROUTER_API_KEY\", \"or-test-key\");\n let _nim_key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n\n // Switch to a NVIDIA NIM model via the profile-prefixed model request.\n provider\n .set_model(\"nvidia-nim:nvidia/llama-3.1-nemotron-ultra-253b-v1\")\n .expect(\"switch to nvidia-nim profile\");\n\n assert_eq!(\n Provider::name(&provider),\n \"OpenRouter\",\n \"machine-facing name must stay stable for billing/routing\"\n );\n assert_eq!(\n Provider::display_name(&provider),\n \"NVIDIA NIM\",\n \"header/UI display name must reflect the active runtime profile\"\n );\n\n // Switching back to the plain OpenRouter aggregator restores the label.\n provider\n .set_model(\"anthropic/claude-sonnet-4\")\n .expect(\"switch back to openrouter aggregator\");\n assert_eq!(Provider::display_name(&provider), \"OpenRouter\");\n}\n\n#[test]\nfn runtime_display_name_for_profile_runtime_instance() {\n // Direct unit coverage of the per-instance resolver used by\n // `Provider::display_name`.\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"NVIDIA_API_KEY\", \"nim-test-key\");\n\n let nim = OpenRouterProvider::new_openai_compatible_profile_runtime(\n crate::provider_catalog::NVIDIA_NIM_PROFILE,\n )\n .expect(\"build nvidia-nim runtime\");\n assert_eq!(nim.runtime_display_name(), \"NVIDIA NIM\");\n assert_eq!(Provider::name(&nim), \"openrouter\");\n}\n"}, "f8d25110ba82b1bbb00da90712c9df8d7c298c23": {"t": 1785757218.4402516, "n": 78340, "h": "741b98fa38ef0ee4", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n/// Regression for issue #322: the dedicated Kimi coding endpoint\n/// (`https://api.kimi.com/coding/v1`, model `kimi-for-coding`) enables thinking\n/// server-side and rejects any assistant tool-call message that lacks\n/// `reasoning_content` with 400 \"thinking is enabled but reasoning_content is\n/// missing in assistant tool call message\". When an assistant turn produced a\n/// tool call without an accompanying reasoning block (the common case once the\n/// thinking stream is not persisted), the request builder must still attach a\n/// `reasoning_content` field to that assistant message so the endpoint accepts\n/// the request.\n#[test]\nfn kimi_for_coding_tool_call_message_includes_reasoning_content() {\n let _lock = ENV_LOCK.lock();\n let _thinking = EnvVarGuard::remove(\"JCODE_OPENROUTER_THINKING\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n // The dedicated Kimi coding endpoint is a direct OpenAI-compatible\n // profile (no OpenRouter provider routing features).\n profile_id: Some(\"kimi\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n model: Arc::new(RwLock::new(\"kimi-for-coding\".to_string())),\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"list the files\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that emitted a tool call but whose hidden reasoning was\n // not persisted (so there is no Reasoning block to replay).\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::ToolUse {\n id: \"call_1\".to_string(),\n name: \"bash\".to_string(),\n input: serde_json::json!({\"command\": \"ls\"}),\n thought_signature: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::ToolResult {\n tool_use_id: \"call_1\".to_string(),\n content: \"a.txt\\nb.txt\".to_string(),\n is_error: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| {\n msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\")\n && msg.get(\"tool_calls\").is_some()\n })\n .expect(\"request should retain the assistant tool-call turn\");\n\n let reasoning = assistant.get(\"reasoning_content\");\n assert!(\n reasoning.is_some_and(|value| value.as_str().is_some_and(|s| !s.is_empty())),\n \"Kimi coding endpoint requires reasoning_content on assistant tool-call messages (issue #322); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "b8ba276a46e6e517dc1cf83806cca75da00bbf12": {"t": 1785757220.5674767, "n": 74585, "h": "6d55fcc2bb0b7d0c", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n /// Acquire the process-global test env lock.\n ///\n /// This recovers from a poisoned mutex (`into_inner`) instead of\n /// propagating the `PoisonError`. The env guard only protects shared\n /// process env state, so a panic in one test must not cascade into a\n /// flood of unrelated `PoisonError` failures across every other test\n /// that takes this lock.\n fn lock(&self) -> std::sync::MutexGuard<'static, ()> {\n crate::storage::lock_test_env()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_TRANSPORT_STATE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_PROVIDER_FEATURES\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL_CATALOG\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_AUTH_HEADER_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_STATIC_MODELS\"),\n EnvVarGuard::remove(\"JCODE_ACTIVE_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n/// Extract the JSON request body from a captured raw HTTP request.\nfn parse_captured_request_body(request: &str) -> serde_json::Value {\n let body = request\n .split_once(\"\\r\\n\\r\\n\")\n .map(|(_, body)| body)\n .unwrap_or(request);\n serde_json::from_str(body)\n .unwrap_or_else(|err| panic!(\"captured request body should be JSON ({err}): {body}\"))\n}\n\n/// Regression for issue #321: when an assistant turn is interrupted mid-thinking\n/// on a direct OpenAI-compatible provider that does not support reasoning replay\n/// (e.g. DeepSeek), the persisted assistant message contains only a `Reasoning`\n/// block. The request builder must not emit an assistant message that has\n/// neither `content` nor `tool_calls`, otherwise the provider rejects the whole\n/// request with 400 \"Invalid assistant message: content or tool_calls must be\n/// set\" and the session can never recover.\n#[test]\nfn interrupted_reasoning_only_assistant_message_is_not_sent_empty() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n // Assistant turn that was interrupted while only reasoning had streamed,\n // so it carries a Reasoning block but no text or tool calls.\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n for msg in api_messages {\n if msg.get(\"role\").and_then(|v| v.as_str()) != Some(\"assistant\") {\n continue;\n }\n let has_content = msg\n .get(\"content\")\n .map(|v| !v.is_null() && v.as_str().map(|s| !s.is_empty()).unwrap_or(true))\n .unwrap_or(false);\n let has_tool_calls = msg\n .get(\"tool_calls\")\n .and_then(|v| v.as_array())\n .map(|calls| !calls.is_empty())\n .unwrap_or(false);\n assert!(\n has_content || has_tool_calls,\n \"assistant message must carry content or tool_calls (issue #321); got: {msg}\"\n );\n }\n eprintln!(\"DBG321 BODY = {}\", serde_json::to_string_pretty(&body).unwrap());\n}\n\n/// Companion to issue #321: when the provider *does* support reasoning replay\n/// (e.g. a generic OpenRouter-style endpoint with provider features enabled and\n/// thinking on), an interrupted reasoning-only assistant turn should be sent\n/// with both a `reasoning_content` field and a valid (empty) `content`, so the\n/// turn is preserved without violating the \"content or tool_calls\" requirement.\n#[test]\nfn interrupted_reasoning_only_assistant_message_keeps_reasoning_with_content() {\n let _lock = ENV_LOCK.lock();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: None,\n supports_provider_features: true,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n\n let messages = vec![\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"do a thing\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::Assistant,\n content: vec![ContentBlock::Reasoning {\n text: \"thinking about the request\".to_string(),\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"actually do this instead\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n },\n ];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n let body = parse_captured_request_body(&request);\n let api_messages = body\n .get(\"messages\")\n .and_then(|m| m.as_array())\n .expect(\"request should contain messages array\");\n\n let assistant = api_messages\n .iter()\n .find(|msg| msg.get(\"role\").and_then(|v| v.as_str()) == Some(\"assistant\"))\n .expect(\"request should retain the interrupted assistant turn\");\n\n assert!(\n assistant.get(\"reasoning_content\").is_some(),\n \"reasoning-capable provider should keep reasoning_content; got: {assistant}\"\n );\n assert!(\n assistant.get(\"content\").is_some(),\n \"interrupted reasoning-only assistant turn must still carry content (issue #321); got: {assistant}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n/// Regression for issue #304: a `default_provider` pointing at an\n/// `openai-compatible` profile must build requests with the direct\n/// OpenAI-compatible request shape, NOT the OpenRouter request builder, even\n/// when `model_catalog` is left enabled (the default). Using the OpenRouter\n/// builder leaks the `provider` routing object / OpenRouter-only headers and\n/// causes strict third-party gateways to reject the request with\n/// 400 \"Unrecognized chat message\".\n#[test]\nfn default_named_openai_compatible_with_catalog_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features even with catalog enabled\"\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock();\n // Isolate the on-disk config/credential lookup the same way the sibling\n // autodetect tests do, so this test does not read whatever provider\n // profile happens to be configured on the host machine.\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "cb2b256691116e63f223b5d27f9146896f8eba46": {"t": 1785757244.6065383, "n": 63254, "h": "ca2c8966a4757485", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock().unwrap();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n#[test]\nfn default_named_openai_compatible_provider_uses_direct_compatible_request_path() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let jcode_home = temp.path().join(\"jcode-home\");\n let _jcode_home = EnvVarGuard::set(\"JCODE_HOME\", &jcode_home);\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n let _key = EnvVarGuard::set(\"TEST_DEFAULT_COMPAT_KEY\", \"test-key\");\n let (api_base, request_rx) = spawn_single_response_chat_server();\n\n std::fs::create_dir_all(&jcode_home).expect(\"create test config dir\");\n std::fs::write(\n jcode_home.join(\"config.toml\"),\n format!(\n r#\"\n[provider]\ndefault_provider = \"my-gateway\"\n\n[providers.my-gateway]\ntype = \"openai-compatible\"\nbase_url = \"{api_base}\"\napi_key_env = \"TEST_DEFAULT_COMPAT_KEY\"\ndefault_model = \"opaque/model@id\"\nmodel_catalog = false\n\"#\n ),\n )\n .expect(\"write test config\");\n crate::config::invalidate_config_cache();\n\n let provider =\n crate::provider::MultiProvider::new_with_auth_status(crate::auth::AuthStatus::default());\n assert_eq!(\n provider.active_provider(),\n crate::provider::ActiveProvider::OpenRouter\n );\n let openrouter = provider\n .openrouter_provider()\n .expect(\"openrouter execution slot\");\n assert!(\n !openrouter.supports_provider_routing_features(),\n \"named openai-compatible defaults must not use OpenRouter provider routing features\"\n );\n assert_eq!(\n openrouter\n .direct_openai_compatible_route_parts()\n .as_ref()\n .map(|parts| parts.1.as_str()),\n Some(\"openai-compatible:my-gateway\")\n );\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = openrouter\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"opaque/model@id\"\"#),\n \"request should use named profile default model: {request}\"\n );\n assert!(\n !request.contains(r#\"\"provider\":\"#),\n \"direct OpenAI-compatible request must not include OpenRouter provider routing object: {request}\"\n );\n assert!(\n !request.contains(\"HTTP-Referer\") && !request.contains(\"X-Title\"),\n \"direct OpenAI-compatible request must not include OpenRouter-only headers: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "01e2a01671fa084eee65f2da34dee26a7401083e": {"t": 1785757246.1784236, "n": 59956, "h": "a05ba743bbac64f2", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_does_not_advertise_image_input_support() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert!(!provider.supports_image_input());\n}\n\n#[test]\nfn direct_deepseek_profile_omits_image_url_parts() {\n let _lock = ENV_LOCK.lock().unwrap();\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n ..make_custom_compatible_provider()\n };\n let messages = vec![Message {\n role: Role::User,\n content: vec![\n ContentBlock::Text {\n text: \"describe this\".to_string(),\n cache_control: None,\n },\n ContentBlock::Image {\n media_type: \"image/png\".to_string(),\n data: \"aW1hZ2U=\".to_string(),\n },\n ],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n if event.is_err() {\n break;\n }\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n !request.contains(r#\"\"type\":\"image_url\"\"#),\n \"DeepSeek request must not contain unsupported image_url content parts: {request}\"\n );\n assert!(\n request.contains(\"Image omitted\"),\n \"DeepSeek request should preserve a textual placeholder for omitted images: {request}\"\n );\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "289a3e9ced759eb5bb3bf5224a845cad83611ccf": {"t": 1785757247.8468544, "n": 57888, "h": "27967d5bbafef33f", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openrouter\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openrouter\")),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(Some(\"openrouter\")).is_real_openrouter());\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "3355a076ee228ecca6bf6d932c5c211ab7db8b63": {"t": 1785757249.615589, "n": 57524, "h": "465dbec9e06028ec", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn observed_session_provider_pin_sticks_without_fallbacks() {\n // Simulates the KV-cache stickiness contract: after OpenRouter serves a\n // request for this model from a concrete provider (recorded as an\n // observed pin), every subsequent request must route to that exact same\n // provider with fallbacks disabled so the upstream prompt cache stays warm.\n let model = \"anthropic/claude-sonnet-4.6\";\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"anthropic\".to_string()].as_slice()),\n \"observed session provider should be pinned exactly\"\n );\n assert!(\n !routing.allow_fallbacks,\n \"observed session pin must disable fallbacks to preserve the KV cache\"\n );\n}\n\n#[test]\nfn observed_pin_yields_to_explicit_user_routing_order() {\n // If the user explicitly narrowed routing themselves (base order set),\n // their configured order wins over the auto-observed session pin.\n let model = \"anthropic/claude-sonnet-4.6\";\n let mut base = ProviderRouting::default();\n base.order = Some(vec![\"fireworks\".to_string()]);\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(model.to_string())),\n provider_routing: Arc::new(RwLock::new(base)),\n provider_pin: Arc::new(Mutex::new(Some(ProviderPin {\n model: model.to_string(),\n provider: \"anthropic\".to_string(),\n source: PinSource::Observed,\n allow_fallbacks: true,\n last_cache_read: None,\n }))),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(model));\n\n assert_eq!(\n routing.order.as_deref(),\n Some([\"fireworks\".to_string()].as_slice()),\n \"explicit user routing order should win over an observed session pin\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "2fa43c0a81c12a39939b3871c1dba7112ab70446": {"t": 1785757252.3886974, "n": 54999, "h": "93f4bce7c4e6b967", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_profile() {\n // Mistral direct profile rejects non-standard reasoning_content/thinking\n // fields with a 422 (issue #261), so it must be flagged strict.\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"mistral\"),\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"MISTRAL\"),\n \"https://example.com/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_detects_mistral_api_base() {\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://api.mistral.ai/v1\"\n ));\n assert!(OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"custom\"),\n \"https://API.MISTRAL.AI/v1\"\n ));\n}\n\n#[test]\nfn strict_openai_schema_endpoint_allows_other_providers() {\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"deepseek\"),\n \"https://api.deepseek.com\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n None,\n \"https://openrouter.ai/api/v1\"\n ));\n assert!(!OpenRouterProvider::strict_openai_schema_endpoint(\n Some(\"openai\"),\n \"https://api.openai.com/v1\"\n ));\n}\n"}, "4aec863e2481c33ce7eb1beed98a935f92c90d08": {"t": 1785757253.9964702, "n": 53733, "h": "3d50809a7ed560e0", "c": "use super::openrouter_sse_stream::OpenRouterStream;\nuse super::*;\nuse std::ffi::OsString;\nuse std::io::{Read, Write};\nuse std::net::TcpListener;\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::time::Duration;\nuse tempfile::TempDir;\n\nstruct SharedEnvLock;\n\nstatic ENV_LOCK: SharedEnvLock = SharedEnvLock;\n\nimpl SharedEnvLock {\n fn lock(&self) -> std::sync::LockResult<std::sync::MutexGuard<'static, ()>> {\n crate::storage::test_env_lock().lock()\n }\n}\n\nstruct EnvVarGuard {\n key: &'static str,\n previous: Option<OsString>,\n}\n\nimpl EnvVarGuard {\n fn set(key: &'static str, value: impl AsRef<std::ffi::OsStr>) -> Self {\n let previous = std::env::var_os(key);\n crate::env::set_var(key, value);\n Self { key, previous }\n }\n\n fn remove(key: &'static str) -> Self {\n let previous = std::env::var_os(key);\n crate::env::remove_var(key);\n Self { key, previous }\n }\n}\n\nimpl Drop for EnvVarGuard {\n fn drop(&mut self) {\n if let Some(previous) = &self.previous {\n crate::env::set_var(self.key, previous);\n } else {\n crate::env::remove_var(self.key);\n }\n }\n}\n\nfn test_config_dir(temp: &TempDir) -> std::path::PathBuf {\n #[cfg(target_os = \"macos\")]\n {\n temp.path().join(\"Library\").join(\"Application Support\")\n }\n #[cfg(target_os = \"windows\")]\n {\n temp.path().join(\"AppData\").join(\"Roaming\")\n }\n #[cfg(all(not(target_os = \"macos\"), not(target_os = \"windows\")))]\n {\n temp.path().to_path_buf()\n }\n}\n\nfn write_test_api_key(temp: &TempDir, env_file: &str, env_key: &str, value: &str) {\n let config_dir = test_config_dir(temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(config_dir.join(env_file), format!(\"{env_key}={value}\\n\"))\n .expect(\"write test api key\");\n}\n\nfn isolate_openrouter_autodetect_env() -> Vec<EnvVarGuard> {\n let mut guards = vec![\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_DYNAMIC_BEARER_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\"),\n EnvVarGuard::remove(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\"),\n EnvVarGuard::remove(\"JCODE_RUNTIME_PROVIDER\"),\n EnvVarGuard::remove(\"JCODE_NAMED_PROVIDER_PROFILE\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_NAME\"),\n EnvVarGuard::remove(\"JCODE_PROVIDER_PROFILE_ACTIVE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_BASE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_API_KEY_NAME\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_ENV_FILE\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_SETUP_URL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_DEFAULT_MODEL\"),\n EnvVarGuard::remove(\"JCODE_OPENAI_COMPAT_LOCAL_ENABLED\"),\n ];\n guards.extend(\n crate::provider_catalog::openai_compatible_profiles()\n .iter()\n .map(|profile| EnvVarGuard::remove(profile.api_key_env)),\n );\n guards\n}\n\n#[test]\nfn test_has_credentials() {\n let _has_creds = OpenRouterProvider::has_credentials();\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_minimal_model_objects() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"glm-51-nvfp4\", \"object\": \"model\", \"created\": null, \"owned_by\": null},\n {\"id\": \"gte-qwen2-7b\", \"object\": \"model\"}\n ]\n }\"#,\n )\n .expect(\"minimal OpenAI-compatible /models response should parse\");\n\n assert_eq!(parsed.len(), 2);\n assert_eq!(parsed[0].id, \"glm-51-nvfp4\");\n assert_eq!(parsed[0].name, \"\");\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_chutes_numeric_pricing() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"object\": \"list\",\n \"data\": [{\n \"id\": \"Qwen/Qwen3-32B-TEE\",\n \"root\": \"Qwen/Qwen3-32B-FP8\",\n \"price\": {\n \"input\": {\"tao\": 0.0002439746644509701, \"usd\": 0.08},\n \"output\": {\"tao\": 0.0007319239933529102, \"usd\": 0.24}\n },\n \"object\": \"model\",\n \"parent\": null,\n \"created\": 1778439139,\n \"pricing\": {\n \"prompt\": 0.08,\n \"completion\": 0.24,\n \"input_cache_read\": 0.04\n },\n \"owned_by\": \"sglang\",\n \"context_length\": 40960,\n \"supported_features\": [\"json_mode\", \"tools\"]\n }]\n }\"#,\n )\n .expect(\"Chutes /models response with numeric pricing should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Qwen/Qwen3-32B-TEE\");\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.08\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.24\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.04\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_together_top_level_array() {\n let parsed = parse_openai_compatible_models_response(\n r#\"[\n {\n \"id\": \"Austism/chronos-hermes-13b\",\n \"object\": \"model\",\n \"created\": 1692896905,\n \"type\": \"chat\",\n \"display_name\": \"Chronos Hermes (13B)\",\n \"context_length\": 2048,\n \"pricing\": {\n \"input\": 0.3,\n \"output\": 0.3,\n \"cached_input\": 0.2\n }\n }\n ]\"#,\n )\n .expect(\"Together /models top-level array should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"Austism/chronos-hermes-13b\");\n assert_eq!(parsed[0].name, \"Chronos Hermes (13B)\");\n assert_eq!(parsed[0].context_length, Some(2048));\n assert_eq!(parsed[0].pricing.prompt.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.completion.as_deref(), Some(\"0.3\"));\n assert_eq!(parsed[0].pricing.input_cache_read.as_deref(), Some(\"0.2\"));\n}\n\n#[test]\nfn openai_compatible_models_endpoint_allows_models_array_with_name_ids() {\n let parsed = parse_openai_compatible_models_response(\n r#\"{\n \"models\": [{\n \"name\": \"accounts/fireworks/models/example\",\n \"displayName\": \"Example Fireworks Model\",\n \"contextLength\": 8192\n }]\n }\"#,\n )\n .expect(\"models array with name-based identifiers should parse\");\n\n assert_eq!(parsed.len(), 1);\n assert_eq!(parsed[0].id, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].name, \"accounts/fireworks/models/example\");\n assert_eq!(parsed[0].context_length, Some(8192));\n}\n\n#[test]\nfn named_openai_compatible_provider_sets_catalog_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"example-model\".to_string()),\n ..Default::default()\n };\n\n let _provider = OpenRouterProvider::new_named_openai_compatible(\"example-compat\", &profile)\n .expect(\"named profile should initialize\");\n\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\").as_deref(),\n Ok(\"example-compat\")\n );\n}\n\n#[test]\nfn named_openai_compatible_provider_exposes_static_models_as_routes() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _key = EnvVarGuard::set(\"TEST_NAMED_COMPAT_KEY\", \"test-key\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"https://llm.example.com/v1\".to_string(),\n api_key_env: Some(\"TEST_NAMED_COMPAT_KEY\".to_string()),\n model_catalog: true,\n default_model: Some(\"glm-51-nvfp4\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"glm-51-nvfp4\".to_string(),\n ..Default::default()\n }],\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"comtegra-test\", &profile)\n .expect(\"named profile should initialize\");\n let routes = provider.model_routes();\n\n assert!(routes.iter().any(|route| {\n route.model == \"glm-51-nvfp4\"\n && route.api_method == \"openai-compatible:comtegra-test\"\n && route.available\n }));\n}\n\n#[test]\nfn direct_openai_compatible_provider_advertises_image_input_support() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n\n let profile = crate::config::NamedProviderConfig {\n base_url: \"http://localhost:1234/v1\".to_string(),\n auth: crate::config::NamedProviderAuth::None,\n default_model: Some(\"local-vision-model\".to_string()),\n ..Default::default()\n };\n\n let provider = OpenRouterProvider::new_named_openai_compatible(\"local-compat\", &profile)\n .expect(\"local named profile should initialize without auth\");\n\n assert!(provider.supports_image_input());\n}\n\n#[test]\nfn minimax_profile_exposes_static_models_before_catalog_refresh() {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::MINIMAX_PROFILE,\n );\n\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2.7-highspeed\"));\n assert!(models.iter().any(|model| model == \"MiniMax-M2\"));\n}\n\n#[test]\nfn cerebras_profile_exposes_live_chat_models_before_catalog_refresh() {\n assert_eq!(\n jcode_provider_metadata::CEREBRAS_PROFILE.default_model,\n Some(\"gpt-oss-120b\")\n );\n\n let models = crate::provider_catalog::openai_compatible_profile_static_models(\n jcode_provider_metadata::CEREBRAS_PROFILE,\n );\n\n assert!(\n !models.iter().any(|model| model == \"qwen-3-coder-480b\"),\n \"old Cerebras default is no longer returned by the live /models catalog\"\n );\n assert!(models.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(models.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n !models\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\")\n );\n assert!(!models.iter().any(|model| model == \"llama3.1-8b\"));\n}\n\n#[test]\nfn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallbacks() {\n let cases = [\n (jcode_provider_metadata::OPENCODE_PROFILE, \"minimax-m2.7\"),\n (jcode_provider_metadata::OPENCODE_GO_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::ZAI_PROFILE, \"glm-4.7\"),\n (\n jcode_provider_metadata::AI302_PROFILE,\n \"qwen3-235b-a22b-instruct-2507\",\n ),\n (jcode_provider_metadata::BASETEN_PROFILE, \"zai-org/GLM-4.7\"),\n (jcode_provider_metadata::CORTECS_PROFILE, \"kimi-k2.5\"),\n (jcode_provider_metadata::KIMI_PROFILE, \"kimi-for-coding\"),\n (jcode_provider_metadata::FIRMWARE_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::HUGGING_FACE_PROFILE,\n \"Qwen/Qwen3-Coder-480B-A35B-Instruct\",\n ),\n (jcode_provider_metadata::MOONSHOT_PROFILE, \"kimi-k2.5\"),\n (\n jcode_provider_metadata::NEBIUS_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (\n jcode_provider_metadata::SCALEWAY_PROFILE,\n \"qwen3-coder-30b-a3b-instruct\",\n ),\n (\n jcode_provider_metadata::STACKIT_PROFILE,\n \"openai/gpt-oss-120b\",\n ),\n (jcode_provider_metadata::PERPLEXITY_PROFILE, \"sonar\"),\n (\n jcode_provider_metadata::DEEPINFRA_PROFILE,\n \"moonshotai/Kimi-K2-Instruct\",\n ),\n (\n jcode_provider_metadata::FIREWORKS_PROFILE,\n \"accounts/fireworks/routers/kimi-k2p5-turbo\",\n ),\n (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, \"mimo-v2.5\"),\n (\n jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE,\n \"qwen3-coder-plus\",\n ),\n ];\n\n for (profile, expected_model) in cases {\n let models = crate::provider_catalog::openai_compatible_profile_static_models(profile);\n assert!(\n models.iter().any(|model| model == expected_model),\n \"{} should expose static fallback model {expected_model}; got {models:?}\",\n profile.id\n );\n }\n}\n\n#[test]\nfn comtegra_profile_uses_endpoint_default_max_tokens() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::remove(\"JCODE_OPENROUTER_MAX_TOKENS\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n None\n );\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"deepseek\")),\n None\n );\n}\n\n#[test]\nfn max_tokens_env_overrides_profile_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _override = EnvVarGuard::set(\"JCODE_OPENROUTER_MAX_TOKENS\", \"4096\");\n\n assert_eq!(\n OpenRouterProvider::configured_max_tokens(Some(\"comtegra\")),\n Some(4096)\n );\n}\n\n#[test]\nfn test_configured_api_base_accepts_https() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\n \"JCODE_OPENROUTER_API_BASE\",\n \"https://api.groq.com/openai/v1/\",\n );\n assert_eq!(configured_api_base(), \"https://api.groq.com/openai/v1\");\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn test_configured_api_base_rejects_insecure_http_remote() {\n let _lock = ENV_LOCK.lock().unwrap();\n let prev = std::env::var(\"JCODE_OPENROUTER_API_BASE\").ok();\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", \"http://example.com/v1\");\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n if let Some(value) = prev {\n crate::env::set_var(\"JCODE_OPENROUTER_API_BASE\", value);\n } else {\n crate::env::remove_var(\"JCODE_OPENROUTER_API_BASE\");\n }\n}\n\n#[test]\nfn autodetects_single_saved_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n\n assert_eq!(configured_api_base(), opencode.api_base);\n assert_eq!(configured_api_key_name(), opencode.api_key_env);\n assert_eq!(configured_env_file_name(), opencode.env_file);\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetects_single_saved_local_openai_compatible_profile() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let lmstudio = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::LMSTUDIO_PROFILE,\n );\n let config_dir = test_config_dir(&temp).join(\"jcode\");\n std::fs::create_dir_all(&config_dir).expect(\"create test config dir\");\n std::fs::write(\n config_dir.join(&lmstudio.env_file),\n format!(\n \"{}=1\\n\",\n crate::provider_catalog::OPENAI_COMPAT_LOCAL_ENABLED_ENV\n ),\n )\n .expect(\"write local config\");\n\n assert_eq!(configured_api_base(), lmstudio.api_base);\n assert_eq!(configured_api_key_name(), lmstudio.api_key_env);\n assert_eq!(configured_env_file_name(), lmstudio.env_file);\n assert!(configured_allow_no_auth());\n assert!(OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn openrouter_transport_state_distinguishes_runtime_identities() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _env = isolate_openrouter_autodetect_env();\n\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::OpenRouterApiKey\n );\n assert!(OpenRouterTransportState::from_current_env(None).accrues_user_api_key_cost());\n assert!(OpenRouterTransportState::from_current_env(None).is_real_openrouter());\n\n crate::env::set_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\", \"direct-api-key\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n crate::env::remove_var(\"JCODE_OPENROUTER_TRANSPORT_STATE\");\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"jcode\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"jcode\")),\n OpenRouterTransportState::JcodeSubscription\n );\n assert!(!OpenRouterTransportState::from_current_env(Some(\"jcode\")).accrues_user_api_key_cost());\n\n crate::env::set_var(\"JCODE_RUNTIME_PROVIDER\", \"openai-compatible\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectApiKey\n );\n\n crate::env::set_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\", \"1\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(Some(\"openai-compatible\")),\n OpenRouterTransportState::DirectNoAuth\n );\n assert!(\n !OpenRouterTransportState::from_current_env(Some(\"openai-compatible\"))\n .accrues_user_api_key_cost()\n );\n\n crate::env::remove_var(\"JCODE_OPENROUTER_ALLOW_NO_AUTH\");\n crate::env::remove_var(\"JCODE_RUNTIME_PROVIDER\");\n crate::env::set_var(\"JCODE_NAMED_PROVIDER_PROFILE\", \"my-gateway\");\n assert_eq!(\n OpenRouterTransportState::from_current_env(None),\n OpenRouterTransportState::DirectApiKey\n );\n}\n\n#[test]\nfn does_not_guess_when_multiple_saved_openai_compatible_profiles_exist() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let opencode = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::OPENCODE_PROFILE,\n );\n let chutes = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::CHUTES_PROFILE,\n );\n write_test_api_key(\n &temp,\n &opencode.env_file,\n &opencode.api_key_env,\n \"test-opencode-key\",\n );\n write_test_api_key(\n &temp,\n &chutes.env_file,\n &chutes.api_key_env,\n \"test-chutes-key\",\n );\n\n assert_eq!(configured_api_base(), DEFAULT_API_BASE);\n assert_eq!(configured_api_key_name(), DEFAULT_API_KEY_NAME);\n assert_eq!(configured_env_file_name(), DEFAULT_ENV_FILE);\n assert!(!OpenRouterProvider::has_credentials());\n}\n\n#[test]\nfn autodetected_profile_seeds_default_model_and_cache_namespace() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _env = isolate_openrouter_autodetect_env();\n\n let zai = crate::provider_catalog::resolve_openai_compatible_profile(\n crate::provider_catalog::ZAI_PROFILE,\n );\n write_test_api_key(&temp, &zai.env_file, &zai.api_key_env, \"test-zai-key\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n assert_eq!(provider.model.blocking_read().clone(), \"glm-4.5\");\n assert_eq!(\n std::env::var(\"JCODE_OPENROUTER_CACHE_NAMESPACE\")\n .ok()\n .as_deref(),\n Some(\"zai\")\n );\n}\n\n#[test]\nfn test_parse_model_spec() {\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@Fireworks!\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Fireworks\");\n assert!(!provider.allow_fallbacks);\n\n let (model, provider) = parse_model_spec(\"moonshotai/kimi-k2.5@moonshot\");\n assert_eq!(model, \"moonshotai/kimi-k2.5\");\n let provider = provider.expect(\"provider\");\n assert_eq!(provider.name, \"Moonshot AI\");\n\n let (model, provider) = parse_model_spec(\"anthropic/claude-sonnet-4@auto\");\n assert_eq!(model, \"anthropic/claude-sonnet-4\");\n assert!(provider.is_none());\n}\n\nfn make_endpoint(name: &str, throughput: f64, uptime: f64, cache: bool, cost: f64) -> EndpointInfo {\n EndpointInfo {\n provider_name: name.to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(format!(\"{:.10}\", cost)),\n completion: None,\n input_cache_read: if cache {\n Some(\"0.00000007\".to_string())\n } else {\n None\n },\n input_cache_write: None,\n },\n context_length: None,\n max_completion_tokens: None,\n quantization: None,\n uptime_last_30m: Some(uptime),\n latency_last_30m: None,\n throughput_last_30m: Some(serde_json::json!({\"p50\": throughput})),\n supports_implicit_caching: Some(cache),\n status: Some(0),\n }\n}\n\nfn make_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: DEFAULT_API_BASE.to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: DEFAULT_API_KEY_NAME.to_string(),\n },\n supports_provider_features: true,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: true,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn make_custom_compatible_provider() -> OpenRouterProvider {\n OpenRouterProvider {\n client: crate::provider::shared_http_client(),\n model: Arc::new(RwLock::new(DEFAULT_MODEL.to_string())),\n reasoning_effort: Arc::new(RwLock::new(None)),\n api_base: \"https://compat.example.test/v1\".to_string(),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"test\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n max_tokens: None,\n static_models: Vec::new(),\n static_context_limits: HashMap::new(),\n send_openrouter_headers: false,\n models_cache: Arc::new(RwLock::new(ModelsCache::default())),\n model_catalog_refresh: Arc::new(Mutex::new(ModelCatalogRefreshState::default())),\n endpoint_refresh: Arc::new(Mutex::new(EndpointRefreshTracker::default())),\n provider_routing: Arc::new(RwLock::new(ProviderRouting::default())),\n provider_pin: Arc::new(Mutex::new(None)),\n endpoints_cache: Arc::new(RwLock::new(HashMap::new())),\n }\n}\n\nfn spawn_single_response_models_server(body: &'static str) -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 8192];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\nfn spawn_single_response_chat_server() -> (String, mpsc::Receiver<String>) {\n let listener = TcpListener::bind(\"127.0.0.1:0\").expect(\"bind fake provider server\");\n let addr = listener.local_addr().expect(\"fake provider addr\");\n let (request_tx, request_rx) = mpsc::channel();\n\n std::thread::spawn(move || {\n let (mut stream, _) = listener.accept().expect(\"accept fake provider request\");\n stream\n .set_read_timeout(Some(Duration::from_secs(2)))\n .expect(\"set read timeout\");\n let mut request = vec![0u8; 16384];\n let n = stream.read(&mut request).unwrap_or(0);\n let request = String::from_utf8_lossy(&request[..n]).into_owned();\n let _ = request_tx.send(request);\n\n let body = \"data: [DONE]\\n\\n\";\n let response = format!(\n \"HTTP/1.1 200 OK\\r\\nContent-Type: text/event-stream\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\\r\\n{}\",\n body.len(),\n body\n );\n stream\n .write_all(response.as_bytes())\n .expect(\"write fake provider response\");\n });\n\n (format!(\"http://{addr}/v1\"), request_rx)\n}\n\n#[test]\nfn direct_deepseek_profile_exposes_max_reasoning_effort() {\n let provider = OpenRouterProvider {\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n ..make_custom_compatible_provider()\n };\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"max\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"max\"));\n}\n\n#[test]\nfn openrouter_profile_exposes_unified_reasoning_effort() {\n let provider = make_provider();\n\n assert_eq!(\n provider.available_efforts(),\n vec![\"none\", \"low\", \"medium\", \"high\", \"xhigh\"]\n );\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"OpenRouter max alias should be accepted\");\n assert_eq!(provider.reasoning_effort().as_deref(), Some(\"xhigh\"));\n}\n\n#[test]\nfn non_deepseek_compatible_profile_does_not_expose_reasoning_effort() {\n let provider = make_custom_compatible_provider();\n\n assert!(provider.available_efforts().is_empty());\n let error = provider\n .set_reasoning_effort(\"max\")\n .expect_err(\"generic compatible profile should not expose DeepSeek effort UX\");\n assert!(\n error.to_string().contains(\"OpenRouter and DeepSeek\"),\n \"unexpected error: {error:?}\"\n );\n}\n\n#[test]\nfn openrouter_chat_request_sends_unified_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"anthropic/claude-sonnet-4.6\".to_string())),\n supports_model_catalog: false,\n ..make_provider()\n };\n provider\n .set_reasoning_effort(\"high\")\n .expect(\"OpenRouter unified reasoning should accept high effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.contains(r#\"\"reasoning\":{\"effort\":\"high\"}\"#),\n \"OpenRouter request should include unified reasoning effort: {request}\"\n );\n assert!(\n !request.contains(r#\"\"thinking\":{\"type\":\"enabled\"}\"#),\n \"unified reasoning should supersede legacy thinking override: {request}\"\n );\n}\n\nfn live_openrouter_models() -> Vec<String> {\n std::env::var(\"JCODE_LIVE_OPENROUTER_MODELS\")\n .or_else(|_| std::env::var(\"JCODE_OPENROUTER_MODEL\"))\n .unwrap_or_else(|_| \"anthropic/claude-sonnet-4.6\".to_string())\n .split([',', '\\n'])\n .map(str::trim)\n .filter(|model| !model.is_empty())\n .map(ToString::to_string)\n .collect()\n}\n\nasync fn collect_openrouter_live_smoke_stream(\n mut stream: EventStream,\n timeout: Duration,\n) -> Result<(usize, usize, bool)> {\n tokio::time::timeout(timeout, async move {\n let mut text_bytes = 0usize;\n let mut thinking_bytes = 0usize;\n let mut saw_message_end = false;\n while let Some(event) = stream.next().await {\n match event? {\n StreamEvent::TextDelta(text) => {\n text_bytes += text.len();\n }\n StreamEvent::ThinkingDelta(text) => {\n thinking_bytes += text.len();\n }\n StreamEvent::MessageEnd { .. } => {\n saw_message_end = true;\n break;\n }\n StreamEvent::Error { message, .. } => anyhow::bail!(message),\n _ => {}\n }\n }\n Ok((text_bytes, thinking_bytes, saw_message_end))\n })\n .await\n .context(\"live OpenRouter smoke timed out\")?\n}\n\n#[tokio::test]\n#[ignore = \"live smoke: requires OPENROUTER_API_KEY or configured OpenRouter credentials\"]\nasync fn live_openrouter_unified_reasoning_smoke() -> Result<()> {\n let _env_lock = ENV_LOCK.lock().unwrap();\n let Some(token) = OpenRouterProvider::get_api_key() else {\n eprintln!(\n \"skipping live OpenRouter smoke: OPENROUTER_API_KEY or configured OpenRouter credentials not found\"\n );\n return Ok(());\n };\n\n let models = live_openrouter_models();\n let effort = std::env::var(\"JCODE_LIVE_OPENROUTER_REASONING_EFFORT\")\n .unwrap_or_else(|_| \"low\".to_string());\n let max_tokens = std::env::var(\"JCODE_LIVE_OPENROUTER_MAX_TOKENS\")\n .ok()\n .and_then(|value| value.trim().parse::<u32>().ok())\n .unwrap_or(1024);\n\n for model in models {\n let provider = OpenRouterProvider {\n auth: ProviderAuth::AuthorizationBearer {\n token: token.clone(),\n label: configured_api_key_name(),\n },\n model: Arc::new(RwLock::new(model.clone())),\n max_tokens: Some(max_tokens),\n ..make_provider()\n };\n provider.set_reasoning_effort(&effort)?;\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"Live smoke test: answer exactly OK.\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let stream = provider\n .complete(\n &messages,\n &[],\n \"You are a live provider smoke test. Keep the answer tiny.\",\n None,\n )\n .await\n .with_context(|| format!(\"starting live OpenRouter stream for {model}\"))?;\n let (text_bytes, thinking_bytes, saw_message_end) =\n collect_openrouter_live_smoke_stream(stream, Duration::from_secs(90))\n .await\n .with_context(|| format!(\"collecting live OpenRouter stream for {model}\"))?;\n\n eprintln!(\n \"live OpenRouter reasoning smoke passed: model={model}, effort={effort}, text_bytes={text_bytes}, thinking_bytes={thinking_bytes}, message_end={saw_message_end}\"\n );\n assert!(\n text_bytes > 0 || thinking_bytes > 0,\n \"live OpenRouter response for {model} contained neither text nor thinking deltas\"\n );\n }\n\n Ok(())\n}\n\n#[test]\nfn direct_deepseek_chat_request_sends_reasoning_effort() {\n let (api_base, request_rx) = spawn_single_response_chat_server();\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"deepseek-v4-pro\".to_string())),\n profile_id: Some(\"deepseek\".to_string()),\n supports_provider_features: false,\n supports_model_catalog: false,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n provider\n .set_reasoning_effort(\"max\")\n .expect(\"DeepSeek direct profile should accept max effort\");\n\n let messages = vec![Message {\n role: Role::User,\n content: vec![ContentBlock::Text {\n text: \"hello\".to_string(),\n cache_control: None,\n }],\n timestamp: None,\n tool_duration_ms: None,\n }];\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(async {\n let mut stream = provider\n .complete(&messages, &[], \"\", None)\n .await\n .expect(\"fake chat request should start\");\n while let Some(event) = stream.next().await {\n event.expect(\"stream event should parse\");\n }\n });\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"POST /v1/chat/completions \"),\n \"unexpected chat request: {request}\"\n );\n assert!(\n request.contains(r#\"\"model\":\"deepseek-v4-pro\"\"#),\n \"request should contain model: {request}\"\n );\n assert!(\n request.contains(r#\"\"reasoning_effort\":\"max\"\"#),\n \"DeepSeek request should include max reasoning effort: {request}\"\n );\n}\n\n#[test]\nfn openai_compatible_model_catalog_refresh_calls_models_endpoint_and_updates_display() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-openai-compatible-flow\",\n );\n let (api_base, request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"live-login-flow-model\", \"object\": \"model\", \"context_length\": 131072}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"OPENAI_COMPAT_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n static_models: vec![\"static-login-flow-fallback\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let fetched = rt\n .block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n assert_eq!(fetched[0].id, \"live-login-flow-model\");\n assert_eq!(provider.context_window(), 131_072);\n\n let request = request_rx\n .recv_timeout(Duration::from_secs(2))\n .expect(\"capture fake provider request\");\n assert!(\n request.starts_with(\"GET /v1/models \"),\n \"unexpected catalog request: {request}\"\n );\n assert!(\n request\n .to_ascii_lowercase()\n .contains(\"authorization: bearer sk-live-catalog\"),\n \"catalog request should include saved API key auth header: {request}\"\n );\n assert!(\n request.to_ascii_lowercase().contains(\"user-agent: jcode/\"),\n \"catalog requests must include a User-Agent because providers like Cerebras reject bare HTTP clients: {request}\"\n );\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"live-login-flow-model\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"static-login-flow-fallback\"),\n \"static fallback/default models should remain visible alongside live catalog models: {display:?}\"\n );\n\n let fresh_provider = OpenRouterProvider {\n api_base: provider.api_base.clone(),\n model: Arc::new(RwLock::new(\"live-login-flow-model\".to_string())),\n auth: provider.auth.clone(),\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: None,\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n assert_eq!(fresh_provider.context_window(), 131_072);\n}\n\n#[test]\nfn built_in_openai_compatible_static_models_drop_out_after_live_catalog() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp home\");\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::set(\n \"JCODE_OPENROUTER_CACHE_NAMESPACE\",\n \"test-cerebras-live-catalog-filters-static-fallback\",\n );\n let (api_base, _request_rx) = spawn_single_response_models_server(\n r#\"{\n \"object\": \"list\",\n \"data\": [\n {\"id\": \"qwen-3-235b-a22b-instruct-2507\", \"object\": \"model\"},\n {\"id\": \"zai-glm-4.7\", \"object\": \"model\"},\n {\"id\": \"gpt-oss-120b\", \"object\": \"model\"}\n ]\n }\"#,\n );\n let provider = OpenRouterProvider {\n api_base,\n auth: ProviderAuth::AuthorizationBearer {\n token: \"sk-live-catalog\".to_string(),\n label: \"CEREBRAS_API_KEY\".to_string(),\n },\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string(), \"zai-glm-4.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n rt.block_on(provider.refresh_models())\n .expect(\"refresh fake model catalog\");\n\n let display = provider.available_models_display();\n assert!(display.iter().any(|model| model == \"gpt-oss-120b\"));\n assert!(display.iter().any(|model| model == \"zai-glm-4.7\"));\n assert!(\n display\n .iter()\n .any(|model| model == \"qwen-3-235b-a22b-instruct-2507\"),\n \"live catalog chat-capable models should remain visible: {display:?}\"\n );\n}\n\n#[test]\nfn direct_openai_compatible_static_models_are_marked_as_fallback_before_live_catalog() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"opencode\".to_string()),\n static_models: vec![\"minimax-m2.7\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n let routes = provider.model_routes();\n let route = routes\n .iter()\n .find(|route| route.model == \"minimax-m2.7\")\n .expect(\"static fallback route should be present before live catalog fetch\");\n\n assert!(\n route\n .detail\n .contains(\"fallback: static provider model list\"),\n \"fallback routes should be clearly labeled in the model picker: {route:?}\"\n );\n}\n\n#[test]\nfn cerebras_live_catalog_models_are_selectable_on_explicit_switch() {\n let provider = OpenRouterProvider {\n supports_provider_features: false,\n supports_model_catalog: true,\n profile_id: Some(\"cerebras\".to_string()),\n static_models: vec![\"gpt-oss-120b\".to_string()],\n send_openrouter_headers: false,\n ..make_custom_compatible_provider()\n };\n\n provider\n .set_model(\"zai-glm-4.7\")\n .expect(\"live Cerebras model should be selectable\");\n assert_eq!(provider.model(), \"zai-glm-4.7\");\n provider\n .set_model(\"gpt-oss-120b\")\n .expect(\"default Cerebras model should remain selectable\");\n assert_eq!(provider.model(), \"gpt-oss-120b\");\n}\n\n#[test]\nfn direct_deepseek_profile_uses_static_1m_context_when_catalog_is_absent() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _base = EnvVarGuard::set(\"JCODE_OPENROUTER_API_BASE\", \"https://api.deepseek.com\");\n let _key_name = EnvVarGuard::set(\"JCODE_OPENROUTER_API_KEY_NAME\", \"DEEPSEEK_API_KEY\");\n let _api_key = EnvVarGuard::set(\"DEEPSEEK_API_KEY\", \"test\");\n let _namespace = EnvVarGuard::set(\"JCODE_OPENROUTER_CACHE_NAMESPACE\", \"deepseek\");\n let _model = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL\", \"deepseek-v4-flash\");\n let _catalog = EnvVarGuard::set(\"JCODE_OPENROUTER_MODEL_CATALOG\", \"0\");\n\n let provider = OpenRouterProvider::new().expect(\"provider\");\n\n assert_eq!(provider.context_window(), 1_000_000);\n}\n\n#[test]\nfn named_openai_compatible_model_context_window_overrides_default() {\n let _lock = ENV_LOCK.lock().unwrap();\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let mut config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key: Some(\"test\".to_string()),\n default_model: Some(\"custom-long-context\".to_string()),\n models: vec![crate::config::NamedProviderModelConfig {\n id: \"custom-long-context\".to_string(),\n context_window: Some(512_000),\n input: Vec::new(),\n }],\n ..Default::default()\n };\n config.model_catalog = false;\n\n let provider =\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config).expect(\"provider\");\n\n assert_eq!(provider.context_window(), 512_000);\n}\n\n#[test]\nfn named_openai_compatible_loads_api_key_from_env_file() {\n let _lock = ENV_LOCK.lock().unwrap();\n let temp = TempDir::new().expect(\"create temp dir\");\n let _xdg = EnvVarGuard::set(\"XDG_CONFIG_HOME\", temp.path());\n let _home = EnvVarGuard::set(\"HOME\", temp.path());\n let _appdata = EnvVarGuard::set(\"APPDATA\", temp.path().join(\"AppData\").join(\"Roaming\"));\n let _namespace = EnvVarGuard::remove(\"JCODE_OPENROUTER_CACHE_NAMESPACE\");\n let _api_key = EnvVarGuard::remove(\"CUSTOM_API_KEY\");\n write_test_api_key(&temp, \"custom.env\", \"CUSTOM_API_KEY\", \"from-env-file\");\n\n let config = crate::config::NamedProviderConfig {\n base_url: \"https://compat.example.test/v1\".to_string(),\n api_key_env: Some(\"CUSTOM_API_KEY\".to_string()),\n env_file: Some(\"custom.env\".to_string()),\n default_model: Some(\"custom-model\".to_string()),\n ..Default::default()\n };\n\n OpenRouterProvider::new_named_openai_compatible(\"custom\", &config)\n .expect(\"provider should load key from env file\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_claude_like_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"claude-opus4.6-thinking\").unwrap();\n\n assert_eq!(provider.model(), \"claude-opus4.6-thinking\");\n}\n\n#[test]\nfn custom_compatible_provider_preserves_at_sign_model_ids() {\n let provider = make_custom_compatible_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"gpt-5.4@OpenAI\");\n}\n\n#[test]\nfn openrouter_provider_normalizes_bare_pinned_model_ids() {\n let provider = make_provider();\n\n provider.set_model(\"gpt-5.4@OpenAI\").unwrap();\n\n assert_eq!(provider.model(), \"openai/gpt-5.4\");\n}\n\n#[test]\nfn test_rank_providers_cache_priority() {\n let endpoints = vec![\n make_endpoint(\"FastCache\", 50.0, 99.0, true, 0.0000002),\n make_endpoint(\"FasterNoCache\", 60.0, 99.0, false, 0.0000001),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"FastCache\"));\n}\n\n#[test]\nfn test_rank_providers_speed_priority_among_cache_capable() {\n let endpoints = vec![\n make_endpoint(\"Fireworks\", 120.0, 99.0, true, 0.0000013),\n make_endpoint(\"Moonshot AI\", 80.0, 99.0, true, 0.0000010),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.first().map(|s| s.as_str()), Some(\"Fireworks\"));\n}\n\n#[test]\nfn test_rank_providers_filters_down_providers() {\n let mut down_ep = make_endpoint(\"DownProvider\", 200.0, 100.0, true, 0.0000001);\n down_ep.status = Some(1); // down\n let endpoints = vec![\n down_ep,\n make_endpoint(\"UpProvider\", 50.0, 99.0, true, 0.0000002),\n ];\n\n let ranked = OpenRouterProvider::rank_providers_from_endpoints(&endpoints);\n assert_eq!(ranked.len(), 1);\n assert_eq!(ranked[0], \"UpProvider\");\n}\n\n#[test]\nfn test_background_refresh_waits_for_soft_ttl() {\n let provider = make_provider();\n\n assert!(!provider.should_background_refresh_model_catalog(\n MODEL_CATALOG_SOFT_REFRESH_SECS.saturating_sub(1)\n ));\n assert!(provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_background_refresh_is_throttled_between_attempts() {\n let provider = make_provider();\n assert!(provider.begin_background_model_catalog_refresh());\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n\n OpenRouterProvider::finish_background_model_catalog_refresh(&provider.model_catalog_refresh);\n\n assert!(!provider.should_background_refresh_model_catalog(MODEL_CATALOG_SOFT_REFRESH_SECS));\n}\n\n#[test]\nfn test_kimi_routing_uses_endpoints_or_fallback() {\n let provider = OpenRouterProvider {\n model: Arc::new(RwLock::new(\"moonshotai/kimi-k2.5\".to_string())),\n ..make_provider()\n };\n\n let rt = tokio::runtime::Builder::new_current_thread()\n .enable_all()\n .build()\n .expect(\"runtime\");\n let routing = rt.block_on(provider.effective_routing(\"moonshotai/kimi-k2.5\"));\n let order = routing.order.expect(\"provider order should be set\");\n // Should have providers - either from endpoint API or Kimi fallback\n assert!(\n !order.is_empty(),\n \"Kimi routing should always produce a provider order\"\n );\n}\n\n#[test]\nfn test_kimi_coding_header_detection_matches_endpoint_and_model() {\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.kimi.com/coding/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://api.z.ai/api/coding/paas/v4\",\n None,\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://example.com/v1\",\n Some(\"kimi-for-coding\"),\n ));\n assert!(should_send_kimi_coding_agent_headers(\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n ));\n assert!(!should_send_kimi_coding_agent_headers(\n \"https://api.openrouter.ai/api/v1\",\n Some(\"anthropic/claude-sonnet-4\"),\n ));\n}\n\n#[test]\nfn test_openrouter_kimi_chat_request_includes_compat_user_agent() {\n let request = apply_kimi_coding_agent_headers(\n Client::new().post(\"https://openrouter.ai/api/v1/chat/completions\"),\n \"https://openrouter.ai/api/v1\",\n Some(\"moonshotai/kimi-k2.5\"),\n )\n .build()\n .expect(\"build request\");\n assert!(\n request\n .headers()\n .get(\"User-Agent\")\n .and_then(|value| value.to_str().ok())\n == Some(KIMI_CODING_USER_AGENT),\n \"Kimi OpenRouter chat request should include compatibility User-Agent\"\n );\n}\n\n#[test]\nfn test_parse_next_event_accepts_compact_sse_data_and_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"kimi-for-coding\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"thinking\\\"}}]}\\n\\n\".to_string();\n\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"thinking\"),\n other => panic!(\"expected ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_parse_next_event_emits_only_incremental_reasoning_content() {\n let mut stream = OpenRouterStream::new(\n futures::stream::empty::<Result<Bytes, reqwest::Error>>(),\n \"moonshotai/kimi-k2.5\".to_string(),\n Arc::new(Mutex::new(None)),\n );\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking\\\"}}]}\\n\\n\".to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \"Thinking\"),\n other => panic!(\"expected first ThinkingDelta, got {:?}\", other),\n }\n\n stream.buffer =\n \"data:{\\\"choices\\\":[{\\\"delta\\\":{\\\"reasoning_content\\\":\\\"Thinking more\\\"}}]}\\n\\n\"\n .to_string();\n match stream.parse_next_event() {\n Some(StreamEvent::ThinkingDelta(text)) => assert_eq!(text, \" more\"),\n other => panic!(\"expected incremental ThinkingDelta, got {:?}\", other),\n }\n}\n\n#[test]\nfn test_endpoint_detail_string() {\n let ep = EndpointInfo {\n provider_name: \"TestProvider\".to_string(),\n tag: None,\n pricing: ModelPricing {\n prompt: Some(\"0.00000045\".to_string()),\n completion: Some(\"0.00000225\".to_string()),\n input_cache_read: Some(\"0.00000007\".to_string()),\n input_cache_write: Some(\"0.00000012\".to_string()),\n },\n context_length: Some(131072),\n max_completion_tokens: Some(8192),\n quantization: Some(\"fp8\".to_string()),\n uptime_last_30m: Some(99.5),\n latency_last_30m: Some(serde_json::json!({\"p50\": 500, \"p75\": 800})),\n throughput_last_30m: Some(serde_json::json!({\"p50\": 42, \"p75\": 55})),\n supports_implicit_caching: Some(true),\n status: Some(0),\n };\n let detail = ep.detail_string();\n assert!(\n detail.contains(\"$0.45/M\"),\n \"should contain price: {}\",\n detail\n );\n assert!(detail.contains(\"100%\"), \"should contain uptime: {}\", detail);\n assert!(\n detail.contains(\"out $2.25/M\"),\n \"should contain output price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache write $0.12/M\"),\n \"should contain cache write price: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache read $0.07/M\"),\n \"should contain cache read price: {}\",\n detail\n );\n assert!(\n detail.contains(\"500ms p50\"),\n \"should contain latency: {}\",\n detail\n );\n assert!(\n detail.contains(\"42tps\"),\n \"should contain throughput: {}\",\n detail\n );\n assert!(\n detail.contains(\"cache on\"),\n \"should contain cache: {}\",\n detail\n );\n assert!(\n detail.contains(\"fp8\"),\n \"should contain quantization: {}\",\n detail\n );\n}\n"}}}} |