Skip to main content

byokey_provider/
versions.rs

1//! Remote version/fingerprint config fetched from `assets.byokey.io/versions/`.
2//!
3//! Each provider's CLI version and user-agent string can change with every
4//! upstream release. Instead of hardcoding them, we fetch at startup and
5//! fall back to compile-time defaults if the network is unreachable.
6
7use byokey_types::ProviderId;
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12const BASE_URL: &str = "https://assets.byokey.io/versions";
13
14/// Version/identity info for a single provider.
15#[derive(Debug, Clone, Deserialize)]
16pub struct ProviderVersions {
17    /// CLI release version (e.g. `"2.1.109"`, `"0.120.0"`).
18    #[serde(default)]
19    pub cli_version: Option<String>,
20    /// Full User-Agent header value.
21    #[serde(default)]
22    pub user_agent: Option<String>,
23    /// Stainless SDK package version (`x-stainless-package-version`).
24    #[serde(default)]
25    pub stainless_package_version: Option<String>,
26    /// Stainless runtime version (`x-stainless-runtime-version`).
27    #[serde(default)]
28    pub stainless_runtime_version: Option<String>,
29    /// Copilot: VS Code editor version.
30    #[serde(default)]
31    pub editor_version: Option<String>,
32    /// Copilot: chat plugin version.
33    #[serde(default)]
34    pub plugin_version: Option<String>,
35    /// Copilot: GitHub API version.
36    #[serde(default)]
37    pub github_api_version: Option<String>,
38}
39
40/// Shared, read-only store of provider version info loaded at startup.
41#[derive(Debug, Clone)]
42pub struct VersionStore(Arc<HashMap<ProviderId, ProviderVersions>>);
43
44impl VersionStore {
45    /// Fetch version info for all known providers.
46    ///
47    /// Failures are logged and silently skipped — the store will simply
48    /// be empty for that provider, and callers fall back to compile-time defaults.
49    pub async fn fetch(http: &rquest::Client) -> Self {
50        let providers = [
51            (ProviderId::Claude, "claude"),
52            (ProviderId::Codex, "codex"),
53            (ProviderId::Copilot, "copilot"),
54            (ProviderId::Antigravity, "antigravity"),
55            (ProviderId::Kimi, "kimi"),
56            (ProviderId::Qwen, "qwen"),
57            (ProviderId::IFlow, "iflow"),
58        ];
59
60        let mut map = HashMap::new();
61        for (id, name) in providers {
62            match fetch_one(http, name).await {
63                Ok(v) => {
64                    map.insert(id, v);
65                }
66                Err(e) => {
67                    tracing::debug!(provider = name, %e, "failed to fetch version info, using defaults");
68                }
69            }
70        }
71
72        Self(Arc::new(map))
73    }
74
75    /// Create an empty store (all providers use compile-time defaults).
76    #[must_use]
77    pub fn empty() -> Self {
78        Self(Arc::new(HashMap::new()))
79    }
80
81    /// Look up a provider's version info.
82    #[must_use]
83    pub fn get(&self, provider: &ProviderId) -> Option<&ProviderVersions> {
84        self.0.get(provider)
85    }
86
87    /// Get a specific string field with compile-time fallback.
88    #[must_use]
89    pub fn user_agent(&self, provider: &ProviderId, default: &str) -> String {
90        self.get(provider)
91            .and_then(|v| v.user_agent.as_deref())
92            .unwrap_or(default)
93            .to_string()
94    }
95
96    /// Get CLI version with fallback.
97    #[must_use]
98    pub fn cli_version(&self, provider: &ProviderId, default: &str) -> String {
99        self.get(provider)
100            .and_then(|v| v.cli_version.as_deref())
101            .unwrap_or(default)
102            .to_string()
103    }
104
105    /// Get stainless runtime version with fallback.
106    #[must_use]
107    pub fn stainless_runtime(&self, provider: &ProviderId, default: &str) -> String {
108        self.get(provider)
109            .and_then(|v| v.stainless_runtime_version.as_deref())
110            .unwrap_or(default)
111            .to_string()
112    }
113
114    /// Get stainless package version with fallback.
115    #[must_use]
116    pub fn stainless_package(&self, provider: &ProviderId, default: &str) -> String {
117        self.get(provider)
118            .and_then(|v| v.stainless_package_version.as_deref())
119            .unwrap_or(default)
120            .to_string()
121    }
122}
123
124async fn fetch_one(http: &rquest::Client, provider_name: &str) -> Result<ProviderVersions, String> {
125    let url = format!("{BASE_URL}/{provider_name}.json");
126    let resp = http
127        .get(&url)
128        .send()
129        .await
130        .map_err(|e| format!("fetch failed: {e}"))?;
131    if !resp.status().is_success() {
132        return Err(format!("HTTP {}", resp.status()));
133    }
134    resp.json::<ProviderVersions>()
135        .await
136        .map_err(|e| format!("parse failed: {e}"))
137}