Skip to main content

qcode/provider/
record.rs

1//! What a provider is: the tag that names it, the kind of service it is, where it answers, the
2//! shape it speaks, what it can run and the key it is reached with.
3
4use super::{Key, Tag};
5
6/// A kind of provider QCode knows how to ask.
7///
8/// Every one of them speaks the Anthropic message shape at `/v1/messages` and the OpenAI one at
9/// `/v1/chat/completions`, under [`ProviderKind::api_root`], which is why a harness can be
10/// pointed straight at them in its own shape and no translating endpoint is built.
11///
12/// The last two are ready-made: picking one fills in where it answers, where its API sits for
13/// each shape, which header its key goes in and the models it offers, so the person types their
14/// key and nothing else. Each of those facts was asked of the real service with a real key
15/// (2026-09-23) and the addresses are the ones the maker's own pages name.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ProviderKind {
18    /// An ollama server, the person's own or one on their network.
19    Ollama,
20    /// OpenRouter.
21    OpenRouter,
22    /// Xiaomi MiMo's Token Plan, the subscription. Its keys (`tp-…`) are not the pay-as-you-go
23    /// account's (`sk-…` at `api.xiaomimimo.com`) and neither is accepted by the other's address.
24    MimoTokenPlan,
25    /// Kimi Code, Moonshot's coding subscription.
26    KimiCode,
27}
28
29/// The shape of the request a provider is sent, and of the answer that comes back.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Wire {
32    /// The Anthropic message shape: `/v1/messages`, and `usage.input_tokens` in the answer.
33    Anthropic,
34    /// The OpenAI completion shape: `/v1/chat/completions`, and `usage.prompt_tokens`.
35    OpenAi,
36}
37
38/// One of the addresses a ready-made provider answers at, which is a choice of where in the world
39/// the person's subscription lives rather than of anything they could type better themselves.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct Region {
42    /// How the region is named in the language files, under `provider.region-`.
43    pub id: &'static str,
44    /// The address, as the maker's own page writes it without the path of either shape.
45    pub base: &'static str,
46}
47
48/// A model a ready-made provider offers, as its maker publishes it: the window it gives and the
49/// page that says so, so that a figure QCode did not ask the service for always says where it
50/// was read.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct Published {
53    /// The model's name, as the service takes it.
54    pub id: &'static str,
55    /// The window the maker publishes for it.
56    pub window: u64,
57    /// Where that was read.
58    pub source: &'static str,
59}
60
61/// Xiaomi's own model page gives every one of these "Context Window: 1M", and its own opencode
62/// instructions write that as 1 048 576. The service's `/v1/models` names the models and no
63/// window, so this is the only honest figure there is.
64const MIMO_MODELS: &str = "https://mimo.mi.com/static/docs/quick-start/summary/model.md";
65
66/// Kimi's own Claude Code page, whose table gives each model's window by plan. `k3` is 262 144 on
67/// the plans below Pro and 1 048 576 above; the smaller is written, because it is the one every
68/// plan that has the model gets. The service's `/v1/models` gives the window itself for the
69/// models it lists, and that answer is taken over this one whenever it is there.
70const KIMI_MODELS: &str = "https://www.kimi.com/code/docs/en/third-party-tools/claude-code";
71
72const MIMO_PUBLISHED: [Published; 4] = [
73    Published { id: "mimo-v2.6-pro", window: 1_048_576, source: MIMO_MODELS },
74    Published { id: "mimo-v2.6-flash", window: 1_048_576, source: MIMO_MODELS },
75    Published { id: "mimo-v2.5-pro", window: 1_048_576, source: MIMO_MODELS },
76    Published { id: "mimo-v2.5", window: 1_048_576, source: MIMO_MODELS },
77];
78
79const KIMI_PUBLISHED: [Published; 4] = [
80    Published { id: "kimi-for-coding", window: 1_048_576, source: KIMI_MODELS },
81    Published { id: "k3", window: 262_144, source: KIMI_MODELS },
82    Published { id: "k3-256k", window: 262_144, source: KIMI_MODELS },
83    Published { id: "kimi-for-coding-highspeed", window: 262_144, source: KIMI_MODELS },
84];
85
86/// The Token Plan's three clusters, from Xiaomi's Token Plan quick-access page
87/// (`mimo.mi.com/static/docs/tokenplan/Token Plan/quick-access.md`). A key works only at the
88/// cluster the subscription page names, so the person picks the one they were shown.
89const MIMO_REGIONS: [Region; 3] = [
90    Region { id: "europe", base: "https://token-plan-ams.xiaomimimo.com" },
91    Region { id: "singapore", base: "https://token-plan-sgp.xiaomimimo.com" },
92    Region { id: "china", base: "https://token-plan-cn.xiaomimimo.com" },
93];
94
95/// Kimi Code's two addresses, from its own overview page (`kimi.com/code/docs/en/`), which calls
96/// the first China's and the second the one for everywhere else. The same key was accepted at
97/// both.
98const KIMI_REGIONS: [Region; 2] =
99    [Region { id: "china", base: "https://api.kimi.com" }, Region { id: "overseas", base: "https://api.kimi.ai" }];
100
101impl ProviderKind {
102    /// Every kind, in the order the page offers them.
103    pub const ALL: [Self; 4] = [Self::Ollama, Self::OpenRouter, Self::MimoTokenPlan, Self::KimiCode];
104
105    /// How the kind is written in the providers file.
106    #[must_use]
107    pub fn id(self) -> &'static str {
108        match self {
109            Self::Ollama => "ollama",
110            Self::OpenRouter => "openrouter",
111            Self::MimoTokenPlan => "mimo-token-plan",
112            Self::KimiCode => "kimi-code",
113        }
114    }
115
116    /// The kind written as `id`, if there is one.
117    #[must_use]
118    pub fn parse(id: &str) -> Option<Self> {
119        Self::ALL.into_iter().find(|kind| kind.id() == id)
120    }
121
122    /// The address the page offers when this kind is picked. An ollama server is usually the
123    /// machine QCode runs on; the others are the service's own, and for a ready-made one the
124    /// first of its [`regions`](Self::regions).
125    #[must_use]
126    pub fn suggested_base(self) -> &'static str {
127        match self {
128            Self::Ollama => "http://127.0.0.1:11434",
129            Self::OpenRouter => "https://openrouter.ai",
130            Self::MimoTokenPlan => MIMO_REGIONS[0].base,
131            Self::KimiCode => KIMI_REGIONS[0].base,
132        }
133    }
134
135    /// The tag the page offers for a ready-made kind, so that a person who only has a key to
136    /// give is not stopped by a name they had no reason to think about. Kinds whose address is
137    /// the person's own offer none: two ollama servers are what a tag tells apart.
138    #[must_use]
139    pub fn suggested_tag(self) -> Option<&'static str> {
140        match self {
141            Self::Ollama | Self::OpenRouter => None,
142            Self::MimoTokenPlan => Some("mimo"),
143            Self::KimiCode => Some("kimi"),
144        }
145    }
146
147    /// The addresses a ready-made kind can be picked at; empty for a kind whose address is
148    /// written by hand.
149    #[must_use]
150    pub fn regions(self) -> &'static [Region] {
151        match self {
152            Self::Ollama | Self::OpenRouter => &[],
153            Self::MimoTokenPlan => &MIMO_REGIONS,
154            Self::KimiCode => &KIMI_REGIONS,
155        }
156    }
157
158    /// Whether this kind is reached with a key. An ollama server on the person's own network
159    /// asks for none, which is why a profile on one carries no secret into a container at all.
160    #[must_use]
161    pub fn needs_key(self) -> bool {
162        match self {
163            Self::Ollama => false,
164            Self::OpenRouter | Self::MimoTokenPlan | Self::KimiCode => true,
165        }
166    }
167
168    /// The shape this kind speaks by default.
169    #[must_use]
170    pub fn wire(self) -> Wire {
171        Wire::Anthropic
172    }
173
174    /// The header a key goes in, and what stands in front of it there.
175    ///
176    /// Each is the header the maker's own instructions name: Xiaomi's show `api-key`, Kimi's
177    /// Claude Code page sets `ANTHROPIC_API_KEY`, which Claude Code sends as `x-api-key`. Both
178    /// services were seen to accept `Authorization: Bearer` too, in both shapes, but the header a
179    /// service documents is the one it will keep accepting.
180    #[must_use]
181    pub fn key_header(self) -> (&'static str, &'static str) {
182        match self {
183            Self::Ollama | Self::OpenRouter => ("Authorization", "Bearer "),
184            Self::MimoTokenPlan => ("api-key", ""),
185            Self::KimiCode => ("x-api-key", ""),
186        }
187    }
188
189    /// Where this kind's API sits under the address a person knows it by, for a request of
190    /// shape `wire` — which is not always the address itself, and not always the same place for
191    /// both shapes.
192    ///
193    /// OpenRouter is known as `https://openrouter.ai`, and that is the address its page names,
194    /// but its API answers under `/api`: `https://openrouter.ai/api/v1/messages`. The same path
195    /// without it is the website, which answers a message with `200` and a page of HTML — a
196    /// harness reading that as a model's answer is told nothing true about what went wrong. An
197    /// ollama server answers at its own root. Xiaomi's Token Plan answers the Anthropic shape
198    /// under `/anthropic` and the OpenAI one at its root, and its `/anthropic/v1/models` is a
199    /// `404`; Kimi Code answers both under `/coding`.
200    #[must_use]
201    pub fn api_root(self, wire: Wire) -> &'static str {
202        match (self, wire) {
203            (Self::Ollama, _) | (Self::MimoTokenPlan, Wire::OpenAi) => "",
204            (Self::OpenRouter, _) => "/api",
205            (Self::MimoTokenPlan, Wire::Anthropic) => "/anthropic",
206            (Self::KimiCode, _) => "/coding",
207        }
208    }
209
210    /// The address `path` of this kind's API sits at under `base`, for a request of shape
211    /// `wire`: under [`api_root`](Self::api_root), whichever of the kind's roots `base` was
212    /// written with. OpenRouter's own instructions for Claude Code name
213    /// `https://openrouter.ai/api` as the base, and Xiaomi's name `…/anthropic` for one shape and
214    /// `…/v1` for the other, so a person who copied any of those must be sent neither to
215    /// `/api/api` nor, asking in the other shape, to `/anthropic/v1/chat/completions`.
216    #[must_use]
217    pub fn api_address(self, base: &str, wire: Wire, path: &str) -> String {
218        let base = trim_base(base);
219        let mut endings: Vec<String> = Wire::ALL
220            .iter()
221            .map(|wire| self.api_root(*wire))
222            .flat_map(|root| [format!("{root}/v1"), root.to_owned()])
223            .filter(|ending| !ending.is_empty())
224            .collect();
225        // The longest first, so `/anthropic/v1` is taken off whole rather than leaving
226        // `/anthropic` behind; `/v1` is taken off too, because the path brings it again.
227        endings.sort_by_key(|ending| std::cmp::Reverse(ending.len()));
228        let host = endings.iter().find_map(|ending| base.strip_suffix(ending.as_str())).unwrap_or(&base);
229        format!("{host}{}{path}", self.api_root(wire))
230    }
231
232    /// The models a ready-made kind is known to offer, each with the window its maker publishes
233    /// and where that was read. Empty for a kind whose models are only known by asking.
234    #[must_use]
235    pub fn published(self) -> &'static [Published] {
236        match self {
237            Self::Ollama | Self::OpenRouter => &[],
238            Self::MimoTokenPlan => &MIMO_PUBLISHED,
239            Self::KimiCode => &KIMI_PUBLISHED,
240        }
241    }
242
243    /// Whether the window this kind claims for a model is the window a harness gets.
244    ///
245    /// Not for an ollama server: its record said 262 144 while its endpoints cut prompts at
246    /// about three thousand, which is why only a measured window is handed to a harness there.
247    /// A ready-made service is the maker's own endpoint, and what it or its maker says it gives
248    /// is what it was built to give; a harness told nothing would assume a window of its own
249    /// instead, which is a guess about somebody else's model.
250    #[must_use]
251    pub fn claims_are_served(self) -> bool {
252        matches!(self, Self::MimoTokenPlan | Self::KimiCode)
253    }
254}
255
256impl Wire {
257    /// Every shape, in the order the page offers them.
258    pub const ALL: [Self; 2] = [Self::Anthropic, Self::OpenAi];
259
260    /// How the shape is written in the providers file.
261    #[must_use]
262    pub fn id(self) -> &'static str {
263        match self {
264            Self::Anthropic => "anthropic",
265            Self::OpenAi => "openai",
266        }
267    }
268
269    /// The shape written as `id`, if there is one.
270    #[must_use]
271    pub fn parse(id: &str) -> Option<Self> {
272        Self::ALL.into_iter().find(|wire| wire.id() == id)
273    }
274
275    /// Where a message is sent, under the provider's base address.
276    #[must_use]
277    pub fn messages_path(self) -> &'static str {
278        match self {
279            Self::Anthropic => "/v1/messages",
280            Self::OpenAi => "/v1/chat/completions",
281        }
282    }
283
284    /// The shape a request for `path` is in, judged by the path a harness asked for. A model
285    /// listing is asked of the OpenAI root: both shapes name it `/v1/models`, and the one
286    /// service whose roots differ serves it only there.
287    #[must_use]
288    pub fn of_path(path: &str) -> Self {
289        let path = path.split('?').next().unwrap_or(path);
290        if path == Self::Anthropic.messages_path() { Self::Anthropic } else { Self::OpenAi }
291    }
292}
293
294/// What a server was seen to accept, as opposed to what its model claims.
295///
296/// The two are not the same thing and the difference is the point: an ollama server answered for
297/// a model whose own record says 262 144 tokens and then silently threw away everything past
298/// about three thousand. A window is only ever reported as [`About`](Measured::About) when
299/// growth was seen to stop; when the largest probe still grew, all that is known is that the
300/// window is at least that big, and saying more would be inventing a number.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum Measured {
303    /// Growth stopped here: prompts larger than this are cut, whatever the model claims.
304    About(u64),
305    /// Every probe was taken whole, so the window is at least this and its edge was not found.
306    AtLeast(u64),
307}
308
309impl Measured {
310    /// The number itself.
311    #[must_use]
312    pub fn tokens(self) -> u64 {
313        match self {
314            Self::About(tokens) | Self::AtLeast(tokens) => tokens,
315        }
316    }
317
318    /// How it is written in the providers file.
319    #[must_use]
320    pub fn id(self) -> &'static str {
321        match self {
322            Self::About(_) => "about",
323            Self::AtLeast(_) => "at-least",
324        }
325    }
326
327    /// The measurement of `tokens` written as `id`, if there is one.
328    #[must_use]
329    pub fn parse(id: &str, tokens: u64) -> Option<Self> {
330        match id {
331            "about" => Some(Self::About(tokens)),
332            "at-least" => Some(Self::AtLeast(tokens)),
333            _ => None,
334        }
335    }
336
337    /// Whether a window this size leaves a coding agent enough room to work.
338    ///
339    /// A harness sends its own instructions, the files that are open and the conversation so
340    /// far. Below this the instructions alone no longer fit, the rest is thrown away without a
341    /// word, and the agent behaves as though it never saw the files.
342    #[must_use]
343    pub fn is_cramped(self) -> bool {
344        matches!(self, Self::About(tokens) if tokens < CRAMPED)
345    }
346}
347
348/// The window below which a coding agent loses its own instructions. Measured against the
349/// owner's server, whose compatibility endpoints pinned the window to 4096 and cut prompts at
350/// about 2 050 and 3 012 input tokens.
351pub const CRAMPED: u64 = 16_384;
352
353/// What a model costs: nothing to use, or money out of the account it is reached with.
354///
355/// Nobody saying is an answer of its own and is kept as the absence of this rather than as a
356/// third value: an ollama server runs on the person's own machine and Xiaomi's and Kimi's are
357/// part of a subscription they already pay for, so no model of theirs has a price to record,
358/// and a price invented for one would be the one number on the page nobody can check.
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub enum Price {
361    /// OpenRouter lists the model at nothing for what goes in and for what comes back.
362    Free,
363    /// Either half of the price is above zero, so a request that lands here spends money.
364    Paid,
365}
366
367impl Price {
368    /// Every price, in the order the file writes them.
369    pub const ALL: [Self; 2] = [Self::Free, Self::Paid];
370
371    /// How the price is written in the providers file.
372    #[must_use]
373    pub fn id(self) -> &'static str {
374        match self {
375            Self::Free => "free",
376            Self::Paid => "paid",
377        }
378    }
379
380    /// The price written as `id`, if there is one.
381    #[must_use]
382    pub fn parse(id: &str) -> Option<Self> {
383        Self::ALL.into_iter().find(|price| price.id() == id)
384    }
385}
386
387/// One model of a provider, with both of its context figures and what it costs.
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct Model {
390    /// The model's name as the provider writes it, which is what a harness is given.
391    pub id: String,
392    /// The window the model's own record claims, as the provider's API reports it.
393    pub claimed: Option<u64>,
394    /// The window this server was seen to accept, when it has been measured.
395    pub measured: Option<Measured>,
396    /// What using it costs, when the service says.
397    pub price: Option<Price>,
398}
399
400impl Model {
401    /// A model nothing has been asked about yet.
402    #[must_use]
403    pub fn new(id: impl Into<String>) -> Self {
404        Self { id: id.into(), claimed: None, measured: None, price: None }
405    }
406
407    /// The window a harness on this model is told of: what this server was measured giving, or,
408    /// for a kind whose claims are what it serves ([`ProviderKind::claims_are_served`]), what it
409    /// claims. `None` when neither is known, and the harness is left to its own assumption.
410    #[must_use]
411    pub fn window(&self, kind: ProviderKind) -> Option<u64> {
412        self.measured.map(Measured::tokens).or_else(|| self.claimed.filter(|_| kind.claims_are_served()))
413    }
414
415    /// Where the claimed window was read, when it is the maker's published figure rather than
416    /// something the service itself answered.
417    #[must_use]
418    pub fn published_at(&self, kind: ProviderKind) -> Option<&'static str> {
419        kind.published()
420            .iter()
421            .find(|published| published.id == self.id && Some(published.window) == self.claimed)
422            .map(|published| published.source)
423    }
424
425    /// Whether the server gives this model far less room than the model claims, which is the one
426    /// thing a person has to be told before they point a coding agent at it.
427    #[must_use]
428    pub fn is_short_changed(&self) -> bool {
429        match (self.claimed, self.measured) {
430            (Some(claimed), Some(measured)) => measured.tokens() * 2 < claimed,
431            _ => false,
432        }
433    }
434}
435
436/// A named order of one provider's models: which one to try first, and what to fall back to.
437///
438/// The name is the person's own word for it — `coder` — because that is what they pick and what a
439/// harness is told instead of a model nobody would recognise. It follows the same rules as a
440/// provider's tag, so it can never read as a model name or collide with a key of the file.
441#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct Lineup {
443    /// The name this order is chosen and shown by.
444    pub name: Tag,
445    /// The models in the order they are tried.
446    pub models: Vec<String>,
447}
448
449impl Lineup {
450    /// The window to promise a harness that runs on this order: the smallest of the steps whose
451    /// window is known.
452    ///
453    /// The smallest, because the conversation may go on on any step of the order and what is
454    /// promised has to hold for the one that ends up answering. `None` when no step's window is
455    /// known, which is not the same as a small one: the harness is then left to its own
456    /// assumption rather than cut off by a figure nobody measured.
457    #[must_use]
458    pub fn window(&self, entry: &ProviderEntry) -> Option<u64> {
459        self.models.iter().filter_map(|id| entry.model(id)?.window(entry.kind)).min()
460    }
461
462    /// The steps a request spends money on when it lands on one, so that the person hears it
463    /// while they are choosing the order rather than from a bill afterwards. A step whose price
464    /// nobody published is not named here: nothing is claimed about a price that was not read.
465    #[must_use]
466    pub fn paid<'a>(&self, entry: &'a ProviderEntry) -> Vec<&'a str> {
467        self.models
468            .iter()
469            .filter_map(|id| entry.model(id))
470            .filter(|model| model.price == Some(Price::Paid))
471            .map(|model| model.id.as_str())
472            .collect()
473    }
474
475    /// The steps whose window is not known, which the person is told about rather than left to
476    /// discover as a conversation cut in half. A step naming a model this provider was not last
477    /// seen to have is in here too: nothing is known about a window, for exactly that reason.
478    #[must_use]
479    pub fn unknown_window(&self, entry: &ProviderEntry) -> Vec<&str> {
480        self.models
481            .iter()
482            .filter(|id| entry.model(id).and_then(|model| model.window(entry.kind)).is_none())
483            .map(String::as_str)
484            .collect()
485    }
486}
487
488/// A provider as the file holds it.
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct ProviderEntry {
491    /// The person's own name for it, which stands in front of a model name.
492    pub tag: Tag,
493    /// Which service it is.
494    pub kind: ProviderKind,
495    /// Where it answers, without a trailing slash.
496    pub base: String,
497    /// The shape it speaks.
498    pub wire: Wire,
499    /// What it was last seen to offer. Empty until it has been asked.
500    pub models: Vec<Model>,
501    /// The key it is reached with, for a provider that needs one.
502    pub key: Option<Key>,
503    /// The named orders of its models. Empty until the person has written one: an order is their
504    /// own decision about which of the models to lean on, and no service publishes one.
505    pub lineups: Vec<Lineup>,
506}
507
508impl ProviderEntry {
509    /// A provider of `kind` at `base`, named `tag`, with nothing asked of it yet.
510    #[must_use]
511    ///
512    /// A ready-made kind starts with the models its maker publishes, so the page and the profile
513    /// wizard have something to offer before the service has been asked anything.
514    pub fn new(tag: Tag, kind: ProviderKind, base: &str) -> Self {
515        let models = kind
516            .published()
517            .iter()
518            .map(|published| Model {
519                id: published.id.to_owned(),
520                claimed: Some(published.window),
521                measured: None,
522                price: None,
523            })
524            .collect();
525        Self { tag, kind, base: trim_base(base), wire: kind.wire(), models, key: None, lineups: Vec::new() }
526    }
527
528    /// The address `path` sits at under this provider's base, which is what the page shows
529    /// before a request goes out.
530    #[must_use]
531    pub fn address(&self, path: &str) -> String {
532        format!("{}{path}", self.base)
533    }
534
535    /// The address `path` of this provider's API sits at for a request of shape `wire`; see
536    /// [`ProviderKind::api_address`].
537    #[must_use]
538    pub fn api_address(&self, wire: Wire, path: &str) -> String {
539        self.kind.api_address(&self.base, wire, path)
540    }
541
542    /// Where a message to `model` would go. The page prints this before the person presses
543    /// anything, so that nothing leaves the machine towards an address they have not read.
544    #[must_use]
545    pub fn messages_address(&self) -> String {
546        self.api_address(self.wire, self.wire.messages_path())
547    }
548
549    /// The model named `id`, when this provider was last seen to have one.
550    #[must_use]
551    pub fn model(&self, id: &str) -> Option<&Model> {
552        self.models.iter().find(|model| model.id == id)
553    }
554
555    /// The lineup called `name`, when this provider has one. A name is a person's own word, so
556    /// it is looked up as it is written rather than parsed into a tag first.
557    #[must_use]
558    pub fn lineup(&self, name: &str) -> Option<&Lineup> {
559        self.lineups.iter().find(|lineup| lineup.name.as_str() == name)
560    }
561}
562
563/// An address with the trailing slashes taken off, so that joining a path to it never doubles
564/// one. A person pasting an address from a browser brings the slash with them.
565#[must_use]
566pub fn trim_base(base: &str) -> String {
567    base.trim().trim_end_matches('/').to_owned()
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    fn tag(name: &str) -> Tag {
575        Tag::parse(name).expect("a tag")
576    }
577
578    #[test]
579    fn a_pasted_address_never_doubles_its_slash() {
580        let entry = ProviderEntry::new(tag("ev"), ProviderKind::Ollama, "  http://192.168.122.1:11434/  ");
581        assert_eq!(entry.base, "http://192.168.122.1:11434");
582        assert_eq!(entry.messages_address(), "http://192.168.122.1:11434/v1/messages");
583        assert_eq!(entry.address("/api/tags"), "http://192.168.122.1:11434/api/tags");
584    }
585
586    #[test]
587    fn an_openai_shaped_provider_is_asked_at_the_other_path() {
588        let mut entry = ProviderEntry::new(tag("ev"), ProviderKind::Ollama, "http://h:1");
589        assert_eq!(entry.messages_address(), "http://h:1/v1/messages", "both kinds speak Anthropic by default");
590        entry.wire = Wire::OpenAi;
591        assert_eq!(entry.messages_address(), "http://h:1/v1/chat/completions");
592    }
593
594    #[test]
595    fn openrouter_is_asked_under_its_api_whichever_of_its_two_addresses_was_written() {
596        // Measured against the real service: `https://openrouter.ai/v1/messages` is the website
597        // and answers `200` with HTML; only the path under `/api` is the API.
598        for base in [
599            "https://openrouter.ai",
600            "https://openrouter.ai/",
601            "https://openrouter.ai/api",
602            "https://openrouter.ai/api/",
603        ] {
604            let entry = ProviderEntry::new(tag("yol"), ProviderKind::OpenRouter, base);
605            assert_eq!(entry.messages_address(), "https://openrouter.ai/api/v1/messages", "{base}");
606            assert_eq!(entry.api_address(Wire::OpenAi, "/v1/models"), "https://openrouter.ai/api/v1/models", "{base}");
607        }
608        let mut entry = ProviderEntry::new(tag("yol"), ProviderKind::OpenRouter, "https://openrouter.ai");
609        entry.wire = Wire::OpenAi;
610        assert_eq!(entry.messages_address(), "https://openrouter.ai/api/v1/chat/completions");
611        let ollama = ProviderEntry::new(tag("ev"), ProviderKind::Ollama, "http://h:1");
612        assert_eq!(
613            ollama.api_address(Wire::OpenAi, "/v1/models"),
614            "http://h:1/v1/models",
615            "an ollama server answers at its root"
616        );
617    }
618
619    #[test]
620    fn every_written_value_reads_back_as_itself() {
621        for kind in ProviderKind::ALL {
622            assert_eq!(ProviderKind::parse(kind.id()), Some(kind));
623        }
624        for wire in Wire::ALL {
625            assert_eq!(Wire::parse(wire.id()), Some(wire));
626        }
627        for measured in [Measured::About(7), Measured::AtLeast(7)] {
628            assert_eq!(Measured::parse(measured.id(), 7), Some(measured));
629        }
630        for price in Price::ALL {
631            assert_eq!(Price::parse(price.id()), Some(price));
632        }
633        assert_eq!(ProviderKind::parse("ollamaa"), None);
634        assert_eq!(Wire::parse("anthropics"), None);
635        assert_eq!(Measured::parse("exactly", 7), None);
636        assert_eq!(Price::parse("cheaper"), None);
637    }
638
639    /// A provider whose listing carries windows a harness is given, which is what a ready-made
640    /// kind does, and the three steps an order of them is measured against: two with windows and
641    /// one the listing never said anything about.
642    fn with_windows() -> ProviderEntry {
643        let mut entry =
644            ProviderEntry::new(tag("mimo"), ProviderKind::MimoTokenPlan, "https://token-plan-sgp.xiaomimimo.com");
645        entry.models = vec![
646            Model { id: "mimo-v2.6-pro".to_owned(), claimed: Some(131_072), measured: None, price: None },
647            Model { id: "mimo-v2.5".to_owned(), claimed: Some(32_768), measured: None, price: None },
648            Model { id: "mimo-v2.5-asr".to_owned(), claimed: None, measured: None, price: None },
649        ];
650        entry
651    }
652
653    #[test]
654    fn an_order_is_promised_the_smallest_window_of_its_steps_because_a_conversation_may_go_on_on_any_of_them() {
655        let entry = with_windows();
656        let lineup = Lineup {
657            name: tag("kisa"),
658            models: vec!["mimo-v2.6-pro".to_owned(), "mimo-v2.5".to_owned(), "mimo-v2.5-asr".to_owned()],
659        };
660        assert_eq!(lineup.window(&entry), Some(32_768), "what is promised has to hold for the step that answers");
661        assert_eq!(lineup.unknown_window(&entry), ["mimo-v2.5-asr"], "and the one nobody knows a window for is named");
662
663        let unknown = Lineup { name: tag("geci"), models: vec!["mimo-v2.5-asr".to_owned(), "yok".to_owned()] };
664        assert_eq!(unknown.window(&entry), None, "no step with a known window is no window at all");
665        assert_eq!(
666            unknown.unknown_window(&entry),
667            ["mimo-v2.5-asr", "yok"],
668            "a step naming an unknown model is one too"
669        );
670    }
671
672    #[test]
673    fn an_order_names_exactly_the_steps_that_spend_money_and_says_nothing_about_a_price_it_did_not_read() {
674        let mut entry = ProviderEntry::new(tag("yol"), ProviderKind::OpenRouter, "https://openrouter.ai");
675        entry.models = vec![
676            Model { id: "z-ai/glm-4.6".to_owned(), claimed: None, measured: None, price: Some(Price::Paid) },
677            Model { id: "z-ai/glm-4.6:free".to_owned(), claimed: None, measured: None, price: Some(Price::Free) },
678            Model { id: "sirala".to_owned(), claimed: None, measured: None, price: None },
679        ];
680        let lineup = Lineup {
681            name: tag("coder"),
682            models: vec!["z-ai/glm-4.6:free".to_owned(), "z-ai/glm-4.6".to_owned(), "sirala".to_owned()],
683        };
684        assert_eq!(lineup.paid(&entry), ["z-ai/glm-4.6"], "only the step that costs, not the free one beside it");
685    }
686
687    #[test]
688    fn an_order_is_found_under_the_name_it_was_given_and_a_provider_of_no_orders_has_none() {
689        let mut entry = with_windows();
690        let lineup = Lineup { name: tag("coder"), models: vec!["mimo-v2.5".to_owned()] };
691        entry.lineups.push(lineup.clone());
692        assert_eq!(entry.lineup("coder"), Some(&lineup));
693        assert_eq!(entry.lineup("yok"), None, "a name nobody gave");
694        assert!(ProviderEntry::new(tag("ev"), ProviderKind::Ollama, "http://h:1").lineups.is_empty());
695    }
696
697    #[test]
698    fn only_a_window_whose_edge_was_found_is_called_cramped() {
699        assert!(Measured::About(3_012).is_cramped(), "the edge was found and it is small");
700        assert!(!Measured::AtLeast(3_012).is_cramped(), "a probe that never stopped growing says nothing yet");
701        assert!(!Measured::About(65_536).is_cramped());
702        assert_eq!(Measured::About(3_012).tokens(), 3_012);
703    }
704
705    #[test]
706    fn a_server_giving_far_less_than_the_model_claims_is_what_the_person_is_told() {
707        // The numbers are the ones measured against the owner's own server.
708        let short = Model {
709            id: "qwen3.8".to_owned(),
710            claimed: Some(262_144),
711            measured: Some(Measured::About(3_012)),
712            price: None,
713        };
714        assert!(short.is_short_changed());
715        let honest =
716            Model { id: "q".to_owned(), claimed: Some(32_768), measured: Some(Measured::AtLeast(32_768)), price: None };
717        assert!(!honest.is_short_changed());
718        assert!(!Model::new("q").is_short_changed(), "nothing is claimed about a model nobody asked about");
719    }
720
721    #[test]
722    fn only_a_local_server_is_reached_without_a_key() {
723        assert!(!ProviderKind::Ollama.needs_key(), "a local server carries no secret into a container");
724        assert!(ProviderKind::OpenRouter.needs_key());
725        assert!(ProviderKind::MimoTokenPlan.needs_key());
726        assert!(ProviderKind::KimiCode.needs_key());
727    }
728
729    #[test]
730    fn xiaomi_is_asked_under_anthropic_in_one_shape_and_at_its_root_in_the_other() {
731        // Measured with a real key on 2026-09-23: `/anthropic/v1/messages` and
732        // `/v1/chat/completions` answer, `/v1/models` lists, `/anthropic/v1/models` is a 404.
733        for base in [
734            "https://token-plan-ams.xiaomimimo.com",
735            "https://token-plan-ams.xiaomimimo.com/",
736            "https://token-plan-ams.xiaomimimo.com/anthropic",
737            "https://token-plan-ams.xiaomimimo.com/v1",
738            "https://token-plan-ams.xiaomimimo.com/anthropic/v1/",
739        ] {
740            let entry = ProviderEntry::new(tag("mimo"), ProviderKind::MimoTokenPlan, base);
741            assert_eq!(
742                entry.messages_address(),
743                "https://token-plan-ams.xiaomimimo.com/anthropic/v1/messages",
744                "{base}"
745            );
746            assert_eq!(
747                entry.api_address(Wire::OpenAi, "/v1/chat/completions"),
748                "https://token-plan-ams.xiaomimimo.com/v1/chat/completions",
749                "{base}"
750            );
751            assert_eq!(
752                entry.api_address(Wire::of_path("/v1/models"), "/v1/models"),
753                "https://token-plan-ams.xiaomimimo.com/v1/models",
754                "{base}: the listing is only at the root"
755            );
756        }
757    }
758
759    #[test]
760    fn kimi_is_asked_under_coding_in_both_shapes() {
761        for base in ["https://api.kimi.com", "https://api.kimi.com/coding", "https://api.kimi.com/coding/v1"] {
762            let entry = ProviderEntry::new(tag("kimi"), ProviderKind::KimiCode, base);
763            assert_eq!(entry.messages_address(), "https://api.kimi.com/coding/v1/messages", "{base}");
764            assert_eq!(
765                entry.api_address(Wire::OpenAi, "/v1/chat/completions"),
766                "https://api.kimi.com/coding/v1/chat/completions",
767                "{base}"
768            );
769        }
770    }
771
772    #[test]
773    fn a_harness_says_its_shape_by_the_path_it_asks_for() {
774        assert_eq!(Wire::of_path("/v1/messages"), Wire::Anthropic);
775        assert_eq!(Wire::of_path("/v1/messages?beta=true"), Wire::Anthropic, "what Claude Code really sends");
776        assert_eq!(Wire::of_path("/v1/chat/completions"), Wire::OpenAi);
777        assert_eq!(Wire::of_path("/v1/models"), Wire::OpenAi, "the one root every kind lists at");
778    }
779
780    #[test]
781    fn each_kind_puts_its_key_in_the_header_its_maker_names() {
782        assert_eq!(ProviderKind::Ollama.key_header(), ("Authorization", "Bearer "));
783        assert_eq!(ProviderKind::OpenRouter.key_header(), ("Authorization", "Bearer "));
784        assert_eq!(ProviderKind::MimoTokenPlan.key_header(), ("api-key", ""));
785        assert_eq!(ProviderKind::KimiCode.key_header(), ("x-api-key", ""));
786    }
787
788    #[test]
789    fn a_ready_made_kind_starts_with_its_makers_models_and_a_kind_of_ones_own_with_none() {
790        let mimo =
791            ProviderEntry::new(tag("mimo"), ProviderKind::MimoTokenPlan, ProviderKind::MimoTokenPlan.suggested_base());
792        let ids: Vec<&str> = mimo.models.iter().map(|model| model.id.as_str()).collect();
793        assert_eq!(ids, ["mimo-v2.6-pro", "mimo-v2.6-flash", "mimo-v2.5-pro", "mimo-v2.5"]);
794        let flash = mimo.model("mimo-v2.6-flash").expect("the model");
795        assert_eq!(flash.claimed, Some(1_048_576));
796        assert!(
797            flash
798                .published_at(ProviderKind::MimoTokenPlan)
799                .is_some_and(|source| source.starts_with("https://mimo.mi.com/"))
800        );
801        let kimi = ProviderEntry::new(tag("kimi"), ProviderKind::KimiCode, ProviderKind::KimiCode.suggested_base());
802        assert_eq!(kimi.model("kimi-for-coding").and_then(|model| model.claimed), Some(1_048_576));
803        assert!(ProviderEntry::new(tag("ev"), ProviderKind::Ollama, "http://h:1").models.is_empty());
804    }
805
806    #[test]
807    fn a_harness_is_told_a_claimed_window_only_where_the_claim_is_what_is_served() {
808        let claimed = Model { id: "m".to_owned(), claimed: Some(262_144), measured: None, price: None };
809        // An ollama server's record said 262 144 while it cut prompts at three thousand.
810        assert_eq!(claimed.window(ProviderKind::Ollama), None);
811        assert_eq!(claimed.window(ProviderKind::OpenRouter), None);
812        assert_eq!(claimed.window(ProviderKind::MimoTokenPlan), Some(262_144));
813        assert_eq!(claimed.window(ProviderKind::KimiCode), Some(262_144));
814        let measured = Model { measured: Some(Measured::About(31_512)), ..claimed };
815        for kind in ProviderKind::ALL {
816            assert_eq!(measured.window(kind), Some(31_512), "{kind:?}: what was measured comes first");
817        }
818    }
819
820    #[test]
821    fn every_region_offered_is_an_address_of_its_own_kind() {
822        for kind in ProviderKind::ALL {
823            for region in kind.regions() {
824                assert!(region.base.starts_with("https://"), "{kind:?} {region:?}");
825                assert_eq!(trim_base(region.base), region.base, "{kind:?}: no trailing slash");
826            }
827            if let Some(first) = kind.regions().first() {
828                assert_eq!(first.base, kind.suggested_base(), "{kind:?}: the offered address is the first region");
829            }
830        }
831        assert_eq!(ProviderKind::MimoTokenPlan.regions().len(), 3);
832        assert_eq!(ProviderKind::KimiCode.suggested_base(), "https://api.kimi.com");
833    }
834}