use crate::constants::DEFAULT_TEMPERATURE;
use crate::models::reasoning::ReasoningLevel;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub model: String,
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
pub system_prompt: Option<String>,
#[serde(skip)]
pub dynamic_system_suffix: Option<String>,
#[serde(default)]
pub reasoning: ReasoningLevel,
#[serde(default)]
pub backend_options: HashMap<String, HashMap<String, String>>,
#[serde(skip)]
pub output_schema: Option<serde_json::Value>,
#[serde(skip)]
pub tools: Vec<serde_json::Value>,
#[serde(skip)]
pub resolved_context_window: Option<usize>,
#[serde(skip)]
pub resolved_max_output: Option<usize>,
#[serde(skip)]
pub native_compaction: Option<NativeCompaction>,
#[serde(skip)]
pub native_tools: NativeTools,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NativeTools {
pub text_editor: bool,
pub shell: bool,
pub computer: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NativeCompaction {
pub trigger_tokens: usize,
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
model: String::new(),
temperature: default_temperature(),
max_tokens: default_max_tokens(),
system_prompt: None,
dynamic_system_suffix: None,
reasoning: ReasoningLevel::default(),
backend_options: HashMap::new(),
tools: Vec::new(),
resolved_context_window: None,
resolved_max_output: None,
output_schema: None,
native_compaction: None,
native_tools: NativeTools::default(),
}
}
}
impl ModelConfig {
#[must_use]
pub fn get_backend_option(&self, backend: &str, key: &str) -> Option<&String> {
self.backend_options.get(backend)?.get(key)
}
#[must_use]
pub fn get_backend_option_i32(&self, backend: &str, key: &str) -> Option<i32> {
self.get_backend_option(backend, key)?.parse::<i32>().ok()
}
#[must_use]
pub fn get_backend_option_bool(&self, backend: &str, key: &str) -> Option<bool> {
self.get_backend_option(backend, key)?.parse::<bool>().ok()
}
pub fn set_backend_option(&mut self, backend: String, key: String, value: String) {
self.backend_options
.entry(backend)
.or_default()
.insert(key, value);
}
#[must_use]
pub fn combined_system_prompt(&self) -> Option<String> {
match (
self.system_prompt.as_deref(),
self.dynamic_system_suffix.as_deref(),
) {
(Some(s), Some(suffix)) if !s.is_empty() && !suffix.is_empty() => {
Some(format!("{s}\n\n---\n\n{suffix}"))
},
(Some(s), _) if !s.is_empty() => Some(s.to_string()),
(_, Some(suffix)) if !suffix.is_empty() => Some(suffix.to_string()),
_ => None,
}
}
#[must_use]
pub fn ollama_options(&self) -> OllamaOptions {
OllamaOptions {
num_gpu: self.get_backend_option_i32("ollama", "num_gpu"),
num_thread: self.get_backend_option_i32("ollama", "num_thread"),
num_ctx: self.get_backend_option_i32("ollama", "num_ctx"),
num_predict: self.get_backend_option_i32("ollama", "num_predict"),
numa: self.get_backend_option_bool("ollama", "numa"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OllamaOptions {
pub num_gpu: Option<i32>,
pub num_thread: Option<i32>,
pub num_ctx: Option<i32>,
pub num_predict: Option<i32>,
pub numa: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendConfig {
#[serde(default = "default_ollama_url")]
pub ollama_url: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
#[serde(default = "default_max_idle")]
pub max_idle_per_host: usize,
#[serde(default = "default_ollama_autostart")]
pub ollama_autostart: bool,
}
impl Default for BackendConfig {
fn default() -> Self {
Self {
ollama_url: default_ollama_url(),
timeout_secs: default_timeout(),
max_idle_per_host: default_max_idle(),
ollama_autostart: default_ollama_autostart(),
}
}
}
fn default_temperature() -> f32 {
DEFAULT_TEMPERATURE
}
fn default_max_tokens() -> usize {
0
}
fn default_ollama_url() -> String {
"http://localhost:11434".to_string()
}
fn default_timeout() -> u64 {
10
}
fn default_ollama_autostart() -> bool {
true
}
fn default_max_idle() -> usize {
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_config_defaults_autostart_on_when_key_absent() {
let cfg: BackendConfig = serde_json::from_str(
r#"{"ollama_url":"http://localhost:11434","timeout_secs":5,"max_idle_per_host":2}"#,
)
.expect("parse");
assert!(cfg.ollama_autostart);
assert!(BackendConfig::default().ollama_autostart);
}
}