diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 237f7dab3dc..4b6cd395ea1 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -87,6 +87,10 @@ Custom pricing overlays are exact-match overrides used only where the local spen OpenCode-held OpenAI/Codex OAuth can be reused for **remote Codex account quota** only when the Codex provider's `External OAuth sources` setting is explicitly enabled. Native Codex credentials still take precedence, an explicit `CODEX_HOME` stays isolated, and external credentials remain read-only. This does **not** import ordinary OpenCode sessions into Codex token or spend totals. OpenCode Go's local SQLite reader remains scoped to its own `opencode-go` assistant records; OpenAI API-platform usage is a separate provider. +### OpenRouter keys + +The **API key** field (or `OPENROUTER_API_KEY`) is required and accepts either a regular API key or a Management API key. A Management key entered there also enables account Activity on the official OpenRouter API. The separate **Management API key** field (or `OPENROUTER_MANAGEMENT_API_KEY`) is an optional additional key for account Activity, only needed to use a Management key different from the one in the required field. Filling only the Management field does not select an account for quota or balance; with no primary key, the provider reports the missing-key message instead. + ### z.ai Coding Plan quotas z.ai Coding Plans accept both `TOKENS_LIMIT` and `CREDIT_LIMIT` rows. The shortest known Coding Plan window becomes primary and the longest becomes secondary; `TIME_LIMIT` is the separate MCP lane. When absolute usage/remaining counts are available they determine the used percentage, otherwise the provider percentage is used, always clamped to 0–100%. This behavior is shared by the tray, provider detail, CLI, and other Windows surfaces. diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index fb96ecd4b8b..94f3235e750 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -842,7 +842,7 @@ ProviderAutoResumeAfterQuotaResetHelper = Reopen the exact local Codex or Claude OpenRouterManagementKeyTitle = OpenRouter spend history OpenRouterManagementKeyLabel = Management API key -OpenRouterManagementKeyHelp = Optional. Used only for exact 30-day Activity spend from openrouter.ai. The primary OpenRouter API key remains separate. You can also set OPENROUTER_MANAGEMENT_API_KEY. +OpenRouterManagementKeyHelp = Optional additional key for account Activity. Only needed to use a separate Management API key from the one in the required API key field above. You can also set OPENROUTER_MANAGEMENT_API_KEY. OpenRouterManagementKeyConfigured = Management key configured ClaudeAccountsTitle = Claude Code accounts ClaudeAccountsHint = Close Claude Code before switching, then reopen it. Claude Desktop uses a separate login. diff --git a/rust/src/providers/openrouter/mod.rs b/rust/src/providers/openrouter/mod.rs index 1c15164c823..b9e244db0e2 100755 --- a/rust/src/providers/openrouter/mod.rs +++ b/rust/src/providers/openrouter/mod.rs @@ -33,6 +33,9 @@ const OPENROUTER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3 const OPENROUTER_KEY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1); const OPENROUTER_ACTIVITY_URL: &str = "https://openrouter.ai/api/v1/activity"; const OPENROUTER_MANAGEMENT_ENV: &str = "OPENROUTER_MANAGEMENT_API_KEY"; +/// Shown when no primary key resolves. The optional Management key field never +/// substitutes for the primary key when selecting an account for quota/balance. +const MISSING_API_KEY_MESSAGE: &str = "Enter a regular API key or a Management API key in the API key field, or set OPENROUTER_API_KEY. In Settings, the optional Management API key field does not replace it."; /// Windows Credential Manager target for OpenRouter API token const OPENROUTER_CREDENTIAL_TARGET: &str = "codexbar-openrouter"; @@ -169,21 +172,11 @@ impl OpenRouterProvider { return Ok(key.to_string()); } - match keyring::Entry::new(OPENROUTER_CREDENTIAL_TARGET, "api_token") { - Ok(entry) => match entry.get_password() { - Ok(token) => Ok(token), - Err(_) => std::env::var("OPENROUTER_API_KEY").map_err(|_| { - ProviderError::NotInstalled( - "OpenRouter API key not found. Set in Preferences → Providers or OPENROUTER_API_KEY environment variable.".to_string(), - ) - }), - }, - Err(_) => std::env::var("OPENROUTER_API_KEY").map_err(|_| { - ProviderError::NotInstalled( - "OpenRouter API key not found. Set in Preferences → Providers or OPENROUTER_API_KEY environment variable.".to_string(), - ) - }), - } + keyring::Entry::new(OPENROUTER_CREDENTIAL_TARGET, "api_token") + .ok() + .and_then(|entry| entry.get_password().ok()) + .or_else(|| std::env::var("OPENROUTER_API_KEY").ok()) + .ok_or_else(|| ProviderError::NotInstalled(MISSING_API_KEY_MESSAGE.to_string())) } fn configured_management_key() -> Option { diff --git a/rust/src/providers/openrouter/tests.rs b/rust/src/providers/openrouter/tests.rs index a7728bde20c..3a1db064311 100644 --- a/rust/src/providers/openrouter/tests.rs +++ b/rust/src/providers/openrouter/tests.rs @@ -387,3 +387,13 @@ fn parsed_key_wire_fields_decode() { assert_eq!(parsed.data.limit_remaining, Some(12.5)); assert_eq!(parsed.data.limit_reset.as_deref(), Some("monthly")); } + +// Upstream 0.67.0 `OpenRouterSettingsError.missingToken` copy: a Management key +// is a valid primary key, and the optional Management field never replaces it. +#[test] +fn missing_key_message_explains_primary_and_management_fields() { + assert_eq!( + MISSING_API_KEY_MESSAGE, + "Enter a regular API key or a Management API key in the API key field, or set OPENROUTER_API_KEY. In Settings, the optional Management API key field does not replace it." + ); +} diff --git a/rust/src/settings/api_keys.rs b/rust/src/settings/api_keys.rs index c2adf3565cf..7ee76399359 100644 --- a/rust/src/settings/api_keys.rs +++ b/rust/src/settings/api_keys.rs @@ -264,7 +264,9 @@ pub fn get_api_key_providers() -> Vec { name: "OpenRouter", requires_api_key: true, api_key_env_var: Some("OPENROUTER_API_KEY"), - api_key_help: Some("Get your API key from openrouter.ai/settings/keys"), + api_key_help: Some( + "Required. Enter a regular API key or a Management API key here. Management keys also enable account Activity on the official OpenRouter API.", + ), config_file_path: None, dashboard_url: Some("https://openrouter.ai/settings/credits"), }, diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index a6db4aa05b2..d78b52b36fa 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -548,6 +548,20 @@ fn test_api_key_provider_catalog_includes_token_providers() { } } +#[test] +fn openrouter_api_key_help_explains_management_keys() { + let info = get_api_key_providers() + .into_iter() + .find(|info| info.id == ProviderId::OpenRouter) + .expect("OpenRouter api key metadata"); + assert_eq!( + info.api_key_help, + Some( + "Required. Enter a regular API key or a Management API key here. Management keys also enable account Activity on the official OpenRouter API." + ) + ); +} + #[test] fn test_t3_chat_is_cookie_configured_not_api_key_configured() { let providers = get_api_key_providers();