ferrox_api/cancel.rs
1//! Wire shapes for `POST /v1/cancel`.
2//!
3//! One request id in, one honest verdict out. The verdict is
4//! deliberately not a bare `ok: true`: a client that cancels the moment
5//! the last token arrives, or retries a cancel after the first one
6//! worked, needs to be able to tell "I stopped a live generation" from
7//! "there was nothing left to stop". Both are fine outcomes; only one
8//! of them saved any work, and a UI that cannot distinguish them will
9//! claim it stopped something it did not.
10
11use serde::{Deserialize, Serialize};
12
13/// The body of `POST /v1/cancel`.
14///
15/// `request_id` is the value the server states as `request_id` on the
16/// first chunk of a streamed completion (and as `id` on every chunk),
17/// so a client never has to invent or correlate one.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct CancelGenerationRequest {
20 pub request_id: String,
21}
22
23/// The answer to `POST /v1/cancel`.
24///
25/// Sent with `200` when a live generation was signalled and with `404`
26/// when the id names nothing that is running -- already finished, never
27/// issued, or served by a path that does not register for cancellation.
28/// The body says the same thing in both cases so a client that only
29/// reads JSON is not left guessing at the status line.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct CancelGenerationResponse {
32 pub request_id: String,
33 /// Whether a generation was actually signalled to stop.
34 pub cancelled: bool,
35 /// A human sentence for the state above, on the same principle as
36 /// the capability reasons in [`crate::health`]: the UI shows the
37 /// server's explanation rather than re-deriving one.
38 pub detail: String,
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn the_verdict_survives_a_round_trip_and_keeps_both_states() {
47 for cancelled in [true, false] {
48 let response = CancelGenerationResponse {
49 request_id: "chatcmpl-1".to_string(),
50 cancelled,
51 detail: "…".to_string(),
52 };
53 let json = serde_json::to_string(&response).unwrap();
54 assert_eq!(
55 serde_json::from_str::<CancelGenerationResponse>(&json).unwrap(),
56 response
57 );
58 // The field must never be elided: a missing `cancelled`
59 // would default to `false` in some clients and `true` in
60 // the reader's head.
61 assert!(json.contains("\"cancelled\""), "{json}");
62 }
63 }
64
65 #[test]
66 fn the_request_names_only_the_id_the_server_already_stated() {
67 let parsed: CancelGenerationRequest =
68 serde_json::from_str(r#"{"request_id":"chatcmpl-abc"}"#).unwrap();
69 assert_eq!(parsed.request_id, "chatcmpl-abc");
70 }
71}