Skip to main content

walrus_core/config/
provider.rs

1//! Remote provider configuration.
2
3use anyhow::{Result, bail};
4use compact_str::CompactString;
5use serde::{Deserialize, Serialize};
6
7/// API protocol standard for remote providers.
8///
9/// Only two wire formats exist: OpenAI-compatible and Anthropic.
10/// Defaults to `OpenAI` when omitted in config.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum ApiStandard {
14    /// OpenAI-compatible chat completions API (covers DeepSeek, Grok, Qwen, Kimi, Ollama, etc.).
15    #[default]
16    OpenAI,
17    /// Anthropic Messages API.
18    Anthropic,
19}
20
21/// Provider definition — credentials and a list of models served.
22///
23/// Each `[provider.<name>]` in TOML becomes one `ProviderDef`. The TOML key
24/// is the provider name (not stored in the struct). Multiple models share
25/// the same credentials and endpoint.
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ProviderDef {
28    /// API key. Supports `${ENV_VAR}` expansion at the daemon layer.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub api_key: Option<String>,
31    /// Base URL for the provider endpoint.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub base_url: Option<String>,
34    /// API protocol standard. Defaults to OpenAI if omitted.
35    #[serde(default)]
36    pub standard: ApiStandard,
37    /// Model names served by this provider.
38    #[serde(default)]
39    pub models: Vec<CompactString>,
40}
41
42impl ProviderDef {
43    /// Resolve the effective API standard.
44    ///
45    /// Returns `Anthropic` if the field is explicitly set to `Anthropic`,
46    /// or if `base_url` contains "anthropic". Otherwise `OpenAI`.
47    pub fn effective_standard(&self) -> ApiStandard {
48        if self.standard == ApiStandard::Anthropic {
49            return ApiStandard::Anthropic;
50        }
51        if let Some(url) = &self.base_url
52            && url.contains("anthropic")
53        {
54            return ApiStandard::Anthropic;
55        }
56        ApiStandard::OpenAI
57    }
58
59    /// Validate field combinations.
60    pub fn validate(&self, provider_name: &str) -> Result<()> {
61        if self.models.is_empty() {
62            bail!("provider '{provider_name}' has no models");
63        }
64        // api_key is required unless base_url is set (e.g. Ollama).
65        if self.api_key.is_none() && self.base_url.is_none() {
66            bail!("provider '{provider_name}' requires api_key or base_url");
67        }
68        Ok(())
69    }
70}