main
py 632 lines 25.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for ``vllm/entrypoints/cohere/api_router.py``.
4
5 Covers:
6
7 * The optional-import guard: ``attach_router`` is a no-op when the
8 ``cohere`` SDK isn't installed.
9 * The env-var opt-in gate: ``attach_router`` is a no-op unless
10 ``VLLM_ENABLE_COHERE_API=1`` is set.
11 * The router wiring: response shapes (JSON + SSE), error translation,
12 and the ``cohere_serving_chat_v2 is None`` fallback (501 Not
13 Implemented).
14 * The render route (``POST /cohere/v2/chat/render``): that the v2 body
15 reaches :class:`ServingRender` as a converted ``ChatCompletionRequest``
16 and that the resulting ``GenerateRequest`` is returned verbatim.
17 """
18
19 import json
20 from argparse import Namespace
21 from collections.abc import AsyncGenerator
22 from http import HTTPStatus
23
24 import pytest
25 from fastapi import FastAPI, HTTPException
26 from fastapi.exceptions import RequestValidationError
27 from fastapi.testclient import TestClient
28
29 from vllm.entrypoints.cohere import api_router as api_router_mod
30 from vllm.entrypoints.cohere.api_router import attach_router
31 from vllm.entrypoints.cohere.protocol import (
32 AssistantMessageResponse,
33 CohereChatV2Response,
34 )
35 from vllm.entrypoints.cohere.serving import CohereServingChatV2
36 from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateRequest
37 from vllm.entrypoints.serve.engine.protocol import ErrorInfo, ErrorResponse
38 from vllm.entrypoints.serve.exception_handling.handlers.http import (
39 http_exception_handler,
40 )
41 from vllm.entrypoints.serve.exception_handling.handlers.validation import (
42 validation_exception_handler,
43 )
44 from vllm.entrypoints.serve.exception_handling.handlers.vllm_error import (
45 vllm_error_handler,
46 )
47 from vllm.exceptions import VLLMError
48 from vllm.sampling_params import SamplingParams
49
50
51 @pytest.fixture(autouse=True)
52 def _enable_cohere_api(monkeypatch):
53 """Auto-enable the Cohere API gate for every test in this module.
54
55 The endpoint is opt-in in production (``VLLM_ENABLE_COHERE_API=1``);
56 every test in this file exercises the enabled path *except* the
57 dedicated gate test in :class:`TestEnvVarGate`, which unsets the
58 flag inside the test body.
59 """
60 monkeypatch.setenv("VLLM_ENABLE_COHERE_API", "1")
61
62
63 # ----------------------------------------------------------------------
64 # Fakes
65 # ----------------------------------------------------------------------
66
67
68 class _Handler:
69 """Minimal stand-in for :class:`CohereServingChatV2` used by the
70 router. Each test sets ``self.result`` to either:
71
72 * a :class:`CohereChatV2Response` (non-streaming JSON path);
73 * an async generator yielding SSE frames (streaming path);
74 * an :class:`ErrorResponse` (error envelope path); or
75 * an exception (router-level 500 path).
76 """
77
78 def __init__(self, result):
79 self.result = result
80
81 async def create_chat_v2(self, request, raw_request):
82 if isinstance(self.result, Exception):
83 raise self.result
84 return self.result
85
86
87 class _RenderChatHandler:
88 """Stand-in for :class:`CohereServingChatV2` on the render route.
89
90 Only ``to_chat_completion_request`` is reachable from
91 ``/cohere/v2/chat/render``, and it delegates to the real conversion
92 classmethod (which needs no engine or tokenizer) so the tests
93 exercise the actual v2 -> ``ChatCompletionRequest`` translation
94 rather than a stub of it.
95 """
96
97 def to_chat_completion_request(self, request):
98 return CohereServingChatV2._convert_v2_to_chat_completion(request)
99
100
101 class _RenderHandler:
102 """Minimal stand-in for :class:`ServingRender`.
103
104 Records the ``ChatCompletionRequest`` it was handed so tests can
105 assert on the conversion, and returns ``self.result`` - either a
106 :class:`GenerateRequest`, an :class:`ErrorResponse`, or an exception
107 to raise.
108 """
109
110 def __init__(self, result):
111 self.result = result
112 self.seen_request = None
113
114 async def render_chat_request(self, request):
115 self.seen_request = request
116 if isinstance(self.result, Exception):
117 raise self.result
118 return self.result
119
120
121 def _generate_request() -> GenerateRequest:
122 return GenerateRequest(
123 request_id="chatcmpl-abc",
124 token_ids=[5, 6, 7],
125 sampling_params=SamplingParams(temperature=0.25),
126 model="m",
127 )
128
129
130 def _build_app(handler: _Handler | None, *, enable_scale_out: bool = True) -> FastAPI:
131 app = FastAPI()
132 app.state.args = Namespace(enable_scale_out=enable_scale_out, tokens_only=False)
133 attach_router(app)
134 app.state.cohere_serving_chat_v2 = handler
135 return app
136
137
138 def _build_render_app(
139 chat_handler, render_handler, *, enable_scale_out: bool = True
140 ) -> FastAPI:
141 app = FastAPI()
142 app.state.args = Namespace(enable_scale_out=enable_scale_out, tokens_only=False)
143 attach_router(app)
144 app.state.cohere_serving_chat_v2 = chat_handler
145 app.state.serving_render = render_handler
146 return app
147
148
149 def _build_app_with_vllm_handlers(handler: _Handler | None) -> FastAPI:
150 """Build a FastAPI app that mirrors the real vLLM setup by installing
151 validation, HTTP, and vLLM exception handlers. The
152 :class:`CohereErrorEnvelopeMiddleware` registered by ``attach_router``
153 is expected to translate any resulting vLLM ``ErrorResponse`` body
154 into the ``CohereError`` wire shape.
155 """
156 app = FastAPI()
157 # ``validation_exception_handler`` reads ``req.app.state.args``; the
158 # real cli builds this via argparse.
159 app.state.args = Namespace(
160 log_error_stack=False, enable_scale_out=True, tokens_only=False
161 )
162 attach_router(app)
163 app.state.cohere_serving_chat_v2 = handler
164 app.exception_handler(RequestValidationError)(validation_exception_handler)
165 app.exception_handler(HTTPException)(http_exception_handler)
166 app.exception_handler(VLLMError)(vllm_error_handler)
167 return app
168
169
170 def _minimal_request_body() -> dict:
171 return {
172 "model": "m",
173 "messages": [{"role": "user", "content": "hi"}],
174 }
175
176
177 # ----------------------------------------------------------------------
178 # Optional-import guard
179 # ----------------------------------------------------------------------
180
181
182 class TestOptionalCohereImport:
183 """``attach_router`` probes for the SDK once at module load (because
184 the route handler uses types imported from ``cohere``) and stashes
185 the result in ``_SDK_AVAILABLE``. Tests simulate the "SDK missing"
186 state by flipping that flag for the duration of the test.
187
188 ``attach_router`` checks the env-var gate before the SDK probe, so
189 the SDK-missing branch is only reachable when the operator opts in
190 via ``VLLM_ENABLE_COHERE_API=1``. The flag-off-and-SDK-missing case
191 below exists to pin down that ordering — the flag-off short-circuits
192 """
193
194 def test_flag_off_and_sdk_missing_stays_silent_about_sdk(self, monkeypatch, caplog):
195 """Flag off doesn't do SDK-missing check.
196
197 When the operator hasn't opted in, ``attach_router`` must not
198 warn about the ``cohere`` SDK being missing: they never asked
199 for the endpoint, so surfacing the SDK gap is misleading noise.
200 Only the flag-off DEBUG message should fire.
201 """
202 monkeypatch.delenv("VLLM_ENABLE_COHERE_API", raising=False)
203 monkeypatch.setattr(api_router_mod, "_SDK_AVAILABLE", False)
204
205 with caplog.at_level("DEBUG", logger="vllm.entrypoints.cohere.api_router"):
206 app = FastAPI()
207 attach_router(app)
208
209 paths = [getattr(r, "path", None) for r in app.routes]
210 assert "/cohere/v2/chat" not in paths
211 # The flag-off short-circuit ran; the SDK check never did.
212 assert not any(
213 "SDK is not installed" in rec.message for rec in caplog.records
214 ), "SDK-missing log leaked despite the flag being off"
215
216 def test_flag_on_but_sdk_missing_logs_warning(self, monkeypatch, caplog):
217 """Misconfiguration path: the operator explicitly opted into the
218 endpoint via ``VLLM_ENABLE_COHERE_API=1`` (already set by the
219 autouse fixture) but forgot to install ``cohere``.
220 """
221 monkeypatch.setattr(api_router_mod, "_SDK_AVAILABLE", False)
222
223 with caplog.at_level("DEBUG", logger="vllm.entrypoints.cohere.api_router"):
224 app = FastAPI()
225 attach_router(app)
226
227 paths = [getattr(r, "path", None) for r in app.routes]
228 assert "/cohere/v2/chat" not in paths
229 warn_sdk_records = [
230 rec
231 for rec in caplog.records
232 if "VLLM_ENABLE_COHERE_API=1" in rec.message
233 and "SDK is not installed" in rec.message
234 ]
235 assert warn_sdk_records, (
236 "expected a WARNING that pairs the opt-in flag with the "
237 "missing SDK so operators notice the misconfiguration"
238 )
239 assert all(rec.levelname == "WARNING" for rec in warn_sdk_records)
240
241 def test_attach_router_registers_route_when_cohere_present(self):
242 app = _build_app(handler=None)
243 paths = [getattr(r, "path", None) for r in app.routes]
244 assert "/cohere/v2/chat" in paths
245
246
247 # ----------------------------------------------------------------------
248 # VLLM_ENABLE_COHERE_API gate
249 # ----------------------------------------------------------------------
250
251
252 class TestEnvVarGate:
253 """The Cohere v2 endpoint is opt-in via ``VLLM_ENABLE_COHERE_API``.
254
255 Even with the SDK installed, :func:`attach_router` must skip route
256 registration and middleware installation unless the env flag is
257 set. The autouse fixture on this module enables the flag by
258 default, so each test here explicitly disables it.
259 """
260
261 def test_attach_router_noop_when_flag_unset(self, monkeypatch, caplog):
262 monkeypatch.delenv("VLLM_ENABLE_COHERE_API", raising=False)
263
264 # The flag-off skip logs at DEBUG on purpose: this is the default
265 # state for every non-Cohere vLLM deployment, so an INFO log on
266 # every server startup would be pointless noise. The test raises
267 # caplog's level accordingly.
268 with caplog.at_level("DEBUG", logger="vllm.entrypoints.cohere.api_router"):
269 app = FastAPI()
270 attach_router(app)
271
272 paths = [getattr(r, "path", None) for r in app.routes]
273 assert "/cohere/v2/chat" not in paths
274 debug_flag_records = [
275 rec
276 for rec in caplog.records
277 if "VLLM_ENABLE_COHERE_API is not set" in rec.message
278 ]
279 assert debug_flag_records, (
280 "expected a DEBUG message that the cohere flag is off"
281 )
282 assert all(rec.levelname == "DEBUG" for rec in debug_flag_records)
283
284 def test_attach_router_noop_when_flag_zero(self, monkeypatch):
285 monkeypatch.setenv("VLLM_ENABLE_COHERE_API", "0")
286
287 app = FastAPI()
288 attach_router(app)
289
290 paths = [getattr(r, "path", None) for r in app.routes]
291 assert "/cohere/v2/chat" not in paths
292
293
294 # ----------------------------------------------------------------------
295 # Endpoint behavior
296 # ----------------------------------------------------------------------
297
298
299 class TestEndpoint:
300 def test_501_when_handler_missing(self):
301 app = _build_app(handler=None)
302 with TestClient(app) as client:
303 r = client.post("/cohere/v2/chat", json=_minimal_request_body())
304 assert r.status_code == HTTPStatus.NOT_IMPLEMENTED
305 body = r.json()
306 assert "does not support" in body["message"]
307 assert "id" not in body # excluded by ``exclude_none=True``
308
309 def test_non_streaming_response_is_json(self):
310 msg = AssistantMessageResponse(content=[{"type": "text", "text": "hello"}])
311 result = CohereChatV2Response(id="r1", finish_reason="COMPLETE", message=msg)
312 app = _build_app(handler=_Handler(result))
313 with TestClient(app) as client:
314 r = client.post("/cohere/v2/chat", json=_minimal_request_body())
315 assert r.status_code == HTTPStatus.OK
316 assert r.headers["content-type"].startswith("application/json")
317 body = r.json()
318 assert body["id"] == "r1"
319 assert body["finish_reason"] == "COMPLETE"
320 assert body["message"]["content"][0]["text"] == "hello"
321
322 def test_streaming_response_is_sse(self):
323 async def _gen() -> AsyncGenerator[str, None]:
324 yield 'data: {"type":"message-start"}\n\n'
325 yield "data: [DONE]\n\n"
326
327 app = _build_app(handler=_Handler(_gen()))
328 with TestClient(app) as client:
329 r = client.post(
330 "/cohere/v2/chat",
331 json={**_minimal_request_body(), "stream": True},
332 )
333 assert r.status_code == HTTPStatus.OK
334 assert r.headers["content-type"].startswith("text/event-stream")
335 body = r.text
336 assert "message-start" in body
337 assert body.rstrip().endswith("[DONE]")
338
339 def test_error_response_translated_to_cohere_envelope(self):
340 err = ErrorResponse(
341 error=ErrorInfo(
342 message="bad request",
343 type="bad_request",
344 code=400,
345 )
346 )
347 app = _build_app(handler=_Handler(err))
348 with TestClient(app) as client:
349 r = client.post("/cohere/v2/chat", json=_minimal_request_body())
350 assert r.status_code == HTTPStatus.BAD_REQUEST
351 body = r.json()
352 assert body == {"message": "bad request"}
353
354 def test_handler_exception_returns_500_envelope(self):
355 app = _build_app(handler=_Handler(RuntimeError("kaboom")))
356 with TestClient(app) as client:
357 r = client.post("/cohere/v2/chat", json=_minimal_request_body())
358 assert r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
359 body = r.json()
360 assert body == {"message": "kaboom"}
361
362 def test_non_json_content_type_rejected(self):
363 """The ``validate_json_request`` dependency raises
364 ``RequestValidationError`` (HTTP 422) for non-JSON content
365 types, matching the behavior of the other vLLM API routers.
366 """
367 app = _build_app(handler=None)
368 with TestClient(app) as client:
369 r = client.post(
370 "/cohere/v2/chat",
371 content=json.dumps(_minimal_request_body()),
372 headers={"content-type": "text/plain"},
373 )
374 assert r.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
375
376 def test_invalid_body_returns_422(self):
377 # ``model`` is required; omit it to trip Pydantic validation.
378 app = _build_app(handler=None)
379 with TestClient(app) as client:
380 r = client.post(
381 "/cohere/v2/chat",
382 json={"messages": [{"role": "user", "content": "hi"}]},
383 )
384 assert r.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
385
386
387 # ----------------------------------------------------------------------
388 # Render endpoint
389 # ----------------------------------------------------------------------
390
391
392 class TestRenderEndpoint:
393 """``POST /cohere/v2/chat/render`` is the Cohere counterpart to
394 ``POST /v1/chat/completions/render``: it converts the v2 body to a
395 ``ChatCompletionRequest`` and hands it to the shared
396 ``ServingRender``, returning the resulting ``GenerateRequest``
397 instead of running generation.
398
399 The route therefore spans *two* app-state handlers, and these tests
400 pin both halves: that the conversion preserves the Cohere-specific
401 request surface, and that the render handler's output (or error)
402 reaches the client unchanged.
403 """
404
405 def test_route_registered(self):
406 app = _build_render_app(_RenderChatHandler(), _RenderHandler(None))
407 paths = [getattr(r, "path", None) for r in app.routes]
408 assert "/cohere/v2/chat/render" in paths
409
410 def test_route_not_registered_when_flag_unset(self):
411 app = _build_app(handler=None, enable_scale_out=False)
412 paths = [getattr(r, "path", None) for r in app.routes]
413 assert "/cohere/v2/chat/render" not in paths
414 assert "/cohere/v2/chat" in paths
415
416 def test_returns_generate_request_json(self):
417 render_handler = _RenderHandler(_generate_request())
418 app = _build_render_app(_RenderChatHandler(), render_handler)
419 with TestClient(app) as client:
420 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
421 assert r.status_code == HTTPStatus.OK
422 assert r.headers["content-type"].startswith("application/json")
423 body = r.json()
424 assert body["request_id"] == "chatcmpl-abc"
425 assert body["token_ids"] == [5, 6, 7]
426 assert body["sampling_params"]["temperature"] == 0.25
427 # The body must be a wire-valid GenerateRequest so callers can
428 # feed it straight back to /v1/generate.
429 assert GenerateRequest.model_validate(body).token_ids == [5, 6, 7]
430
431 def test_cohere_request_surface_survives_conversion(self):
432 """The point of routing through ``CohereServingChatV2`` rather
433 than asking callers to pre-convert: Cohere-only fields
434 (``documents``, ``safety_mode``, ``citation_options``) must land
435 in ``chat_template_kwargs`` so the rendered prompt matches what
436 ``/cohere/v2/chat`` would have produced.
437 """
438 render_handler = _RenderHandler(_generate_request())
439 app = _build_render_app(_RenderChatHandler(), render_handler)
440 body = {
441 **_minimal_request_body(),
442 "documents": [{"id": "doc-1", "data": {"text": "Paris is in France."}}],
443 "safety_mode": "STRICT",
444 "max_tokens": 32,
445 }
446 with TestClient(app) as client:
447 r = client.post("/cohere/v2/chat/render", json=body)
448 assert r.status_code == HTTPStatus.OK
449
450 seen = render_handler.seen_request
451 assert seen is not None
452 assert seen.model == "m"
453 assert seen.messages[0]["content"] == "hi"
454 # ``max_tokens`` matters because ServingRender feeds it to
455 # ``get_max_tokens`` when building sampling params.
456 assert seen.max_completion_tokens == 32
457 kwargs = seen.chat_template_kwargs or {}
458 assert kwargs["documents"][0]["id"] == "doc-1"
459 # The conversion lower-cases safety_mode for the renderer.
460 assert kwargs["safety_mode"] == "strict"
461
462 def test_501_when_chat_handler_missing(self):
463 app = _build_render_app(None, _RenderHandler(_generate_request()))
464 with TestClient(app) as client:
465 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
466 assert r.status_code == HTTPStatus.NOT_IMPLEMENTED
467 assert "does not support" in r.json()["message"]
468
469 def test_501_when_render_handler_missing(self):
470 # A server without ``serving_render`` in app state (e.g. a
471 # non-generate task set) can't render, even though the Cohere
472 # chat handler exists.
473 app = _build_render_app(_RenderChatHandler(), None)
474 with TestClient(app) as client:
475 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
476 assert r.status_code == HTTPStatus.NOT_IMPLEMENTED
477 assert "does not support" in r.json()["message"]
478
479 def test_error_response_translated_to_cohere_envelope(self):
480 err = ErrorResponse(
481 error=ErrorInfo(message="unknown model", type="NotFound", code=404)
482 )
483 app = _build_render_app(_RenderChatHandler(), _RenderHandler(err))
484 with TestClient(app) as client:
485 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
486 assert r.status_code == HTTPStatus.NOT_FOUND
487 assert r.json() == {"message": "unknown model"}
488
489 def test_render_exception_returns_500_envelope(self):
490 app = _build_render_app(
491 _RenderChatHandler(), _RenderHandler(RuntimeError("tokenizer exploded"))
492 )
493 with TestClient(app) as client:
494 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
495 assert r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
496 assert r.json() == {"message": "tokenizer exploded"}
497
498 def test_conversion_exception_returns_500_envelope(self):
499 """Conversion runs inside the same ``try`` as the render call, so
500 a bad v2 body that slips past Pydantic still yields the Cohere
501 error envelope rather than an unhandled 500 with vLLM's shape.
502 """
503
504 class _Boom:
505 def to_chat_completion_request(self, request):
506 raise ValueError("unconvertible message")
507
508 app = _build_render_app(_Boom(), _RenderHandler(_generate_request()))
509 with TestClient(app) as client:
510 r = client.post("/cohere/v2/chat/render", json=_minimal_request_body())
511 assert r.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
512 assert r.json() == {"message": "unconvertible message"}
513
514 def test_invalid_body_returns_422(self):
515 app = _build_render_app(_RenderChatHandler(), _RenderHandler(None))
516 with TestClient(app) as client:
517 r = client.post(
518 "/cohere/v2/chat/render",
519 json={"messages": [{"role": "user", "content": "hi"}]},
520 )
521 assert r.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
522
523
524 # ----------------------------------------------------------------------
525 # CohereErrorEnvelopeMiddleware
526 # ----------------------------------------------------------------------
527
528
529 class TestCohereErrorEnvelope:
530 """When the app installs vLLM's global exception handlers, validation
531 and HTTP errors escape as ``ErrorResponse`` bodies before the route
532 handler runs. The middleware installed by ``attach_router`` must
533 normalise those bodies to the ``CohereError`` shape declared on the
534 endpoint's OpenAPI ``responses`` map so schema-conformance tests
535 (``test_openai_schema.py``) don't see a mismatch on ``/cohere/*``
536 responses.
537 """
538
539 def test_validation_error_body_is_cohere_shaped(self):
540 # ``model=""`` and ``messages=[]`` trip our custom field
541 # validators, which raise VLLMValidationError and are routed
542 # through ``vllm_error_handler`` in the real vLLM
543 # server (producing the ``{"error": {...}}`` shape).
544 app = _build_app_with_vllm_handlers(handler=None)
545 with TestClient(app) as client:
546 r = client.post("/cohere/v2/chat", json={"messages": [], "model": ""})
547 assert r.status_code == HTTPStatus.BAD_REQUEST
548 body = r.json()
549 # ``CohereError`` has ``message`` at the top level, not nested
550 # under an ``error`` envelope.
551 assert "error" not in body
552 assert "message" in body
553 assert isinstance(body["message"], str) and body["message"]
554
555 def test_http_error_body_is_cohere_shaped(self):
556 # A raised ``HTTPException`` from anywhere in the request cycle
557 # is routed through ``http_exception_handler`` (producing the
558 # ``ErrorResponse`` shape) and must be translated.
559 app = _build_app_with_vllm_handlers(handler=None)
560
561 @app.get("/cohere/v2/boom")
562 async def _boom():
563 raise HTTPException(status_code=418, detail="teapot")
564
565 with TestClient(app) as client:
566 r = client.get("/cohere/v2/boom")
567 assert r.status_code == 418
568 body = r.json()
569 assert body == {"message": "teapot"}
570
571 def test_non_cohere_path_is_not_translated(self):
572 app = _build_app_with_vllm_handlers(handler=None)
573
574 @app.get("/v1/other")
575 async def _other():
576 raise HTTPException(status_code=400, detail="nope")
577
578 with TestClient(app) as client:
579 r = client.get("/v1/other")
580 assert r.status_code == HTTPStatus.BAD_REQUEST
581 body = r.json()
582 # Non-cohere paths keep the vLLM ``ErrorResponse`` shape.
583 assert "error" in body
584 assert body["error"]["message"] == "nope"
585
586 def test_streaming_response_passes_through(self):
587 # SSE responses have content-type text/event-stream; the
588 # middleware must never buffer these (which would break
589 # streaming) even though they're on ``/cohere/*``.
590 async def _gen() -> AsyncGenerator[str, None]:
591 yield 'data: {"type":"message-start"}\n\n'
592 yield "data: [DONE]\n\n"
593
594 app = _build_app_with_vllm_handlers(handler=_Handler(_gen()))
595 with TestClient(app) as client:
596 r = client.post(
597 "/cohere/v2/chat",
598 json={**_minimal_request_body(), "stream": True},
599 )
600 assert r.status_code == HTTPStatus.OK
601 assert r.headers["content-type"].startswith("text/event-stream")
602 assert "message-start" in r.text
603 assert r.text.rstrip().endswith("[DONE]")
604
605 def test_already_cohere_shaped_body_passes_through(self):
606 # When the handler returns an ``ErrorResponse`` the route
607 # itself translates it to ``CohereError``; the middleware sees
608 # the ``CohereError`` shape and must leave it alone.
609 err = ErrorResponse(
610 error=ErrorInfo(message="already cohere", type="Bad Request", code=400)
611 )
612 app = _build_app_with_vllm_handlers(handler=_Handler(err))
613 with TestClient(app) as client:
614 r = client.post("/cohere/v2/chat", json=_minimal_request_body())
615 assert r.status_code == HTTPStatus.BAD_REQUEST
616 body = r.json()
617 # No ``error`` wrapper: the route already emitted the wire shape.
618 assert body == {"message": "already cohere"}
619
620 def test_request_id_preserved_in_translated_body(self):
621 # Client-provided ``X-Request-Id`` should be echoed as
622 # ``CohereError.id`` so callers can correlate failures.
623 app = _build_app_with_vllm_handlers(handler=None)
624 with TestClient(app) as client:
625 r = client.post(
626 "/cohere/v2/chat",
627 json={"messages": [], "model": ""},
628 headers={"X-Request-Id": "req-abc"},
629 )
630 assert r.status_code == HTTPStatus.BAD_REQUEST
631 body = r.json()
632 assert body.get("id") == "req-abc"