fix(usage): treat usage_script credentials as explicit overrides (#4654)

* fix(usage): treat usage_script credentials as explicit overrides

* fix: treat usage script credentials as explicit overrides
This commit is contained in:
yyhhyyyyyy
2026-06-28 17:37:58 +08:00
committed by GitHub
parent eb847642d0
commit d1f6c74be8
6 changed files with 711 additions and 17 deletions
+380 -1
View File
@@ -101,9 +101,9 @@ mod tests {
use crate::claude_desktop_config::PROFILE_ID;
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
use crate::database::Database;
use crate::provider::ProviderMeta;
#[cfg(any(target_os = "macos", windows))]
use crate::provider::{ClaudeDesktopMode, ClaudeDesktopModelRoute};
use crate::provider::{ProviderMeta, UsageScript};
use crate::proxy::types::ProxyConfig;
use crate::store::AppState;
use serde_json::json;
@@ -226,6 +226,68 @@ mod tests {
result
}
fn codex_settings(base_url: &str, api_key: &str) -> Value {
json!({
"auth": {
"OPENAI_API_KEY": api_key
},
"config": format!(
"model_provider = \"custom\"\n\
[model_providers.custom]\n\
name = \"custom\"\n\
base_url = \"{base_url}\"\n\
wire_api = \"chat\"\n"
)
})
}
fn usage_script_with_credentials(
api_key: Option<&str>,
base_url: Option<&str>,
template_type: Option<&str>,
) -> UsageScript {
UsageScript {
enabled: true,
language: "javascript".to_string(),
code: "return { remaining: 1, unit: 'USD' };".to_string(),
timeout: Some(10),
api_key: api_key.map(str::to_string),
base_url: base_url.map(str::to_string),
access_token: None,
user_id: None,
template_type: template_type.map(str::to_string),
auto_query_interval: None,
coding_plan_provider: None,
access_key_id: Some("ak-test".to_string()),
secret_access_key: Some("sk-test".to_string()),
}
}
fn codex_provider_with_usage(
id: &str,
base_url: &str,
api_key: &str,
usage_api_key: Option<&str>,
usage_base_url: Option<&str>,
template_type: Option<&str>,
) -> Provider {
let mut provider = Provider::with_id(
id.to_string(),
format!("Provider {id}"),
codex_settings(base_url, api_key),
None,
);
provider.meta = Some(ProviderMeta {
usage_script: Some(usage_script_with_credentials(
usage_api_key,
usage_base_url,
template_type,
)),
..Default::default()
});
provider
}
fn openclaw_provider(id: &str) -> Provider {
Provider {
id: id.to_string(),
@@ -326,6 +388,255 @@ mod tests {
})
}
#[test]
#[serial]
fn add_clears_usage_credentials_that_match_provider_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-a",
"https://api.a.example/v1/",
"sk-a",
Some(" sk-a "),
Some(" https://api.a.example/v1/ "),
None,
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-a", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
});
}
#[test]
#[serial]
fn update_preserves_usage_credentials_that_only_match_previous_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-usage-old",
"https://api.a.example/v1/",
"sk-a",
Some("sk-a"),
Some("https://api.a.example/v1/"),
None,
);
state
.db
.save_provider(AppType::Codex.as_str(), &provider)
.expect("seed provider with explicit usage credentials");
let mut updated = provider.clone();
updated.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
ProviderService::update(state, AppType::Codex, None, updated)
.expect("update provider main credentials");
let saved = state
.db
.get_provider_by_id("codex-usage-old", AppType::Codex.as_str())
.expect("query updated provider")
.expect("updated provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-a"));
assert_eq!(
script.base_url.as_deref(),
Some("https://api.a.example/v1/")
);
assert_eq!(
saved.resolve_usage_credentials(&AppType::Codex),
("https://api.b.example/v1".to_string(), "sk-b".to_string())
);
});
}
#[test]
#[serial]
fn copied_provider_uses_edited_credentials_after_add_clears_mirrored_usage_credentials() {
with_test_home(|state, _| {
let copied_provider = codex_provider_with_usage(
"codex-copy",
"https://api.a.example/v1/",
"sk-a",
Some("sk-a"),
Some("https://api.a.example/v1/"),
None,
);
ProviderService::add(state, AppType::Codex, copied_provider, false)
.expect("add copied provider");
let saved_after_add = state
.db
.get_provider_by_id("codex-copy", AppType::Codex.as_str())
.expect("query copied provider")
.expect("copied provider should exist");
let script_after_add = saved_after_add
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script_after_add.api_key, None);
assert_eq!(script_after_add.base_url, None);
let mut edited_provider = saved_after_add.clone();
edited_provider.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
ProviderService::update(state, AppType::Codex, None, edited_provider)
.expect("edit copied provider credentials");
let saved_after_update = state
.db
.get_provider_by_id("codex-copy", AppType::Codex.as_str())
.expect("query edited provider")
.expect("edited provider should exist");
let script_after_update = saved_after_update
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script_after_update.api_key, None);
assert_eq!(script_after_update.base_url, None);
assert_eq!(
saved_after_update.resolve_usage_credentials(&AppType::Codex),
("https://api.b.example/v1".to_string(), "sk-b".to_string())
);
});
}
#[test]
#[serial]
fn update_clears_usage_credentials_that_match_current_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-current",
"https://api.a.example/v1",
"sk-a",
Some("sk-usage"),
Some("https://usage.example/api"),
None,
);
state
.db
.save_provider(AppType::Codex.as_str(), &provider)
.expect("seed provider with distinct usage credentials");
let mut updated = provider.clone();
updated.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
updated.meta = Some(ProviderMeta {
usage_script: Some(usage_script_with_credentials(
Some(" sk-b "),
Some(" https://api.b.example/v1/ "),
None,
)),
..Default::default()
});
ProviderService::update(state, AppType::Codex, None, updated)
.expect("update provider with redundant usage credentials");
let saved = state
.db
.get_provider_by_id("codex-current", AppType::Codex.as_str())
.expect("query updated provider")
.expect("updated provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
});
}
#[test]
#[serial]
fn add_preserves_distinct_usage_credentials() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-distinct",
"https://api.main.example/v1",
"sk-main",
Some("sk-usage"),
Some("https://usage.example/api"),
None,
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-distinct", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-usage"));
assert_eq!(
script.base_url.as_deref(),
Some("https://usage.example/api")
);
});
}
#[test]
#[serial]
fn add_does_not_clear_token_plan_credentials() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-token-plan",
"https://api.plan.example/v1",
"sk-plan",
Some("sk-plan"),
Some("https://api.plan.example/v1"),
Some("token_plan"),
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-token-plan", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-plan"));
assert_eq!(
script.base_url.as_deref(),
Some("https://api.plan.example/v1")
);
assert_eq!(script.access_key_id.as_deref(), Some("ak-test"));
assert_eq!(script.secret_access_key.as_deref(), Some("sk-test"));
});
}
#[test]
fn validate_provider_settings_rejects_missing_auth() {
let provider = Provider::with_id(
@@ -1196,6 +1507,72 @@ impl ProviderService {
.live_config_managed = Some(managed);
}
fn normalize_usage_script_credential_overrides(app_type: &AppType, provider: &mut Provider) {
let current_credentials = provider.resolve_usage_credentials(app_type);
let Some(usage_script) = provider
.meta
.as_mut()
.and_then(|meta| meta.usage_script.as_mut())
else {
return;
};
if usage_script.template_type.as_deref() == Some("token_plan") {
return;
}
if usage_script.api_key.as_deref().is_some_and(|api_key| {
Self::should_clear_usage_api_key_override(api_key, &current_credentials)
}) {
usage_script.api_key = None;
}
if usage_script.base_url.as_deref().is_some_and(|base_url| {
Self::should_clear_usage_base_url_override(base_url, &current_credentials)
}) {
usage_script.base_url = None;
}
}
fn should_clear_usage_api_key_override(
script_api_key: &str,
current_credentials: &(String, String),
) -> bool {
let candidate = script_api_key.trim();
if candidate.is_empty() {
return true;
}
let matches_provider_key = |api_key: &str| {
let api_key = api_key.trim();
!api_key.is_empty() && api_key == candidate
};
matches_provider_key(&current_credentials.1)
}
fn should_clear_usage_base_url_override(
script_base_url: &str,
current_credentials: &(String, String),
) -> bool {
let candidate = Self::normalize_usage_base_url_for_compare(script_base_url);
if candidate.is_empty() {
return true;
}
let matches_provider_base_url = |base_url: &str| {
let base_url = Self::normalize_usage_base_url_for_compare(base_url);
!base_url.is_empty() && base_url == candidate
};
matches_provider_base_url(&current_credentials.0)
}
fn normalize_usage_base_url_for_compare(base_url: &str) -> String {
base_url.trim().trim_end_matches('/').to_string()
}
/// List all providers for an app type
pub fn list(
state: &AppState,
@@ -1232,6 +1609,7 @@ impl ProviderService {
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?;
Self::normalize_usage_script_credential_overrides(&app_type, &mut provider);
if app_type.is_additive_mode() {
Self::set_provider_live_config_managed(&mut provider, add_to_live);
}
@@ -1286,6 +1664,7 @@ impl ProviderService {
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?;
Self::normalize_usage_script_credential_overrides(&app_type, &mut provider);
if provider_id_changed {
if !app_type.is_additive_mode() {