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
//! Falsifiers for aprender#2375 finding 4 — `POST /v1/chat/completions/stream`.
//!
//! The route is registered unconditionally next to `/v1/chat/completions`
//! (`router.rs`) and 0.63.0 answered it with
//! `{"error":"Model registry error: No model available"}` HTTP 404 on a server
//! whose `/health` said `model_loaded:true` and whose `/v1/chat/completions`
//! returned a full completion in the same second. Cause: the handler resolved
//! ONLY the dense f32 `Model` via `AppState::get_model`, which is `None` for
//! every `apr serve run model.gguf` — the weights live in `quantized_model` —
//! and it mapped *every* resolution failure to 404.
//!
//! Two client-observable claims are asserted here:
//!
//! 1. On the standard quantized deployment the route STREAMS (200,
//! `text/event-stream`, `[DONE]`-terminated) instead of 404ing, and it
//! delivers the same text as `/v1/chat/completions` with `"stream":true`.
//! Two chat routes on one server must not disagree about what the model
//! said.
//! 2. When the server genuinely has no model, the answer is 503 — the status
//! `/v1/predict` and `/v1/gpu/warmup` already use for that condition, and
//! the one `model_resolution_status` mandates (aprender#2376 finding 5).
//! 404 told a client "this route does not exist" about a route that does.
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::util::ServiceExt;
use crate::api::{create_router, AppState};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async fn send(state: AppState, uri: &str, json: &str) -> axum::response::Response {
create_router(state)
.oneshot(
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(json.to_string()))
.expect("build request"),
)
.await
.expect("dispatch")
}
fn content_type(response: &axum::response::Response) -> String {
response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string()
}
async fn body_text(response: axum::response::Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("read body");
String::from_utf8_lossy(&bytes).into_owned()
}
/// Concatenate `choices[0].delta.content` across an SSE body, the way every
/// OpenAI SDK reassembles a stream.
fn streamed_text(body: &str) -> String {
body.lines()
.filter_map(|line| line.strip_prefix("data: "))
.filter(|payload| payload.trim() != "[DONE]")
.filter_map(|payload| serde_json::from_str::<serde_json::Value>(payload).ok())
.filter_map(|frame| {
frame["choices"][0]["delta"]["content"]
.as_str()
.map(str::to_string)
})
.collect()
}
/// A request that pins sampling to greedy so the two routes are comparable.
/// `GREEDY_BODY` closes it; `GREEDY_STREAMING_BODY` adds the explicit flag the
/// `/v1/chat/completions` form needs.
const GREEDY_FIELDS: &str = r#"{"model":"default","messages":[{"role":"user","content":"token5 token6"}],"max_tokens":4,"temperature":0.0"#;
fn greedy_body() -> String {
format!("{GREEDY_FIELDS}}}")
}
fn greedy_streaming_body() -> String {
format!("{GREEDY_FIELDS},\"stream\":true}}")
}
// ---------------------------------------------------------------------------
// Claim 1: the route serves the deployment `apr serve run model.gguf` builds
// ---------------------------------------------------------------------------
#[cfg(feature = "gpu")]
#[tokio::test]
async fn chat_completions_stream_serves_a_quantized_server() {
use super::native_routes_2376::quantized_state;
let response = send(
quantized_state(),
"/v1/chat/completions/stream",
&greedy_body(),
)
.await;
let status = response.status();
let ctype = content_type(&response);
let body = body_text(response).await;
assert_eq!(
status,
StatusCode::OK,
"a quantized server serves /v1/chat/completions; the /stream sibling \
answered {status} with {body}"
);
assert!(
ctype.starts_with("text/event-stream"),
"a streaming route must be SSE-framed, got {ctype:?}"
);
assert!(
body.trim_end().ends_with("data: [DONE]"),
"an OpenAI stream terminates with the [DONE] sentinel; got:\n{body}"
);
}
/// The two chat routes on one server must not disagree about the model output.
///
/// Pre-fix this could not even be compared: `/v1/chat/completions` answered 200
/// with text while `/v1/chat/completions/stream` answered 404 with an error
/// envelope.
#[cfg(feature = "gpu")]
#[tokio::test]
async fn stream_route_and_stream_flag_deliver_the_same_text() {
use super::native_routes_2376::quantized_state;
let via_flag = body_text(
send(
quantized_state(),
"/v1/chat/completions",
&greedy_streaming_body(),
)
.await,
)
.await;
let via_route = body_text(
send(
quantized_state(),
"/v1/chat/completions/stream",
&greedy_body(),
)
.await,
)
.await;
let expected = streamed_text(&via_flag);
assert!(
via_flag.trim_end().ends_with("data: [DONE]"),
"control: the flag form must stream:\n{via_flag}"
);
assert_eq!(
streamed_text(&via_route),
expected,
"the /stream route delivered different text than the stream flag on the \
same server and request:\nroute body:\n{via_route}\nflag body:\n{via_flag}"
);
}
// ---------------------------------------------------------------------------
// Non-regression: the dense backend must survive the same request
// ---------------------------------------------------------------------------
/// `temperature: 0` — the canonical OpenAI deterministic request — must be
/// served on the dense `Model` path by BOTH chat routes.
///
/// The `/stream` handler carried PMAT-790's `temperature == 0` normalization
/// and `registry_fallback` (the dense backend of `/v1/chat/completions`) did
/// not, so the same request was 200 on one route and 500
/// ("Temperature must be a positive finite number") on the other. Folding the
/// routes together without folding the config resolvers together would have
/// converted the working route into the broken one.
#[tokio::test]
async fn temperature_zero_is_served_on_both_chat_routes() {
for uri in ["/v1/chat/completions", "/v1/chat/completions/stream"] {
let state = AppState::demo().expect("dense demo AppState");
let response = send(state, uri, &greedy_streaming_body()).await;
let status = response.status();
let body = body_text(response).await;
assert_eq!(
status,
StatusCode::OK,
"{uri} refused temperature 0 with {status}: {body}"
);
assert!(
body.trim_end().ends_with("data: [DONE]"),
"{uri} must stream to completion; got:\n{body}"
);
}
}
// ---------------------------------------------------------------------------
// Claim 2: "no model loaded" is a server condition — 503, never 404
// ---------------------------------------------------------------------------
#[tokio::test]
async fn stream_route_with_no_model_is_503_not_404() {
let state = AppState::demo_mock().expect("model-less AppState");
let response = send(state, "/v1/chat/completions/stream", &greedy_body()).await;
let status = response.status();
let body = body_text(response).await;
assert_eq!(
status,
StatusCode::SERVICE_UNAVAILABLE,
"a mounted route with nothing to serve is 503 (what /v1/predict and \
/v1/gpu/warmup answer); 404 claims the route does not exist. Got \
{status} with {body}"
);
}
/// An unknown model NAME is still a client error: 404 must survive for the case
/// it is actually correct for, or the fix above would have traded one wrong
/// status for another.
#[tokio::test]
async fn stream_route_with_an_unknown_model_name_is_still_404() {
let mut state = AppState::demo_mock().expect("model-less AppState");
state.registry = Some(std::sync::Arc::new(crate::registry::ModelRegistry::new(1)));
let response = send(
state,
"/v1/chat/completions/stream",
r#"{"model":"no-such-model","messages":[{"role":"user","content":"token5"}],"max_tokens":2,"temperature":0.0}"#,
)
.await;
let status = response.status();
let body = body_text(response).await;
assert_eq!(
status,
StatusCode::NOT_FOUND,
"the client named a model this server does not have; that is 404. Got \
{status} with {body}"
);
}