Skip to main content

ferrox_api/
routes.rs

1//! Every path `ferrox-server` serves, named once.
2//!
3//! Only routes that actually exist belong here. A constant for a
4//! not-yet-implemented endpoint is worse than no constant at all: it
5//! reads as a promise, and a client that imports it gets a 404 with the
6//! contract crate's blessing.
7
8/// Liveness + readiness + capability handshake. Never behind auth, so a
9/// probe works regardless of `FERROX_API_KEY`.
10pub const HEALTH: &str = "/health";
11
12/// Prometheus text-exposition metrics.
13pub const METRICS: &str = "/metrics";
14
15/// Response- and prefix-cache counters.
16pub const CACHE_STATS: &str = "/cache/stats";
17
18pub const V1_MODELS: &str = "/v1/models";
19pub const V1_CHAT_COMPLETIONS: &str = "/v1/chat/completions";
20pub const V1_COMPLETIONS: &str = "/v1/completions";
21pub const V1_TOKENIZE: &str = "/v1/tokenize";
22pub const V1_DETOKENIZE: &str = "/v1/detokenize";
23pub const V1_EMBEDDINGS: &str = "/v1/embeddings";
24
25/// Anthropic-compatible messages endpoint.
26pub const V1_MESSAGES: &str = "/v1/messages";
27
28/// Anthropic's prompt-sizing endpoint: how many input tokens a request
29/// *would* cost, without generating any. Behind the same key as
30/// [`V1_MESSAGES`], because answering it requires the loaded
31/// checkpoint's own tokenizer and chat template.
32pub const V1_MESSAGES_COUNT_TOKENS: &str = "/v1/messages/count_tokens";
33
34/// Explicit cancellation of one in-flight generation, by the
35/// `request_id` the server states on the first streamed chunk.
36///
37/// Under `/v1` rather than at the root the plan sketched it at, for two
38/// reasons that both matter: it acts on inference and so belongs behind
39/// the same `FERROX_API_KEY` gate as the endpoint that started the
40/// work, and `/v1` is where every other inference path already lives,
41/// so a client configured with one base URL reaches all of them.
42///
43/// This is the second tier of cancellation, not the only one -- a
44/// client that simply drops the connection is also honoured. It exists
45/// because the first tier is unreliable: proxies buffer, and a page
46/// unload races the abort it is supposed to send. `keepalive: true`
47/// makes this one survive that.
48pub const V1_CANCEL: &str = "/v1/cancel";
49
50/// Reconnect into a stream started with `stream_resumable: true`,
51/// resuming after the `Last-Event-ID` the client last saw.
52///
53/// **A template, not a literal** -- see [`ADMIN_TASK_CANCEL`] for why
54/// this crate writes placeholders in the OpenAPI style. Build a
55/// concrete path with [`v1_stream`].
56///
57/// Behind the same key as the endpoint that started the work: the
58/// replay buffer holds the model's output, so reading it must cost
59/// exactly what producing it cost.
60pub const V1_STREAM: &str = "/v1/stream/{request_id}";
61
62/// The same replay buffer over plain JSON, for the case SSE cannot
63/// survive: a reverse proxy that buffers `text/event-stream` turns a
64/// stream into one long silence, and cannot do that to a short response
65/// that has already ended. Build a concrete path with
66/// [`v1_stream_poll`].
67pub const V1_STREAM_POLL: &str = "/v1/stream/{request_id}/poll";
68
69// ---------------------------------------------------------------------
70// Control surface.
71//
72// Everything under `/admin` either changes what the server serves or
73// writes to disk, so all of it sits behind the same `FERROX_API_KEY`
74// gate as `/v1/*` -- never on the unauthenticated `/health` side.
75// ---------------------------------------------------------------------
76
77/// Model inventory: what is on disk, what is loaded, what failed.
78pub const ADMIN_MODELS: &str = "/admin/models";
79
80/// Start loading a discovered model by its `id`. Answers `202` with a
81/// task id; the load itself runs off the request.
82pub const ADMIN_MODELS_LOAD: &str = "/admin/models/load";
83
84/// Drop the active model. Synchronous: unloading is releasing one
85/// `Arc`, and requests already decoding keep theirs.
86pub const ADMIN_MODELS_UNLOAD: &str = "/admin/models/unload";
87
88/// Fetch a `.gguf` from the Hugging Face Hub into the model directory.
89/// Answers `202` with a task id.
90pub const ADMIN_DOWNLOAD: &str = "/admin/download";
91
92/// Every long-running job this server knows about, newest first.
93pub const ADMIN_TASKS: &str = "/admin/tasks";
94
95/// Request cancellation of one task. **A template, not a literal**: the
96/// `{task_id}` placeholder is written in the OpenAPI style rather than
97/// any one web framework's, because this crate is imported by clients
98/// that have never heard of the server's router. Build a concrete path
99/// with [`admin_task_cancel`].
100pub const ADMIN_TASK_CANCEL: &str = "/admin/tasks/{task_id}/cancel";
101
102/// Counters, uptime, and the recent-request ring buffer.
103pub const ADMIN_STATS: &str = "/admin/stats";
104
105/// The OpenAI **Responses** surface -- what `codex` speaks. A different
106/// request/response shaping over the same generation path, not a second
107/// engine.
108pub const V1_RESPONSES: &str = "/v1/responses";
109
110/// One stored response. This server is stateless, so it answers 404 --
111/// deliberately, rather than 404-ing from the router, because the two
112/// say different things: the route EXISTS and keeps nothing, which
113/// tells a client to stop polling rather than to check its base URL.
114pub const V1_RESPONSE: &str = "/v1/responses/{response_id}";
115
116/// Cancel one stored response. Same stateless answer; a live generation
117/// is stopped through [`V1_CANCEL`] with its `request_id`.
118pub const V1_RESPONSE_CANCEL: &str = "/v1/responses/{response_id}/cancel";
119
120/// Live serving telemetry: throughput over a trailing window, request
121/// latency percentile, and the cache pools' occupancy. Distinct from
122/// [`ADMIN_STATS`], which is this server's own operational ring; this
123/// is the shape a desktop or dashboard polls.
124pub const V1_STATS: &str = "/v1/stats";
125
126/// Incremental page over the recent-request ring: `?since=<cursor>` and
127/// `?limit=<n>`. The cursor is all-time, so a poller that keeps up
128/// reads each row exactly once.
129pub const V1_REQUESTS: &str = "/v1/requests";
130
131/// The current cache geometry: how VRAM is split between the expert
132/// cache and the KV pools, and what a re-split could move.
133pub const V1_CACHE_STATUS: &str = "/v1/cache/status";
134
135/// Re-split the caches on a live engine. New generation is refused
136/// while a rebuild is in flight.
137pub const V1_CACHE_REBUILD: &str = "/v1/cache/rebuild";
138
139/// Close admission, drain, and seal the final accounting snapshot. A
140/// supervisor calls this before it sends a signal, so process shutdown
141/// cannot race the last sampled token.
142pub const ADMIN_PREPARE_STOP: &str = "/v1/admin/prepare-stop";
143
144/// The concrete cancel path for one task id.
145pub fn admin_task_cancel(task_id: &str) -> String {
146    ADMIN_TASK_CANCEL.replace("{task_id}", task_id)
147}
148
149/// The concrete resume path for one request id.
150pub fn v1_stream(request_id: &str) -> String {
151    V1_STREAM.replace("{request_id}", request_id)
152}
153
154/// The concrete polling-fallback path for one request id.
155pub fn v1_stream_poll(request_id: &str) -> String {
156    V1_STREAM_POLL.replace("{request_id}", request_id)
157}
158
159/// Every fixed route above, for clients that want to enumerate the
160/// surface (and for the round-trip test below).
161///
162/// [`ADMIN_TASK_CANCEL`], [`V1_STREAM`] and [`V1_STREAM_POLL`] are
163/// deliberately absent: they are templates, and a caller iterating this
164/// list to probe paths would get a 404 for a literal `{task_id}`.
165pub const ALL: &[&str] = &[
166    HEALTH,
167    METRICS,
168    CACHE_STATS,
169    V1_MODELS,
170    V1_CHAT_COMPLETIONS,
171    V1_COMPLETIONS,
172    V1_TOKENIZE,
173    V1_DETOKENIZE,
174    V1_EMBEDDINGS,
175    V1_MESSAGES,
176    V1_MESSAGES_COUNT_TOKENS,
177    V1_CANCEL,
178    ADMIN_MODELS,
179    ADMIN_MODELS_LOAD,
180    ADMIN_MODELS_UNLOAD,
181    ADMIN_DOWNLOAD,
182    ADMIN_TASKS,
183    ADMIN_STATS,
184    V1_RESPONSES,
185    V1_STATS,
186    V1_REQUESTS,
187    V1_CACHE_STATUS,
188    V1_CACHE_REBUILD,
189    ADMIN_PREPARE_STOP,
190];
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn every_route_is_absolute_and_unique() {
198        let mut seen = std::collections::BTreeSet::new();
199        for route in ALL {
200            assert!(route.starts_with('/'), "{route} is not an absolute path");
201            assert!(!route.ends_with('/'), "{route} has a trailing slash");
202            assert!(seen.insert(*route), "{route} is listed twice");
203        }
204    }
205
206    #[test]
207    fn the_admin_surface_is_namespaced() {
208        for route in ALL.iter().filter(|r| r.starts_with("/admin")) {
209            assert!(
210                route.starts_with("/admin/"),
211                "{route} would collide with the /admin prefix itself"
212            );
213        }
214    }
215
216    #[test]
217    fn the_cancel_template_is_not_enumerated_as_a_real_path() {
218        assert!(!ALL.contains(&ADMIN_TASK_CANCEL));
219        assert!(ADMIN_TASK_CANCEL.contains("{task_id}"));
220    }
221
222    /// Same rule for the stream templates: a client that probed this
223    /// list would ask for a literal `{request_id}` and get a 404 with
224    /// the contract crate's blessing.
225    #[test]
226    fn the_stream_templates_are_not_enumerated_as_real_paths() {
227        for template in [V1_STREAM, V1_STREAM_POLL] {
228            assert!(!ALL.contains(&template));
229            assert!(template.contains("{request_id}"));
230        }
231        assert_eq!(v1_stream("chatcmpl-7"), "/v1/stream/chatcmpl-7");
232        assert_eq!(
233            v1_stream_poll("chatcmpl-7"),
234            "/v1/stream/chatcmpl-7/poll".to_string()
235        );
236        assert!(!v1_stream("chatcmpl-7").contains('{'));
237    }
238
239    /// The polling fallback must sit under the stream it falls back
240    /// from, so one base URL and one key reach both.
241    #[test]
242    fn the_poll_route_is_nested_under_the_resume_route() {
243        assert!(V1_STREAM_POLL.starts_with(V1_STREAM));
244        assert!(V1_STREAM.starts_with("/v1/"));
245    }
246
247    #[test]
248    fn a_cancel_path_substitutes_the_only_placeholder() {
249        assert_eq!(
250            admin_task_cancel("task-7"),
251            "/admin/tasks/task-7/cancel".to_string()
252        );
253        assert!(!admin_task_cancel("task-7").contains('{'));
254    }
255}