feat(config): support CLIProxyAPI v8.0.10 template settings
- Add structured editors for request, OAuth, plugin, and concurrency settings - Expand advanced configuration for all eight provider families and credentials - Preserve YAML comments, secrets, unknown fields, and explicit overrides - Add validation, conflict detection, default restoration, and restart notices - Improve responsive layouts and localize configuration controls - Fix provider health checks and extend mocks and integration tests
This commit is contained in:
1 parent
4d1afc82ef
commit
c89e2fb020
46 files changed
+3839
-235
No files matched your search
@@ -5,6 +5,7 @@ mod alias_edit;
|
||||
mod alias_save;
|
||||
mod aliases;
|
||||
mod commands;
|
||||
mod extended;
|
||||
mod provider_groups;
|
||||
mod settings;
|
||||
mod yaml;
|
||||
@@ -12,6 +13,7 @@ pub(crate) use alias_edit::*;
|
||||
pub(crate) use alias_save::*;
|
||||
pub(crate) use aliases::*;
|
||||
pub(crate) use commands::*;
|
||||
pub(crate) use extended::*;
|
||||
pub(crate) use provider_groups::*;
|
||||
pub(crate) use settings::*;
|
||||
pub(crate) use yaml::*;
|
||||
@@ -12,7 +12,7 @@ pub(crate) struct OAuthAliasChannel {
|
||||
pub(crate) force_mapping: bool,
|
||||
}
|
||||
|
||||
pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 8] = [
|
||||
pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 9] = [
|
||||
OAuthAliasChannel {
|
||||
key: "vertex",
|
||||
provider: "Vertex OAuth",
|
||||
@@ -85,6 +85,15 @@ pub(crate) const OAUTH_ALIAS_CHANNELS: [OAuthAliasChannel; 8] = [
|
||||
supports_fast: false,
|
||||
force_mapping: false,
|
||||
},
|
||||
OAuthAliasChannel {
|
||||
key: "meta",
|
||||
provider: "Meta OAuth",
|
||||
kind: "meta-oauth",
|
||||
protocol: "openai",
|
||||
supports_reasoning: false,
|
||||
supports_fast: false,
|
||||
force_mapping: false,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -137,6 +146,7 @@ pub(crate) fn normalize_oauth_alias_channel(value: &str) -> Option<&'static str>
|
||||
"kimi" | "moonshot" => Some("kimi"),
|
||||
"devin" | "cognition" => Some("devin"),
|
||||
"xai" | "x-ai" | "grok" => Some("xai"),
|
||||
"meta" => Some("meta"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,8 +166,8 @@ pub(crate) fn save_network_endpoint_settings(
|
||||
return Err("Port must be between 1 and 65535".to_string());
|
||||
}
|
||||
let host = normalize_optional_config_string(settings.host, "Listen IP")?;
|
||||
if host.parse::<IpAddr>().is_err() {
|
||||
return Err("Listen IP must be a valid IPv4 or IPv6 address".to_string());
|
||||
if !host.is_empty() && !host.eq_ignore_ascii_case("localhost") && host.parse::<IpAddr>().is_err() {
|
||||
return Err("Listen address must be empty, localhost, or a valid IPv4 or IPv6 address".to_string());
|
||||
}
|
||||
let previous = gui_config_state.snapshot()?;
|
||||
let mut next = previous.clone();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -26,7 +26,8 @@ pub(crate) fn validate_api_key_remark(remark: &str) -> Result<(), String> {
|
||||
pub(crate) fn validate_api_access_provider_section(section: &str) -> Result<(), String> {
|
||||
if matches!(
|
||||
section,
|
||||
"gemini-api-key" | "codex-api-key" | "claude-api-key" | "openai-compatibility"
|
||||
"gemini-api-key" | "interactions-api-key" | "vertex-api-key" | "codex-api-key"
|
||||
| "claude-api-key" | "xai-api-key" | "meta-api-key" | "openai-compatibility"
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -44,6 +45,10 @@ pub(crate) fn usage_provider_section(provider: &str) -> Option<&'static str> {
|
||||
"codex" => Some("codex-api-key"),
|
||||
"claude" => Some("claude-api-key"),
|
||||
"gemini" | "aistudio" => Some("gemini-api-key"),
|
||||
"interactions" => Some("interactions-api-key"),
|
||||
"vertex" | "vertex-ai" => Some("vertex-api-key"),
|
||||
"xai" | "grok" => Some("xai-api-key"),
|
||||
"meta" => Some("meta-api-key"),
|
||||
"openai" | "openai-compatibility" => Some("openai-compatibility"),
|
||||
_ => None,
|
||||
}
|
||||
@@ -113,7 +118,8 @@ pub(crate) fn generate_management_secret_key() -> Result<String, String> {
|
||||
|
||||
pub(crate) fn management_secret_requires_rotation(secret_key: &str) -> bool {
|
||||
let secret_key = secret_key.trim();
|
||||
secret_key.is_empty() || is_hashed_management_secret_key(secret_key)
|
||||
// Empty is an intentional disabled Management API, including after restart.
|
||||
is_hashed_management_secret_key(secret_key)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_strong_management_secret(config: &mut GuiConfigFile) -> Result<bool, String> {
|
||||
@@ -150,10 +156,10 @@ pub(crate) fn is_hashed_management_secret_key(secret_key: &str) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) fn validate_routing_strategy(strategy: &str) -> Result<(), String> {
|
||||
if matches!(strategy, "round-robin" | "fill-first") {
|
||||
if matches!(strategy, "round-robin" | "weighted-round-robin" | "fill-first") {
|
||||
return Ok(());
|
||||
}
|
||||
Err("Routing strategy supports only round-robin or fill-first".to_string())
|
||||
Err("Routing strategy supports round-robin, weighted-round-robin, or fill-first".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_optional_config_string(
|
||||
@@ -1423,8 +1429,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
config.management_secret_key = core_settings
|
||||
.management_secret_key
|
||||
.as_deref()
|
||||
.filter(|secret_key| !is_hashed_management_secret_key(secret_key))
|
||||
.unwrap_or_default()
|
||||
.unwrap_or(&config.management_secret_key)
|
||||
.to_string();
|
||||
}
|
||||
if presence.routing_strategy.is_none() {
|
||||
@@ -1464,8 +1469,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
config.management_secret_key = read_installed_core_config_settings()
|
||||
.ok()
|
||||
.and_then(|settings| settings.management_secret_key)
|
||||
.filter(|secret_key| !is_hashed_management_secret_key(secret_key))
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_else(|| config.management_secret_key.clone());
|
||||
changed = true;
|
||||
}
|
||||
if presence.host.is_none() || presence.auth_dir.is_none() {
|
||||
@@ -1783,15 +1787,6 @@ pub(crate) fn sanitize_gui_config_at(
|
||||
changed = true;
|
||||
}
|
||||
let host = config.host.trim();
|
||||
let host = if host.is_empty() {
|
||||
if config.allow_lan {
|
||||
"0.0.0.0"
|
||||
} else {
|
||||
"127.0.0.1"
|
||||
}
|
||||
} else {
|
||||
host
|
||||
};
|
||||
if config.host != host {
|
||||
config.host = host.to_string();
|
||||
changed = true;
|
||||
@@ -1801,14 +1796,16 @@ pub(crate) fn sanitize_gui_config_at(
|
||||
config.allow_lan = allow_lan;
|
||||
changed = true;
|
||||
}
|
||||
match migrate_auth_dir_at(config, install_dir, persistent_auth_dir) {
|
||||
Ok(migrated) => changed |= migrated,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to recover OAuth directory; keeping existing configuration: {error}");
|
||||
if !config.auth_dir_user_selected {
|
||||
match migrate_auth_dir_at(config, install_dir, persistent_auth_dir) {
|
||||
Ok(migrated) => changed |= migrated,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to recover OAuth directory; keeping existing configuration: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.auth_dir.trim().is_empty()
|
||||
|| Path::new(config.auth_dir.trim()) == persistent_auth_dir
|
||||
|| (!config.auth_dir_user_selected && Path::new(config.auth_dir.trim()) == persistent_auth_dir)
|
||||
{
|
||||
config.auth_dir = DEFAULT_AUTH_DIR.to_string();
|
||||
changed = true;
|
||||
@@ -1934,6 +1931,7 @@ pub(crate) fn write_gui_config_to_path(
|
||||
("close-behavior", value(config.close_behavior.as_str())),
|
||||
("default-terminal", value(config.default_terminal.as_str())),
|
||||
("auth-dir", value(config.auth_dir.as_str())),
|
||||
("auth-dir-user-selected", value(config.auth_dir_user_selected)),
|
||||
(
|
||||
"management-secret-key",
|
||||
value(config.management_secret_key.as_str()),
|
||||
@@ -2066,7 +2064,7 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
|
||||
if config.port == 0 {
|
||||
return Err("GUI configuration port must be between 1 and 65535".to_string());
|
||||
}
|
||||
if config.host.trim().is_empty() || config.host.chars().any(char::is_control) {
|
||||
if config.host.chars().any(char::is_control) {
|
||||
return Err("Invalid GUI configuration host".to_string());
|
||||
}
|
||||
if config.auth_dir.trim().is_empty() || config.auth_dir.chars().any(char::is_control) {
|
||||
@@ -2104,7 +2102,11 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
|
||||
}
|
||||
validate_api_key_remark(&entry.remark)?;
|
||||
}
|
||||
validate_strong_management_secret_key(&config.management_secret_key)?;
|
||||
if config.management_secret_key.is_empty() {
|
||||
validate_management_secret_key(&config.management_secret_key)?;
|
||||
} else {
|
||||
validate_strong_management_secret_key(&config.management_secret_key)?;
|
||||
}
|
||||
validate_routing_strategy(config.routing_strategy.trim())?;
|
||||
if config.proxy_override {
|
||||
network_proxy::normalize_optional_proxy_url(&config.proxy_url)?;
|
||||
|
||||
@@ -1468,7 +1468,7 @@ pub(crate) fn core_config_settings_from_value(
|
||||
.ok_or_else(|| "auth-dir must be a string".to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| OAUTH_DIR_NAME.to_string());
|
||||
.unwrap_or_else(|| "~/.cli-proxy-api".to_string());
|
||||
let debug = v8_or_legacy(&["observability", "logs", "debug"], &["debug"])
|
||||
.map(|value| {
|
||||
value
|
||||
@@ -1622,9 +1622,8 @@ pub(crate) fn core_config_settings_from_value(
|
||||
let max_retry_interval = v8_or_legacy(&["routing", "retry", "max-retry-interval"], &["max-retry-interval"])
|
||||
.map(|value| {
|
||||
value
|
||||
.as_u64()
|
||||
.and_then(|value| u32::try_from(value).ok())
|
||||
.ok_or_else(|| "max-retry-interval must be a non-negative integer".to_string())
|
||||
.as_i64()
|
||||
.ok_or_else(|| "max-retry-interval must be an integer".to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
@@ -1743,9 +1742,6 @@ pub(crate) fn extract_core_management_secret_key(
|
||||
.ok_or_else(|| "remote-management.secret-key must be a string".to_string())?
|
||||
.trim()
|
||||
.to_string();
|
||||
if value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -154,7 +154,7 @@ const DEFAULT_AGENT_TERMINAL: &str = "auto";
|
||||
const DEFAULT_API_KEY_INITIAL_REMARK: &str = "Default key";
|
||||
const DEFAULT_REQUEST_RETRY: u32 = 3;
|
||||
const DEFAULT_MAX_RETRY_CREDENTIALS: u32 = 0;
|
||||
const DEFAULT_MAX_RETRY_INTERVAL: u32 = 30;
|
||||
const DEFAULT_MAX_RETRY_INTERVAL: i64 = 30;
|
||||
const DEFAULT_STREAMING_BOOTSTRAP_RETRIES: u32 = 0;
|
||||
const DEFAULT_DISABLE_COOLING: bool = false;
|
||||
const DEFAULT_LOGS_MAX_TOTAL_SIZE_MB: u32 = 0;
|
||||
@@ -603,6 +603,7 @@ struct GuiConfigFile {
|
||||
window_width: Option<u32>,
|
||||
window_height: Option<u32>,
|
||||
auth_dir: String,
|
||||
auth_dir_user_selected: bool,
|
||||
#[serde(deserialize_with = "deserialize_gui_api_keys")]
|
||||
api_keys: Vec<GuiApiKeyEntry>,
|
||||
api_access_remarks: Vec<GuiApiAccessRemark>,
|
||||
@@ -628,7 +629,7 @@ struct GuiConfigFile {
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
max_retry_interval: i64,
|
||||
streaming_bootstrap_retries: u32,
|
||||
}
|
||||
|
||||
@@ -909,6 +910,7 @@ impl Default for GuiConfigFile {
|
||||
window_width: Some(DEFAULT_MAIN_WINDOW_WIDTH),
|
||||
window_height: Some(DEFAULT_MAIN_WINDOW_HEIGHT),
|
||||
auth_dir: DEFAULT_AUTH_DIR.to_string(),
|
||||
auth_dir_user_selected: false,
|
||||
api_keys: vec![default_api_key_entry()],
|
||||
api_access_remarks: Vec::new(),
|
||||
management_secret_key: LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string(),
|
||||
@@ -975,7 +977,7 @@ struct GuiConfigPresence {
|
||||
disable_cooling: Option<bool>,
|
||||
request_retry: Option<u32>,
|
||||
max_retry_credentials: Option<u32>,
|
||||
max_retry_interval: Option<u32>,
|
||||
max_retry_interval: Option<i64>,
|
||||
streaming_bootstrap_retries: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -1467,7 +1469,7 @@ struct GuiNetworkRoutingSettings {
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
max_retry_interval: i64,
|
||||
streaming_bootstrap_retries: u32,
|
||||
}
|
||||
|
||||
@@ -1489,7 +1491,7 @@ struct GuiRetrySettings {
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
max_retry_interval: i64,
|
||||
streaming_bootstrap_retries: u32,
|
||||
}
|
||||
|
||||
@@ -1554,7 +1556,7 @@ struct CoreConfigSettings {
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
max_retry_interval: i64,
|
||||
streaming_bootstrap_retries: u32,
|
||||
#[allow(dead_code)]
|
||||
#[serde(skip_serializing)]
|
||||
@@ -1593,7 +1595,7 @@ struct CoreConfigView {
|
||||
disable_cooling: bool,
|
||||
request_retry: u32,
|
||||
max_retry_credentials: u32,
|
||||
max_retry_interval: u32,
|
||||
max_retry_interval: i64,
|
||||
streaming_bootstrap_retries: u32,
|
||||
}
|
||||
|
||||
@@ -2208,7 +2210,6 @@ impl GuiConfigState {
|
||||
if let Some(secret_key) = settings
|
||||
.management_secret_key
|
||||
.as_deref()
|
||||
.filter(|secret_key| !secret_key.is_empty())
|
||||
.filter(|secret_key| !is_hashed_management_secret_key(secret_key))
|
||||
{
|
||||
config.management_secret_key = secret_key.to_string();
|
||||
@@ -2675,6 +2676,8 @@ fn main() {
|
||||
get_core_sensitive_words_settings,
|
||||
save_core_sensitive_words_settings,
|
||||
get_core_config_settings,
|
||||
get_extended_core_config,
|
||||
save_extended_core_config,
|
||||
save_core_logging_settings,
|
||||
set_core_request_log,
|
||||
add_core_api_key,
|
||||
|
||||
@@ -19,6 +19,8 @@ pub(crate) struct ProviderHealthProbeRequest {
|
||||
header: HashMap<String, String>,
|
||||
data: String,
|
||||
protocol: String,
|
||||
#[serde(default)]
|
||||
source_provider: Option<String>,
|
||||
timeout_ms: Option<u64>,
|
||||
#[serde(default)]
|
||||
model: String,
|
||||
@@ -125,6 +127,15 @@ fn provider_health_json_has_text(protocol: &str, value: &serde_json::Value) -> b
|
||||
})
|
||||
})
|
||||
}),
|
||||
"interactions" => {
|
||||
let event = value.get("event_type").or_else(|| value.get("type")).and_then(serde_json::Value::as_str);
|
||||
(event == Some("content.delta")
|
||||
&& value.pointer("/delta/type").and_then(serde_json::Value::as_str) == Some("text")
|
||||
&& provider_health_value_has_text(value.pointer("/delta/text")))
|
||||
|| (event == Some("interaction.complete")
|
||||
&& value.pointer("/interaction/status").and_then(serde_json::Value::as_str) == Some("completed")
|
||||
&& provider_health_value_has_text(value.pointer("/interaction/outputs")))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -144,6 +155,11 @@ pub(crate) fn provider_health_stream_has_text(protocol: &str, bytes: &[u8]) -> b
|
||||
}
|
||||
|
||||
fn provider_health_json_has_terminal_success(protocol: &str, value: &serde_json::Value) -> bool {
|
||||
if protocol == "interactions" {
|
||||
let event = value.get("event_type").or_else(|| value.get("type")).and_then(serde_json::Value::as_str);
|
||||
return event == Some("interaction.complete")
|
||||
&& value.pointer("/interaction/status").and_then(serde_json::Value::as_str) == Some("completed");
|
||||
}
|
||||
if protocol != "gemini" {
|
||||
return false;
|
||||
}
|
||||
@@ -238,6 +254,7 @@ fn provider_health_usage_provider(protocol: &str) -> &str {
|
||||
"openai-chat" => "openai",
|
||||
"claude" => "claude",
|
||||
"gemini" => "gemini",
|
||||
"interactions" => "interactions",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
@@ -258,7 +275,9 @@ fn persist_provider_health_success(
|
||||
"source": request.source.as_str(),
|
||||
"auth_index": request.auth_index.as_str(),
|
||||
"failed": false,
|
||||
"provider": provider_health_usage_provider(&request.protocol),
|
||||
"provider": request.source_provider.as_deref()
|
||||
.filter(|provider| matches!(*provider, "interactions" | "vertex" | "xai" | "meta"))
|
||||
.unwrap_or_else(|| provider_health_usage_provider(&request.protocol)),
|
||||
"model": request.model.as_str(),
|
||||
"executor_type": "DesktopProviderHealthCheck",
|
||||
"endpoint": endpoint,
|
||||
@@ -296,7 +315,7 @@ pub(crate) async fn provider_health_probe(
|
||||
}
|
||||
if !matches!(
|
||||
request.protocol.as_str(),
|
||||
"openai-chat" | "openai-responses" | "claude" | "gemini"
|
||||
"openai-chat" | "openai-responses" | "claude" | "gemini" | "interactions"
|
||||
) {
|
||||
return Err("Unsupported health check protocol".to_string());
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fn gui_config_defaults_are_stable() {
|
||||
assert!(content.contains("[[api-keys]]"));
|
||||
assert!(content.contains("key = \"123456\""));
|
||||
assert!(content.contains("remark = \"Default key\""));
|
||||
assert!(content.contains("management-secret-key = \"\""));
|
||||
assert!(content.contains(&format!("management-secret-key = \"{LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY}\"")));
|
||||
assert!(content.contains("plugins-enabled = false"));
|
||||
assert!(content.contains("routing-strategy = \"round-robin\""));
|
||||
assert!(content.contains("download-source = \"github\""));
|
||||
|
||||
@@ -116,7 +116,7 @@ fn webui_management_secret_requires_a_non_empty_plaintext_value() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn management_secret_rotation_replaces_empty_values_and_preserves_custom_values() {
|
||||
fn management_secret_rotation_preserves_disabled_and_custom_values() {
|
||||
let fresh = GuiConfigFile::default();
|
||||
assert_eq!(
|
||||
fresh.management_secret_key,
|
||||
@@ -130,9 +130,9 @@ fn management_secret_rotation_replaces_empty_values_and_preserves_custom_values(
|
||||
management_secret_key: String::new(),
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
assert!(ensure_strong_management_secret(&mut empty).unwrap());
|
||||
assert!(empty.management_secret_key.starts_with("wui-Aa9_"));
|
||||
assert!(empty.management_secret_key.len() >= 50);
|
||||
assert!(!ensure_strong_management_secret(&mut empty).unwrap());
|
||||
assert!(empty.management_secret_key.is_empty());
|
||||
assert!(validate_gui_config(&empty).is_ok());
|
||||
|
||||
let mut legacy = GuiConfigFile {
|
||||
management_secret_key: LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string(),
|
||||
@@ -207,6 +207,7 @@ fn default_auth_directory_is_relative_and_legacy_absolute_value_is_migrated() {
|
||||
|
||||
let mut config = GuiConfigFile {
|
||||
auth_dir: path_to_string(&fixed_oauth_dir().unwrap()),
|
||||
auth_dir_user_selected: false,
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
assert!(sanitize_gui_config(&mut config).unwrap());
|
||||
@@ -1886,7 +1887,7 @@ fn core_config_validates_keys_and_routing_strategy() {
|
||||
assert!(validate_core_api_key("").is_err());
|
||||
assert!(validate_core_api_key("contains space").is_err());
|
||||
assert!(validate_routing_strategy("round-robin").is_ok());
|
||||
assert!(validate_routing_strategy("weighted-round-robin").is_err());
|
||||
assert!(validate_routing_strategy("weighted-round-robin").is_ok());
|
||||
assert!(validate_routing_strategy("fill-first").is_ok());
|
||||
assert!(validate_routing_strategy("random").is_err());
|
||||
}
|
||||
@@ -1949,6 +1950,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
|
||||
window_width: None,
|
||||
window_height: None,
|
||||
auth_dir: path_to_string(&fixed_oauth_dir().unwrap()),
|
||||
auth_dir_user_selected: false,
|
||||
api_keys: vec![
|
||||
default_api_key_entry(),
|
||||
GuiApiKeyEntry {
|
||||
@@ -2119,3 +2121,52 @@ fn startup_merge_can_shrink_template_api_key_sequence() {
|
||||
config.management_secret_key
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn v8_signed_retry_weighted_routing_and_empty_host_roundtrip() {
|
||||
let content = "config-version: 8\nserver: {host: '', port: 8317}\nrouting: {strategy: weighted-round-robin, retry: {max-retry-interval: -1}}\nmanagement: {secret-key: ''}\n";
|
||||
let settings = core_config_settings_from_value(&serde_norway::from_str(content).unwrap()).unwrap();
|
||||
let mut config = GuiConfigFile::default();
|
||||
apply_core_settings_to_gui_config(&mut config, &settings);
|
||||
sanitize_gui_config(&mut config).unwrap();
|
||||
validate_gui_config(&config).unwrap();
|
||||
assert_eq!(config.host, "");
|
||||
assert_eq!(config.max_retry_interval, -1);
|
||||
assert_eq!(config.routing_strategy, "weighted-round-robin");
|
||||
assert!(!ensure_strong_management_secret(&mut config).unwrap());
|
||||
assert!(config.management_secret_key.is_empty());
|
||||
let serialized = toml::to_string(&config).unwrap();
|
||||
let mut restored: GuiConfigFile = toml::from_str(&serialized).unwrap();
|
||||
assert!(!ensure_strong_management_secret(&mut restored).unwrap());
|
||||
assert_eq!(restored.max_retry_interval, -1);
|
||||
let saved = apply_gui_managed_settings(content, &restored).unwrap();
|
||||
let document: serde_norway::Value = serde_norway::from_str(&saved).unwrap();
|
||||
assert_eq!(document["server"]["host"], "");
|
||||
assert_eq!(document["management"]["secret-key"], "");
|
||||
assert_eq!(document["routing"]["retry"]["max-retry-interval"], -1);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn explicitly_selected_auth_directory_is_not_moved_on_restart() {
|
||||
let root = agent_test_home("selected-auth-no-migration");
|
||||
let install_dir = root.join("cpa-core");
|
||||
let selected = install_dir.join("oauth");
|
||||
let persistent = root.join("oauth");
|
||||
fs::create_dir_all(&selected).unwrap();
|
||||
fs::write(selected.join("account.json"), "credential").unwrap();
|
||||
let mut config = GuiConfigFile { auth_dir: "oauth".into(), auth_dir_user_selected: true, ..GuiConfigFile::default() };
|
||||
sanitize_gui_config_at(&mut config, &install_dir, &persistent).unwrap();
|
||||
assert_eq!(config.auth_dir, "oauth");
|
||||
assert!(selected.join("account.json").exists());
|
||||
assert!(!persistent.exists());
|
||||
let config_path = root.join("selected.toml");
|
||||
write_gui_config_to_path(&config, &config_path).unwrap();
|
||||
let mut restored: GuiConfigFile = toml::from_str(&fs::read_to_string(&config_path).unwrap()).unwrap();
|
||||
assert!(restored.auth_dir_user_selected);
|
||||
sanitize_gui_config_at(&mut restored, &install_dir, &persistent).unwrap();
|
||||
assert_eq!(restored.auth_dir, "oauth");
|
||||
assert!(selected.join("account.json").exists());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
@@ -49,6 +49,7 @@ fn oauth_auth_provider_names_map_to_alias_channels() {
|
||||
Some("aistudio")
|
||||
);
|
||||
assert_eq!(normalize_oauth_alias_channel("grok"), Some("xai"));
|
||||
assert_eq!(normalize_oauth_alias_channel("meta"), Some("meta"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -97,3 +97,18 @@ data: {"delta":{"type":"text_delta","text":"H"}}
|
||||
gemini_thought_only
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn interactions_health_probe_recognizes_content_and_completed_events() {
|
||||
assert!(provider_health_stream_has_text("interactions", br#"data: {"event_type":"content.delta","delta":{"type":"text","text":"Hello"}}
|
||||
"#));
|
||||
assert!(provider_health_stream_has_text("interactions", br#"data: {"event_type":"interaction.complete","interaction":{"status":"completed","outputs":[{"type":"text","text":"Hello"}]}}
|
||||
"#));
|
||||
assert!(provider_health_stream_has_terminal_success("interactions", br#"data: {"event_type":"interaction.complete","interaction":{"status":"completed"}}
|
||||
"#));
|
||||
assert!(!provider_health_stream_has_terminal_success("interactions", br#"data: {"event_type":"interaction.complete","interaction":{"status":"failed"}}
|
||||
"#));
|
||||
assert!(!provider_health_stream_has_text("interactions", br#"data: {"event_type":"content.delta","delta":{"type":"image","text":"metadata"}}
|
||||
"#));
|
||||
}
|
||||
@@ -5133,6 +5133,7 @@ mod tests {
|
||||
window_width: None,
|
||||
window_height: None,
|
||||
auth_dir: String::new(),
|
||||
auth_dir_user_selected: false,
|
||||
api_keys: Vec::new(),
|
||||
api_access_remarks: Vec::new(),
|
||||
management_secret_key: "123456".to_string(),
|
||||
|
||||
Reference in new issue
Block a user