1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7pub(super) const DEFAULT_PROFILE_NAME: &str = "default";
8const DEFAULT_CONNECT_TIMEOUT_SECONDS: f64 = 30.0;
9const DEFAULT_ANTHROPIC_BASE_URL: &str = "https://api.anthropic.com";
10const DEFAULT_CODEAGENT_BASE_URL: &str = "https://codeagentcli.rnd.huawei.com/codeAgentPro";
11const DEFAULT_MAAS_BASE_URL: &str =
12 "http://snapengine.cida.cce.prod-szv-g.dragon.tools.huawei.com/api/v2/";
13
14pub(super) fn default_temperature() -> f64 {
15 0.7
16}
17
18pub(super) fn default_top_p() -> f64 {
19 1.0
20}
21
22pub(super) fn default_connect_timeout_seconds() -> f64 {
23 DEFAULT_CONNECT_TIMEOUT_SECONDS
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ModelProviderKind {
30 #[serde(rename = "openai_compatible")]
31 OpenAiCompatible,
32 Anthropic,
33 Bigmodel,
34 Minimax,
35 Maas,
36 Codeagent,
37 Echo,
38}
39
40impl ModelProviderKind {
41 pub const fn as_str(self) -> &'static str {
42 match self {
43 Self::OpenAiCompatible => "openai_compatible",
44 Self::Anthropic => "anthropic",
45 Self::Bigmodel => "bigmodel",
46 Self::Minimax => "minimax",
47 Self::Maas => "maas",
48 Self::Codeagent => "codeagent",
49 Self::Echo => "echo",
50 }
51 }
52
53 pub(super) const fn default_base_url(self) -> Option<&'static str> {
54 match self {
55 Self::Anthropic => Some(DEFAULT_ANTHROPIC_BASE_URL),
56 Self::Codeagent => Some(DEFAULT_CODEAGENT_BASE_URL),
57 Self::Maas => Some(DEFAULT_MAAS_BASE_URL),
58 Self::Echo => Some("http://127.0.0.1/echo"),
59 Self::OpenAiCompatible | Self::Bigmodel | Self::Minimax => None,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct ModelRequestHeader {
67 pub name: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub value: Option<String>,
70 #[serde(default)]
71 pub secret: bool,
72 #[serde(default)]
73 pub configured: bool,
74}
75
76impl ModelRequestHeader {
77 fn redacted(&self) -> Self {
78 Self {
79 name: self.name.clone(),
80 value: (!self.secret).then(|| self.value.clone()).flatten(),
81 secret: self.secret,
82 configured: self.configured || self.value.is_some(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
89pub struct ModelCapabilities {
90 #[serde(default)]
91 pub input: ModelModalityMatrix,
92 #[serde(default)]
93 pub output: ModelModalityMatrix,
94}
95
96#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
98pub struct ModelModalityMatrix {
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub text: Option<bool>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub image: Option<bool>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub audio: Option<bool>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub video: Option<bool>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub pdf: Option<bool>,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct ModelProfileSaveRequest {
114 pub provider: ModelProviderKind,
115 pub model: String,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub base_url: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub api_key: Option<String>,
120 #[serde(default)]
121 pub clear_api_key: bool,
122 #[serde(default)]
123 pub headers: Vec<ModelRequestHeader>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub ssl_verify: Option<bool>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub context_window: Option<u32>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub max_tokens: Option<u32>,
130 #[serde(default = "default_temperature")]
131 pub temperature: f64,
132 #[serde(default = "default_top_p")]
133 pub top_p: f64,
134 #[serde(default = "default_connect_timeout_seconds")]
135 pub connect_timeout_seconds: f64,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub capabilities: Option<ModelCapabilities>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub fallback_policy_id: Option<String>,
140 #[serde(default)]
141 pub fallback_priority: u32,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub catalog_provider_id: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub catalog_provider_name: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub catalog_model_name: Option<String>,
148 #[serde(default)]
149 pub is_default: bool,
150}
151
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
154pub struct ModelProfileView {
155 pub name: String,
156 pub provider: ModelProviderKind,
157 pub model: String,
158 pub base_url: String,
159 pub api_key_configured: bool,
160 pub headers: Vec<ModelRequestHeader>,
161 pub ssl_verify: Option<bool>,
162 pub context_window: Option<u32>,
163 pub max_tokens: Option<u32>,
164 pub temperature: f64,
165 pub top_p: f64,
166 pub connect_timeout_seconds: f64,
167 pub capabilities: ModelCapabilities,
168 pub fallback_policy_id: Option<String>,
169 pub fallback_priority: u32,
170 pub catalog_provider_id: Option<String>,
171 pub catalog_provider_name: Option<String>,
172 pub catalog_model_name: Option<String>,
173 pub is_default: bool,
174 pub source: String,
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178pub(super) struct StoredModelProfile {
179 pub(super) provider: ModelProviderKind,
180 pub(super) model: String,
181 pub(super) base_url: String,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub(super) api_key: Option<String>,
184 #[serde(default)]
185 pub(super) headers: Vec<ModelRequestHeader>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub(super) ssl_verify: Option<bool>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub(super) context_window: Option<u32>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub(super) max_tokens: Option<u32>,
192 pub(super) temperature: f64,
193 pub(super) top_p: f64,
194 pub(super) connect_timeout_seconds: f64,
195 #[serde(default)]
196 pub(super) capabilities: ModelCapabilities,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub(super) fallback_policy_id: Option<String>,
199 pub(super) fallback_priority: u32,
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub(super) catalog_provider_id: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub(super) catalog_provider_name: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub(super) catalog_model_name: Option<String>,
206 #[serde(default)]
207 pub(super) is_default: bool,
208 pub(super) source: String,
209}
210
211impl StoredModelProfile {
212 pub(super) fn to_view(&self, name: &str, is_default: bool) -> ModelProfileView {
213 ModelProfileView {
214 name: name.to_owned(),
215 provider: self.provider,
216 model: self.model.clone(),
217 base_url: redacted_url(&self.base_url),
218 api_key_configured: self.api_key.is_some(),
219 headers: self
220 .headers
221 .iter()
222 .map(ModelRequestHeader::redacted)
223 .collect(),
224 ssl_verify: self.ssl_verify,
225 context_window: self.context_window,
226 max_tokens: self.max_tokens,
227 temperature: self.temperature,
228 top_p: self.top_p,
229 connect_timeout_seconds: self.connect_timeout_seconds,
230 capabilities: self.capabilities.clone(),
231 fallback_policy_id: self.fallback_policy_id.clone(),
232 fallback_priority: self.fallback_priority,
233 catalog_provider_id: self.catalog_provider_id.clone(),
234 catalog_provider_name: self.catalog_provider_name.clone(),
235 catalog_model_name: self.catalog_model_name.clone(),
236 is_default,
237 source: self.source.clone(),
238 }
239 }
240}
241
242pub(super) fn redacted_url(value: &str) -> String {
243 let Some((scheme, rest)) = value.split_once("://") else {
244 return value.to_owned();
245 };
246 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
247 let (authority, suffix) = rest.split_at(authority_end);
248 authority
249 .rsplit_once('@')
250 .map(|(_, host)| format!("{scheme}://{host}{suffix}"))
251 .unwrap_or_else(|| value.to_owned())
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256pub struct ModelProfilesResponse {
257 pub loaded: bool,
258 pub default_profile: Option<String>,
259 pub profiles: Vec<ModelProfileView>,
260 pub error: Option<String>,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265pub struct ModelProfileRuntimeSummary {
266 pub loaded: bool,
267 pub profile_count: usize,
268 pub default_profile: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub error: Option<String>,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274pub(super) struct StoredProfileFile {
275 pub(super) default_profile: Option<String>,
276 pub(super) profiles: BTreeMap<String, StoredModelProfile>,
277}
278
279#[cfg(test)]
280#[path = "profile_tests.rs"]
281mod tests;