mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
feat(hermes): align provider schema with Hermes Agent 0.10.0
Hermes 0.10.0 tightened custom_providers validation (commit 2cdae233): invalid base_urls are rejected, unknown fields produce warnings, and new fields (rate_limit_delay, bedrock_converse, key_env) landed. - Add bedrock_converse to the api_mode selector (and i18n labels) - Expose rate_limit_delay in a provider-level advanced panel - Validate base_url client-side (URL shape, template-token friendly) - Drop per-model max_tokens — not in _VALID_CUSTOM_PROVIDER_FIELDS - Round-trip test asserts set_provider preserves rate_limit_delay / key_env / any unknown forward-compat field
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
mod support;
|
||||
|
||||
use cc_switch_lib::{hermes_config, update_settings, AppSettings};
|
||||
|
||||
/// 读取并回写 Hermes provider 时,Hermes v12+ 新增或未来才会出现的字段
|
||||
/// (例如 `rate_limit_delay`、`key_env`)必须透传,不能因为 UI 不感知就静默丢弃。
|
||||
/// 否则用户在 Hermes Web UI 配置的高级字段会在 CC Switch 编辑后消失。
|
||||
fn with_temp_hermes_dir<F: FnOnce(&std::path::Path)>(f: F) {
|
||||
let guard = support::test_mutex().lock().expect("test mutex poisoned");
|
||||
let home = support::ensure_test_home();
|
||||
support::reset_test_fs();
|
||||
|
||||
let hermes_dir = home.join(".hermes-roundtrip");
|
||||
let _ = std::fs::remove_dir_all(&hermes_dir);
|
||||
std::fs::create_dir_all(&hermes_dir).expect("create temp hermes dir");
|
||||
|
||||
update_settings(AppSettings {
|
||||
hermes_config_dir: Some(hermes_dir.to_string_lossy().into_owned()),
|
||||
..AppSettings::default()
|
||||
})
|
||||
.expect("set hermes_config_dir override");
|
||||
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&hermes_dir)));
|
||||
|
||||
// Always restore settings and drop fixture dir, even on test failure.
|
||||
let _ = update_settings(AppSettings::default());
|
||||
let _ = std::fs::remove_dir_all(&hermes_dir);
|
||||
drop(guard);
|
||||
|
||||
if let Err(err) = result {
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_provider_preserves_unknown_and_future_fields() {
|
||||
with_temp_hermes_dir(|dir| {
|
||||
let yaml = r#"custom_providers:
|
||||
- name: myhost
|
||||
base_url: https://api.example.com/v1
|
||||
api_key: sk-old
|
||||
api_mode: chat_completions
|
||||
rate_limit_delay: 0.5
|
||||
key_env: MY_API_KEY
|
||||
foo_bar: keep-me-around
|
||||
models:
|
||||
gpt-4:
|
||||
context_length: 8192
|
||||
"#;
|
||||
let config_path = dir.join("config.yaml");
|
||||
std::fs::write(&config_path, yaml).expect("seed config.yaml");
|
||||
|
||||
// Simulate the UI sending back only the fields it knows about.
|
||||
let patch = serde_json::json!({
|
||||
"name": "myhost",
|
||||
"base_url": "https://api.example.com/v1",
|
||||
"api_key": "sk-new",
|
||||
"api_mode": "chat_completions",
|
||||
"models": [
|
||||
{ "id": "gpt-4", "context_length": 8192 }
|
||||
]
|
||||
});
|
||||
|
||||
hermes_config::set_provider("myhost", patch).expect("set_provider");
|
||||
|
||||
let written = std::fs::read_to_string(&config_path).expect("read written config");
|
||||
|
||||
assert!(
|
||||
written.contains("rate_limit_delay"),
|
||||
"rate_limit_delay stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("key_env"),
|
||||
"key_env key stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("MY_API_KEY"),
|
||||
"key_env value stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("foo_bar"),
|
||||
"unknown forward-compat field stripped:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
written.contains("sk-new"),
|
||||
"api_key was not updated to sk-new:\n{written}"
|
||||
);
|
||||
assert!(
|
||||
!written.contains("sk-old"),
|
||||
"old api_key still present:\n{written}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_providers_surfaces_rate_limit_delay_and_key_env() {
|
||||
with_temp_hermes_dir(|dir| {
|
||||
let yaml = r#"custom_providers:
|
||||
- name: myhost
|
||||
base_url: https://api.example.com/v1
|
||||
api_key: sk-xxx
|
||||
api_mode: chat_completions
|
||||
rate_limit_delay: 2.5
|
||||
key_env: FOO_KEY
|
||||
models:
|
||||
m1: {}
|
||||
"#;
|
||||
std::fs::write(dir.join("config.yaml"), yaml).expect("seed config.yaml");
|
||||
|
||||
let providers = hermes_config::get_providers().expect("get_providers");
|
||||
let entry = providers.get("myhost").expect("myhost missing");
|
||||
|
||||
assert_eq!(
|
||||
entry.get("rate_limit_delay").and_then(|v| v.as_f64()),
|
||||
Some(2.5),
|
||||
"rate_limit_delay not surfaced to DAO payload"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.get("key_env").and_then(|v| v.as_str()),
|
||||
Some("FOO_KEY"),
|
||||
"key_env not surfaced to DAO payload"
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user