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