Model terbaru di 9router adalah claude-opus-5. Update semua jalur model default Opus: - fix(app_config_repo): fallback default_model custom_model.unwrap_or -> claude-opus-5; model_roles list claude-opus-5 - fix(app_config): router provider default_model -> claude-opus-5 - fix(settings test): assertion claude-opus-5 - fix(data): ~/.local/share/zesdex/settings.json model -> claude-opus-5
200 lines
6.5 KiB
Rust
200 lines
6.5 KiB
Rust
//! Pure domain entity for application settings.
|
|
//!
|
|
//! Defines `Settings` (top-level user configuration), `SettingsFlags`
|
|
//! (grouped boolean toggles), and `InternetMode` (network access level).
|
|
//! Serialised to `settings.json` by the infrastructure layer.
|
|
//!
|
|
//! # Architecture
|
|
//! This is a pure data structure with **no I/O logic**. Load/save
|
|
//! responsibilities live in `SettingsRepository` (domain::repository).
|
|
//!
|
|
//! ## Settings Fields
|
|
//! - `internet_mode` — network access policy (Off / ReadOnly / Full)
|
|
//! - `provider` / `model` — default LLM provider and model name
|
|
//! - `api_keys` — per-provider API key overrides (name → key)
|
|
//! - `max_tokens` / `temperature` — generation parameter defaults
|
|
//! - `review_max_lessons_per_run` — max lessons per auto-review pass
|
|
//! - `verify_command` — optional shell command to run for verification
|
|
//! - `workflow_max_concurrency` — max parallel hive-mind nodes
|
|
//! - `hive_mind_node_timeout_ms` — per-node timeout for hive-mind orchestration
|
|
//! - `flags` — grouped boolean feature toggles
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use super::app_config::AppConfig;
|
|
|
|
/// Controls how much network access the agent is permitted during a session.
|
|
///
|
|
/// ## Variants
|
|
/// - `Off` — no network access
|
|
/// - `ReadOnly` — HTTP GET / HEAD only
|
|
/// - `Full` — any HTTP method permitted
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
pub enum InternetMode {
|
|
/// No network access permitted.
|
|
#[default]
|
|
Off,
|
|
/// HTTP GET / HEAD requests only.
|
|
ReadOnly,
|
|
/// Any HTTP method permitted.
|
|
Full,
|
|
}
|
|
|
|
/// Grouped boolean feature toggles for the application.
|
|
///
|
|
/// Kept as a separate struct to avoid clippy's
|
|
/// `default-too-many-fields` threshold on `Settings`.
|
|
///
|
|
/// ## Fields
|
|
/// - `review_enabled` — enable automatic inline review after edits
|
|
/// - `session_archive_enabled` — enable periodic session archiving
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SettingsFlags {
|
|
pub review_enabled: bool,
|
|
pub session_archive_enabled: bool,
|
|
}
|
|
|
|
impl Default for SettingsFlags {
|
|
/// Returns the default flags with all features enabled.
|
|
fn default() -> Self {
|
|
Self {
|
|
review_enabled: true,
|
|
session_archive_enabled: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the default hive-mind node timeout (600 seconds).
|
|
fn default_hive_mind_node_timeout_ms() -> u64 {
|
|
600_000
|
|
}
|
|
|
|
/// Top-level application settings model.
|
|
///
|
|
/// Serialised to `settings.json` by the infrastructure persistence layer.
|
|
/// Holds LLM provider selection, generation parameters, feature flags,
|
|
/// and workflow configuration.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Settings {
|
|
pub internet_mode: InternetMode,
|
|
pub provider: String,
|
|
pub model: String,
|
|
pub api_keys: HashMap<String, String>,
|
|
pub max_tokens: Option<u32>,
|
|
pub temperature: Option<f32>,
|
|
pub review_max_lessons_per_run: usize,
|
|
pub adaptive_review_max_skip: u32,
|
|
pub verify_command: Option<String>,
|
|
pub verify_timeout_ms: u64,
|
|
pub workflow_max_concurrency: usize,
|
|
#[serde(flatten)]
|
|
pub flags: SettingsFlags,
|
|
#[serde(default = "default_hive_mind_node_timeout_ms")]
|
|
pub hive_mind_node_timeout_ms: u64,
|
|
}
|
|
|
|
impl Default for Settings {
|
|
fn default() -> Self {
|
|
Self {
|
|
internet_mode: InternetMode::Off,
|
|
provider: "zen".to_string(),
|
|
model: "deepseek-v4-flash-free".to_string(),
|
|
api_keys: HashMap::new(),
|
|
max_tokens: None,
|
|
temperature: None,
|
|
review_max_lessons_per_run: 5,
|
|
adaptive_review_max_skip: 3,
|
|
verify_command: None,
|
|
verify_timeout_ms: 30_000,
|
|
workflow_max_concurrency: 5,
|
|
flags: SettingsFlags::default(),
|
|
hive_mind_node_timeout_ms: 600_000,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pick the effective model name for the main agent.
|
|
///
|
|
/// When `settings.provider` is `"claude"` (auto-detected from
|
|
/// `~/.claude/settings.json`), the provider's `default_model` (or the
|
|
/// app-level `default_model`) wins over a possibly-stale persisted
|
|
/// `settings.model`. Otherwise the user's explicit `settings.model` is used.
|
|
///
|
|
/// Why: the user's custom Claude endpoint (URL + API key from
|
|
/// `~/.claude/settings.json`) implies Opus as the model; a stale
|
|
/// `settings.json` (e.g. "deepseek-v4-flash-free") must not override it.
|
|
pub fn resolve_effective_model(settings: &Settings, app_config: &AppConfig) -> String {
|
|
if settings.provider == "claude" {
|
|
if let Some(m) = app_config
|
|
.providers
|
|
.get("claude")
|
|
.and_then(|p| p.default_model.clone())
|
|
{
|
|
return m;
|
|
}
|
|
return app_config.default_model.clone();
|
|
}
|
|
settings.model.clone()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::cms::app_config::AppConfig;
|
|
|
|
fn claude_app_config() -> AppConfig {
|
|
let mut cfg = AppConfig::default();
|
|
cfg.providers.insert(
|
|
"claude".to_string(),
|
|
crate::cms::ProviderConfig {
|
|
api_base: "https://9router.example/v1".to_string(),
|
|
api_key_env: Some("ANTHROPIC_API_KEY".to_string()),
|
|
default_model: Some("claude-opus-5".to_string()),
|
|
default_api_key: Some("sk-test".to_string()),
|
|
},
|
|
);
|
|
cfg.default_provider = "claude".to_string();
|
|
cfg.default_model = "claude-opus-5".to_string();
|
|
cfg
|
|
}
|
|
|
|
#[test]
|
|
fn claude_provider_uses_opus_model_over_stale_settings_model() {
|
|
let settings = Settings {
|
|
provider: "claude".to_string(),
|
|
model: "deepseek-v4-flash-free".to_string(), // stale persisted
|
|
..Settings::default()
|
|
};
|
|
|
|
let model = resolve_effective_model(&settings, &claude_app_config());
|
|
assert_eq!(model, "claude-opus-5");
|
|
}
|
|
|
|
#[test]
|
|
fn non_claude_provider_uses_settings_model() {
|
|
let settings = Settings {
|
|
provider: "zen".to_string(),
|
|
model: "my-model".to_string(),
|
|
..Settings::default()
|
|
};
|
|
|
|
let model = resolve_effective_model(&settings, &AppConfig::default());
|
|
assert_eq!(model, "my-model");
|
|
}
|
|
|
|
#[test]
|
|
fn claude_falls_back_to_app_default() {
|
|
let settings = Settings {
|
|
provider: "claude".to_string(),
|
|
model: String::new(),
|
|
..Settings::default()
|
|
};
|
|
|
|
let cfg = AppConfig::default();
|
|
let model = resolve_effective_model(&settings, &cfg);
|
|
assert_eq!(model, cfg.default_model);
|
|
}
|
|
}
|