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
18/// The embedded UI. Served at both `/` and `/ui` only when the server
19/// is started with `--ui-server` / `FERROX_UI=1`.
20pub const ROOT: &str = "/";
21/// See [`ROOT`].
22pub const UI: &str = "/ui";
23
24pub const V1_MODELS: &str = "/v1/models";
25pub const V1_CHAT_COMPLETIONS: &str = "/v1/chat/completions";
26pub const V1_COMPLETIONS: &str = "/v1/completions";
27pub const V1_TOKENIZE: &str = "/v1/tokenize";
28pub const V1_DETOKENIZE: &str = "/v1/detokenize";
29pub const V1_EMBEDDINGS: &str = "/v1/embeddings";
30
31/// Anthropic-compatible messages endpoint.
32pub const V1_MESSAGES: &str = "/v1/messages";
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 a root-level `/cancel` would sit inside the namespace the
41/// embedded UI's SPA fallback owns, where a mistyped path is answered
42/// with HTML rather than a JSON 404.
43///
44/// This is the second tier of cancellation, not the only one -- a
45/// client that simply drops the connection is also honoured. It exists
46/// because the first tier is unreliable: proxies buffer, and a page
47/// unload races the abort it is supposed to send. `keepalive: true`
48/// makes this one survive that.
49pub const V1_CANCEL: &str = "/v1/cancel";
50
51// ---------------------------------------------------------------------
52// Control surface.
53//
54// Everything under `/admin` either changes what the server serves or
55// writes to disk, so all of it sits behind the same `FERROX_API_KEY`
56// gate as `/v1/*` -- never on the unauthenticated `/health` side.
57// ---------------------------------------------------------------------
58
59/// Model inventory: what is on disk, what is loaded, what failed.
60pub const ADMIN_MODELS: &str = "/admin/models";
61
62/// Start loading a discovered model by its `id`. Answers `202` with a
63/// task id; the load itself runs off the request.
64pub const ADMIN_MODELS_LOAD: &str = "/admin/models/load";
65
66/// Drop the active model. Synchronous: unloading is releasing one
67/// `Arc`, and requests already decoding keep theirs.
68pub const ADMIN_MODELS_UNLOAD: &str = "/admin/models/unload";
69
70/// Fetch a `.gguf` from the Hugging Face Hub into the model directory.
71/// Answers `202` with a task id.
72pub const ADMIN_DOWNLOAD: &str = "/admin/download";
73
74/// Every long-running job this server knows about, newest first.
75pub const ADMIN_TASKS: &str = "/admin/tasks";
76
77/// Request cancellation of one task. **A template, not a literal**: the
78/// `{task_id}` placeholder is written in the OpenAPI style rather than
79/// any one web framework's, because this crate is imported by clients
80/// that have never heard of the server's router. Build a concrete path
81/// with [`admin_task_cancel`].
82pub const ADMIN_TASK_CANCEL: &str = "/admin/tasks/{task_id}/cancel";
83
84/// Counters, uptime, and the recent-request ring buffer.
85pub const ADMIN_STATS: &str = "/admin/stats";
86
87/// The concrete cancel path for one task id.
88pub fn admin_task_cancel(task_id: &str) -> String {
89    ADMIN_TASK_CANCEL.replace("{task_id}", task_id)
90}
91
92/// Every fixed route above, for clients that want to enumerate the
93/// surface (and for the round-trip test below).
94///
95/// [`ADMIN_TASK_CANCEL`] is deliberately absent: it is a template, and
96/// a caller iterating this list to probe paths would get a 404 for a
97/// literal `{task_id}`.
98pub const ALL: &[&str] = &[
99    HEALTH,
100    METRICS,
101    CACHE_STATS,
102    ROOT,
103    UI,
104    V1_MODELS,
105    V1_CHAT_COMPLETIONS,
106    V1_COMPLETIONS,
107    V1_TOKENIZE,
108    V1_DETOKENIZE,
109    V1_EMBEDDINGS,
110    V1_MESSAGES,
111    V1_CANCEL,
112    ADMIN_MODELS,
113    ADMIN_MODELS_LOAD,
114    ADMIN_MODELS_UNLOAD,
115    ADMIN_DOWNLOAD,
116    ADMIN_TASKS,
117    ADMIN_STATS,
118];
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn every_route_is_absolute_and_unique() {
126        let mut seen = std::collections::BTreeSet::new();
127        for route in ALL {
128            assert!(route.starts_with('/'), "{route} is not an absolute path");
129            assert!(
130                !route.ends_with('/') || *route == ROOT,
131                "{route} has a trailing slash"
132            );
133            assert!(seen.insert(*route), "{route} is listed twice");
134        }
135    }
136
137    #[test]
138    fn the_admin_surface_is_namespaced() {
139        for route in ALL.iter().filter(|r| r.starts_with("/admin")) {
140            assert!(
141                route.starts_with("/admin/"),
142                "{route} would collide with the /admin prefix itself"
143            );
144        }
145    }
146
147    #[test]
148    fn the_cancel_template_is_not_enumerated_as_a_real_path() {
149        assert!(!ALL.contains(&ADMIN_TASK_CANCEL));
150        assert!(ADMIN_TASK_CANCEL.contains("{task_id}"));
151    }
152
153    #[test]
154    fn a_cancel_path_substitutes_the_only_placeholder() {
155        assert_eq!(
156            admin_task_cancel("task-7"),
157            "/admin/tasks/task-7/cancel".to_string()
158        );
159        assert!(!admin_task_cancel("task-7").contains('{'));
160    }
161}