byokey_provider/
versions.rs1use 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#[derive(Debug, Clone, Deserialize)]
16pub struct ProviderVersions {
17 #[serde(default)]
19 pub cli_version: Option<String>,
20 #[serde(default)]
22 pub user_agent: Option<String>,
23 #[serde(default)]
25 pub stainless_package_version: Option<String>,
26 #[serde(default)]
28 pub stainless_runtime_version: Option<String>,
29 #[serde(default)]
31 pub editor_version: Option<String>,
32 #[serde(default)]
34 pub plugin_version: Option<String>,
35 #[serde(default)]
37 pub github_api_version: Option<String>,
38 #[serde(default)]
40 pub opencode_version: Option<String>,
41 #[serde(default)]
43 pub opencode_api_version: Option<String>,
44}
45
46#[derive(Debug, Clone)]
48pub struct VersionStore(Arc<HashMap<ProviderId, ProviderVersions>>);
49
50impl VersionStore {
51 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 #[must_use]
83 pub fn empty() -> Self {
84 Self(Arc::new(HashMap::new()))
85 }
86
87 #[must_use]
89 pub fn get(&self, provider: &ProviderId) -> Option<&ProviderVersions> {
90 self.0.get(provider)
91 }
92
93 #[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 #[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 #[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 #[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}