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}