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/// Remote provider configuration.
22///
23/// Any model name is valid — the `standard` field (or auto-detection from
24/// `base_url`) determines which API protocol to use. Local models are handled
25/// by the built-in registry, not by this config.
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct ProviderConfig {
28    /// Model identifier sent to the remote API.
29    pub model: CompactString,
30    /// API key for remote providers. Supports `${ENV_VAR}` expansion at the
31    /// daemon layer.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub api_key: Option<String>,
34    /// Base URL for the remote provider endpoint.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub base_url: Option<String>,
37    /// API protocol standard. Defaults to OpenAI if omitted.
38    #[serde(default)]
39    pub standard: ApiStandard,
40}
41
42impl ProviderConfig {
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    ///
61    /// Called on startup and on provider add/reload.
62    pub fn validate(&self) -> Result<()> {
63        if self.model.is_empty() {
64            bail!("model is required");
65        }
66        // Remote providers: api_key is required unless base_url is set
67        // (e.g. Ollama which is keyless with a local base_url).
68        if self.api_key.is_none() && self.base_url.is_none() {
69            bail!(
70                "remote provider '{}' requires api_key or base_url",
71                self.model
72            );
73        }
74        Ok(())
75    }
76}