1use crate::provider::{Key, ProviderEntry, ProviderKind, Tag, TagError, ask};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Problem {
16 Tag(TagError),
18 Taken(String),
20 NoBase,
22 BaseNotAnAddress(String),
24 BaseUnusable(String),
27 NoKey(ProviderKind),
29}
30
31impl Problem {
32 #[must_use]
34 pub fn field(&self) -> &'static str {
35 match self {
36 Self::Tag(_) | Self::Taken(_) => TAG_FIELD,
37 Self::NoBase | Self::BaseNotAnAddress(_) | Self::BaseUnusable(_) => BASE_FIELD,
38 Self::NoKey(_) => KEY_FIELD,
39 }
40 }
41}
42
43pub const TAG_FIELD: &str = "provider-tag";
45
46pub const BASE_FIELD: &str = "provider-base";
48
49pub const KEY_FIELD: &str = "provider-key";
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Draft {
55 pub tag: String,
57 pub kind: ProviderKind,
59 pub base: String,
61 pub key: String,
64 pub problem: Option<Problem>,
67}
68
69impl Draft {
70 #[must_use]
73 pub fn new() -> Self {
74 let kind = ProviderKind::ALL[0];
75 Self { tag: String::new(), kind, base: kind.suggested_base().to_owned(), key: String::new(), problem: None }
76 }
77
78 pub fn pick_kind(&mut self, kind: ProviderKind) {
82 if self.offered_base() {
83 self.base = kind.suggested_base().to_owned();
84 }
85 let tag = self.tag.trim();
86 if tag.is_empty() || self.kind.suggested_tag() == Some(tag) {
87 self.tag = kind.suggested_tag().unwrap_or_default().to_owned();
88 }
89 self.kind = kind;
90 self.problem = None;
91 }
92
93 pub fn pick_region(&mut self, index: usize) {
97 if let Some(region) = self.kind.regions().get(index) {
98 self.base = region.base.to_owned();
99 self.problem = None;
100 }
101 }
102
103 #[must_use]
106 pub fn region(&self) -> Option<usize> {
107 let base = crate::provider::trim_base(&self.base);
108 self.kind.regions().iter().position(|region| region.base == base)
109 }
110
111 fn offered_base(&self) -> bool {
113 self.base == self.kind.suggested_base() || self.region().is_some()
114 }
115
116 pub fn build(&self, taken: impl Fn(&str) -> bool) -> Result<ProviderEntry, Problem> {
124 let tag = Tag::parse(self.tag.trim()).map_err(Problem::Tag)?;
125 if taken(tag.as_str()) {
126 return Err(Problem::Taken(tag.as_str().to_owned()));
127 }
128 let base = self.base.trim();
129 if base.is_empty() {
130 return Err(Problem::NoBase);
131 }
132 if !(base.starts_with("http://") || base.starts_with("https://")) {
136 return Err(Problem::BaseNotAnAddress(base.to_owned()));
137 }
138 let key = Key::new(&self.key);
139 if self.kind.needs_key() && key.is_none() {
140 return Err(Problem::NoKey(self.kind));
141 }
142 let mut entry = ProviderEntry::new(tag, self.kind, base);
143 if !ask::can_be_sent_to(&entry.messages_address()) {
146 return Err(Problem::BaseUnusable(base.to_owned()));
147 }
148 entry.key = key;
149 Ok(entry)
150 }
151}
152
153impl Default for Draft {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn filled(tag: &str, kind: ProviderKind, base: &str, key: &str) -> Draft {
164 Draft { tag: tag.to_owned(), kind, base: base.to_owned(), key: key.to_owned(), problem: None }
165 }
166
167 #[test]
168 fn a_filled_form_becomes_a_provider() {
169 let draft = filled("ev", ProviderKind::Ollama, " http://192.168.122.1:11434/ ", "");
170 let entry = draft.build(|_| false).expect("the form is filled");
171 assert_eq!(entry.tag.as_str(), "ev");
172 assert_eq!(entry.base, "http://192.168.122.1:11434", "the address is tidied as it is taken");
173 assert_eq!(entry.key, None);
174 }
175
176 #[test]
177 fn a_tag_that_is_already_there_is_refused_beside_the_tag_field() {
178 let draft = filled("ev", ProviderKind::Ollama, "http://h:1", "");
179 let problem = draft.build(|tag| tag == "ev").expect_err("the tag is taken");
180 assert_eq!(problem, Problem::Taken("ev".to_owned()));
181 assert_eq!(problem.field(), TAG_FIELD);
182 }
183
184 #[test]
185 fn a_tag_that_could_not_stand_in_front_of_a_model_is_refused_with_its_reason() {
186 let draft = filled("ev/makine", ProviderKind::Ollama, "http://h:1", "");
187 let problem = draft.build(|_| false).expect_err("a slash is not a tag");
188 assert_eq!(problem, Problem::Tag(TagError::Illegal { position: 3, character: '/' }));
189 assert_eq!(problem.field(), TAG_FIELD);
190 }
191
192 #[test]
193 fn an_address_with_no_scheme_is_named_now_rather_than_failing_for_no_visible_reason_later() {
194 let draft = filled("ev", ProviderKind::Ollama, "192.168.122.1:11434", "");
195 let problem = draft.build(|_| false).expect_err("that cannot be reached");
196 assert_eq!(problem, Problem::BaseNotAnAddress("192.168.122.1:11434".to_owned()));
197 assert_eq!(problem.field(), BASE_FIELD);
198 assert_eq!(filled("ev", ProviderKind::Ollama, " ", "").build(|_| false), Err(Problem::NoBase));
199 }
200
201 #[test]
202 fn two_addresses_run_together_are_refused_by_the_parser_the_requests_use() {
203 let joined = "http://127.0.0.1:11434http://192.168.122.1:11434";
204 let problem =
205 filled("ev", ProviderKind::Ollama, joined, "").build(|_| false).expect_err("no request goes there");
206 assert_eq!(problem, Problem::BaseUnusable(joined.to_owned()));
207 assert_eq!(problem.field(), BASE_FIELD);
208 for broken in ["http://", "http://ev makine:11434", "https://[::1"] {
209 assert!(
210 matches!(
211 filled("ev", ProviderKind::Ollama, broken, "").build(|_| false),
212 Err(Problem::BaseUnusable(_))
213 ),
214 "{broken}"
215 );
216 }
217 assert!(filled("ev", ProviderKind::Ollama, "http://[::1]:11434", "").build(|_| false).is_ok());
218 }
219
220 #[test]
221 fn a_kind_that_is_reached_with_a_key_is_not_added_without_one() {
222 let without = filled("yol", ProviderKind::OpenRouter, "https://openrouter.ai", "");
223 assert_eq!(without.build(|_| false), Err(Problem::NoKey(ProviderKind::OpenRouter)));
224 assert_eq!(Problem::NoKey(ProviderKind::OpenRouter).field(), KEY_FIELD);
225 let with = filled("yol", ProviderKind::OpenRouter, "https://openrouter.ai", "not-a-real-key-0000-wxyz");
226 let entry = with.build(|_| false).expect("the key is there");
227 assert_eq!(entry.key.expect("a key").last_four().as_deref(), Some("wxyz"));
228 }
229
230 #[test]
231 fn changing_the_kind_moves_an_address_nobody_typed_and_leaves_one_they_did() {
232 let mut draft = Draft::new();
233 assert_eq!(draft.kind, ProviderKind::Ollama);
234 draft.pick_kind(ProviderKind::OpenRouter);
235 assert_eq!(draft.base, ProviderKind::OpenRouter.suggested_base(), "the offer follows the kind");
236
237 let mut own = Draft::new();
238 own.base = "http://192.168.122.1:11434".to_owned();
239 own.pick_kind(ProviderKind::OpenRouter);
240 assert_eq!(own.base, "http://192.168.122.1:11434", "an address the person typed is theirs");
241 }
242}