1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Dynamic model capability/pricing discovery from third-party feeds.
//!
//! The static [`model_capabilities`](crate::model_capabilities) table is the
//! offline fallback: it ships in the binary and never makes a network call.
//! This module layers a *refreshable* feed on top so newly shipped models gain
//! pricing and limit metadata without an SDK code change.
//!
//! The pieces:
//!
//! - [`CatalogEntry`] — a single provider/model row distilled from a feed.
//! - [`ModelCatalogSource`] — the trait a feed implements; each source pairs an
//! async `fetch` with a pure `parse` so the parsing is testable offline.
//! - [`ModelsDevSource`] (default) and [`OpenRouterSource`] — two concrete
//! feeds.
//! - [`ModelRegistry`] — a layered resolver: user override → feed cache →
//! static table → graceful empty defaults.
//!
//! This whole module is gated behind the `model-discovery` cargo feature
//! because it performs outbound HTTP to a third-party catalog. The default
//! build is unchanged.
use ;
use async_trait;
use Duration;
use cratePricing;
pub use ;
pub use ;
pub use ;
/// Select the rates that apply to a call of `input_tokens`: the highest tier
/// the call reaches, or the base rates when it reaches none.
///
/// The bound is inclusive — see [`PricingTier::min_input_tokens`]. `tiers` need
/// not be sorted.
pub const MODELS_DEV_URL: &str = "https://models.dev/api.json";
pub const OPENROUTER_URL: &str = "https://openrouter.ai/api/v1/models";
const FEED_TIMEOUT_SECS: u64 = 30;
/// A single provider/model capability + pricing row distilled from a feed.
///
/// Field shapes mirror [`ModelCapabilities`](crate::model_capabilities::ModelCapabilities)
/// so the registry can resolve feed rows and static rows into the same shape.
/// A long-context price band: from `min_input_tokens` upwards, the provider
/// bills the whole call at `pricing` instead of the base rates.
///
/// Frontier models price long context at a premium — GPT-5.x doubles its input
/// rate above 272K tokens, Gemini and Claude above 200K — so a source that
/// publishes tiers must be billed through them, or a long-context run is priced
/// at a fraction of what it costs and sails past its budget.
///
/// The bound is **inclusive** on both feeds, confirmed against each one's
/// documentation rather than inferred from a field name. `OpenRouter`'s
/// provider docs state a tier "applies when input tokens meet or exceed the
/// `min_context` value", so `min_prompt_tokens` maps straight across. The
/// models.dev SDK schema documents `tier.size` as "the context size at which
/// this tier starts to apply" / "pricing that applies from a given context size
/// upward" — also inclusive, so its `size` maps straight across too. (The
/// models.dev `context_over_<n>` field reads exclusive, but the schema marks it
/// a legacy projection to be superseded by `tiers`, so it does not define the
/// bound.)
/// Merge a band's stated rates over the base row: a rate the band restates
/// wins, one it omits keeps its base value.
///
/// Both feeds publish partial bands — `OpenRouter`'s Gemini 2.5 Pro override
/// raises `prompt`, `completion` and `input_cache_read` above 200K but says
/// nothing about `input_cache_write`, and models.dev's tier for the same model
/// omits `cache_write` too. Two independent feeds mirroring the vendor's table
/// with the same omission is the vendor not restating that rate for the band,
/// not the rate vanishing: the base value still applies. Inheriting it is
/// therefore what the data means, and it is also the only reading that cannot
/// silently under-bill a component.
/// A third-party feed of [`CatalogEntry`] rows.
pub