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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
//! Asking an endpoint which models it serves.
//!
//! `drep init` used to offer a hardcoded model name per preset and let the user
//! type over it, with nothing checking the result. `presets.rs` said so out
//! loud: the defaults are "the one thing here that goes stale". A typo, or a
//! model the plan does not include, surfaced as a 404 on the first push rather
//! than at the prompt.
//!
//! The endpoint already knows the answer, and the wizard is holding the key at
//! exactly the moment it asks. So it asks the endpoint.
//!
//! ## Why not a registry
//!
//! A vendored catalogue would go stale exactly as the hardcoded defaults do
//! and would additionally have to be noticed and updated. A third-party index
//! (models.dev) covers every provider at once, but describes what a *vendor*
//! publishes rather than what *this account's plan* serves, and it is a 4 MB
//! network dependency on somebody else's uptime. The endpoint is authoritative
//! for the only question being asked.
//!
//! ## One shape, three vendors
//!
//! Every endpoint drep ships a preset for answers `GET {base_url}/models` with
//! `{"data": [{"id": ...}]}`, whichever protocol it otherwise speaks. They
//! disagree only on what *else* is in each entry: z.ai sends OpenAI's
//! `object`/`created`/`owned_by`, MiniMax sends Anthropic's `type`/`created_at`
//! /`display_name`, and Kimi sends both plus `context_length` and
//! `supports_reasoning`. Reading `id` and an optional `display_name` covers all
//! three, and serde ignores the rest - which is also what stops a new field
//! breaking the parse.
//!
//! The protocol still decides the **auth header**, because that is not
//! negotiable per request: bearer for OpenAI-compatible, `x-api-key` plus a
//! version for Anthropic.
//!
//! The configured endpoint is the exact origin allowed to receive that header.
//! A redirect is reported like any other non-success status and is never
//! followed, including when its destination stays on the same origin.
//!
//! ## Failure is never fatal
//!
//! A listing is a convenience during setup. An endpoint that does not serve one
//! (a local llama.cpp build, a gateway, anything older) must leave the user
//! typing a name exactly as before, so every error here is something the caller
//! reports and moves past. Nothing in this module can stop `drep init`.
use Duration;
use ApiProtocol;
use Deserialize;
use Error;
/// How long to wait for a listing before giving up and letting the user type.
///
/// Short on purpose. This sits between two prompts in an interactive session,
/// and a setup that appears to hang is worse than one that asks for a name.
const TIMEOUT: Duration = from_secs;
/// The API version header Anthropic-shaped endpoints require.
///
/// Duplicated from the SDK rather than imported because the SDK does not export
/// it, and it is a one-line constant whose value is pinned by the same tests
/// that pin the request shape. If it ever needs to change, `list` fails and the
/// wizard falls back to typing a name.
const ANTHROPIC_VERSION: &str = "2023-06-01";
/// A model an endpoint offers.
/// Why a listing could not be produced.
///
/// Every variant is non-fatal: the caller reports it and asks for a name.
/// Where the wizard gets a model list.
///
/// A trait so the wizard can be driven by a stub: the alternative is a wizard
/// test suite that makes real network calls, which would be slow, offline-
/// hostile, and dependent on somebody's plan still including a given model.
/// The largest listing drep will read into memory.
///
/// A real listing is a few kilobytes - the longest of the four is Kimi's, at
/// well under one. 8 MB is a margin no honest endpoint approaches, and it is
/// what stops a mirror, a redirect to something else, or a compromised host
/// making `drep init` allocate without bound. The timeout does not prevent
/// that on its own: a fast host can send a great deal inside one.
const MAX_LISTING_BYTES: u64 = 8 * 1024 * 1024;
/// The real thing: one HTTP GET.
/// The listing URL for `endpoint`.
///
/// `{base_url}/models` for both protocols - verified against all three
/// subscription endpoints, whose base URLs already carry whatever version
/// segment they use (`/api/coding/paas/v4`, `/anthropic/v1`, `/coding/v1`).
/// A trailing slash on the configured endpoint would otherwise produce `//`,
/// which some gateways answer with a redirect and others with a 404.
/// Map an HTTP status onto the reason the caller reports.
///
/// 404 and 405 are the endpoint saying it has no such route, which is the
/// ordinary case for a local server rather than a fault. 401 and 403 are worth
/// separating because they mean the key is wrong - the user is about to store
/// it, and finding out now beats finding out on the first push.
/// The half of a listing response drep reads.
/// One entry. Every other field the vendors send is ignored by serde.
/// Parse a listing body into models, in the order the endpoint sent them.
///
/// Order is preserved rather than sorted: every one of these endpoints lists
/// its newest model first, which is the one a user setting drep up almost
/// always wants, and alphabetical order would bury it (`MiniMax-M2` sorts above
/// `MiniMax-M3`; `glm-4.5` above `glm-5.3`).
///
/// An empty list is [`ListError::Unsupported`] rather than an empty menu: a
/// prompt offering nothing is worse than the free-text prompt it replaced.