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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion rust/src/locale/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 8 additions & 15 deletions rust/src/providers/openrouter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<String> {
Expand Down
10 changes: 10 additions & 0 deletions rust/src/providers/openrouter/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
);
}
4 changes: 3 additions & 1 deletion rust/src/settings/api_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ pub fn get_api_key_providers() -> Vec<ProviderConfigInfo> {
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"),
},
Expand Down
14 changes: 14 additions & 0 deletions rust/src/settings/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down