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    /// Copilot: `OpenCode` release the `OpenCode` client claims to be.
39    #[serde(default)]
40    pub opencode_version: Option<String>,
41    /// Copilot: GitHub API version `OpenCode` pins.
42    #[serde(default)]
43    pub opencode_api_version: Option<String>,
44}
45
46/// Shared, read-only store of provider version info loaded at startup.
47#[derive(Debug, Clone)]
48pub struct VersionStore(Arc<HashMap<ProviderId, ProviderVersions>>);
49
50impl VersionStore {
51    /// Fetch version info for all known providers.
52    ///
53    /// Failures are logged and silently skipped — the store will simply
54    /// be empty for that provider, and callers fall back to compile-time defaults.
55    pub async fn fetch(http: &wreq::Client) -> Self {
56        let providers = [
57            (ProviderId::Claude, "claude"),
58            (ProviderId::Codex, "codex"),
59            (ProviderId::Copilot, "copilot"),
60            (ProviderId::Antigravity, "antigravity"),
61            (ProviderId::Kimi, "kimi"),
62            (ProviderId::Qwen, "qwen"),
63            (ProviderId::IFlow, "iflow"),
64        ];
65
66        let mut map = HashMap::new();
67        for (id, name) in providers {
68            match fetch_one(http, name).await {
69                Ok(v) => {
70                    map.insert(id, v);
71                }
72                Err(e) => {
73                    tracing::debug!(provider = name, %e, "failed to fetch version info, using defaults");
74                }
75            }
76        }
77
78        Self(Arc::new(map))
79    }
80
81    /// Create an empty store (all providers use compile-time defaults).
82    #[must_use]
83    pub fn empty() -> Self {
84        Self(Arc::new(HashMap::new()))
85    }
86
87    /// Look up a provider's version info.
88    #[must_use]
89    pub fn get(&self, provider: &ProviderId) -> Option<&ProviderVersions> {
90        self.0.get(provider)
91    }
92
93    /// Get a specific string field with compile-time fallback.
94    #[must_use]
95    pub fn user_agent(&self, provider: &ProviderId, default: &str) -> String {
96        self.get(provider)
97            .and_then(|v| v.user_agent.as_deref())
98            .unwrap_or(default)
99            .to_string()
100    }
101
102    /// Get CLI version with fallback.
103    #[must_use]
104    pub fn cli_version(&self, provider: &ProviderId, default: &str) -> String {
105        self.get(provider)
106            .and_then(|v| v.cli_version.as_deref())
107            .unwrap_or(default)
108            .to_string()
109    }
110
111    /// Get stainless runtime version with fallback.
112    #[must_use]
113    pub fn stainless_runtime(&self, provider: &ProviderId, default: &str) -> String {
114        self.get(provider)
115            .and_then(|v| v.stainless_runtime_version.as_deref())
116            .unwrap_or(default)
117            .to_string()
118    }
119
120    /// Get stainless package version with fallback.
121    #[must_use]
122    pub fn stainless_package(&self, provider: &ProviderId, default: &str) -> String {
123        self.get(provider)
124            .and_then(|v| v.stainless_package_version.as_deref())
125            .unwrap_or(default)
126            .to_string()
127    }
128}
129
130async fn fetch_one(http: &wreq::Client, provider_name: &str) -> Result<ProviderVersions, String> {
131    let url = format!("{BASE_URL}/{provider_name}.json");
132    let resp = http
133        .get(&url)
134        .send()
135        .await
136        .map_err(|e| format!("fetch failed: {e}"))?;
137    if !resp.status().is_success() {
138        return Err(format!("HTTP {}", resp.status()));
139    }
140    resp.json::<ProviderVersions>()
141        .await
142        .map_err(|e| format!("parse failed: {e}"))
143}