main
py 135 lines 4.89 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for :mod:`vllm.entrypoints.cohere.cohere_chat_message`.
4
5 The module hosts the citation-carrying subclasses of the OpenAI chat
6 completion protocol (``CohereChatMessage`` / ``CohereDeltaMessage``) plus
7 the shared ``Citation`` / ``CitationSource`` shapes. These tests pin:
8
9 1. Serialization behavior we own (empty-``citations`` cleanup on the
10 subclass ``_serialize``, ``exclude_none`` shape, round-trip).
11 2. The constraint that ``type`` discriminators reject unknown values.
12 """
13
14 import pytest
15 from pydantic import ValidationError
16
17 from vllm.entrypoints.cohere.cohere_chat_message import (
18 Citation,
19 CitationSource,
20 CohereChatMessage,
21 CohereDeltaMessage,
22 )
23
24 # ======================================================================
25 # CitationSource
26 # ======================================================================
27
28
29 class TestCitationSource:
30 def test_invalid_type_rejected(self):
31 with pytest.raises(ValidationError):
32 CitationSource(type="other")
33
34 def test_none_fields_excluded_from_dump(self):
35 s = CitationSource(type="document", id="d1")
36 assert s.model_dump(exclude_none=True) == {
37 "type": "document",
38 "id": "d1",
39 }
40
41 def test_resolved_source_survives_json_round_trip(self):
42 # The parser produces fully-resolved sources (see
43 # ``_melody_sources_to_vllm`` in
44 # ``vllm/reasoning/cohere_command_reasoning_parser.py``) which
45 # then flow through the internal streaming pipeline (parser ->
46 # OpenAI stream chunk -> ``_chat_completion_stream_to_v2``).
47 # Every field must round-trip through ``model_dump_json`` at
48 # that layer or the wire event ends up missing pieces.
49 src = CitationSource(
50 type="tool",
51 id="res_a0",
52 tool_output={"id": "res_a0", "text": "r"},
53 )
54 src2 = CitationSource.model_validate_json(src.model_dump_json())
55 assert src2.type == "tool"
56 assert src2.id == "res_a0"
57 assert src2.tool_output == {"id": "res_a0", "text": "r"}
58
59
60 # ======================================================================
61 # Citation
62 # ======================================================================
63
64
65 class TestCitation:
66 def test_invalid_type_rejected(self):
67 with pytest.raises(ValidationError):
68 Citation(type="OTHER")
69
70 def test_dump_excludes_none_fields(self):
71 c = Citation(start=0, end=5, text="hello")
72 dumped = c.model_dump(exclude_none=True)
73 assert dumped == {
74 "start": 0,
75 "end": 5,
76 "text": "hello",
77 "sources": [],
78 }
79
80
81 # ======================================================================
82 # CohereDeltaMessage
83 # ======================================================================
84
85
86 class TestCohereDeltaMessage:
87 def test_citations_omitted_from_dump_when_none(self):
88 # Non-grounded deltas should never carry ``citations: null`` on
89 # the wire even when routed through the Cohere subclass.
90 d = CohereDeltaMessage(content="hello")
91 dumped = d.model_dump(exclude_none=True)
92 assert "citations" not in dumped
93
94 def test_citations_dump_round_trip(self):
95 d = CohereDeltaMessage(citations=[Citation(start=0, end=5, text="hi")])
96 dumped = d.model_dump(exclude_none=True)
97 assert dumped["citations"][0]["text"] == "hi"
98 # Round-trip through the subclass to confirm the field validates.
99 d2 = CohereDeltaMessage.model_validate(dumped)
100 assert d2.citations[0].text == "hi"
101
102
103 # ======================================================================
104 # CohereChatMessage
105 # ======================================================================
106
107
108 class TestCohereChatMessage:
109 def test_citations_omitted_from_dump_when_none(self):
110 m = CohereChatMessage(role="assistant", content="hi")
111 dumped = m.model_dump(exclude_none=True)
112 assert "citations" not in dumped
113
114 def test_citations_dump_round_trip(self):
115 m = CohereChatMessage(
116 role="assistant",
117 content="hello",
118 citations=[
119 Citation(
120 start=0,
121 end=5,
122 text="hello",
123 sources=[CitationSource(type="document", id="d1")],
124 )
125 ],
126 )
127 dumped = m.model_dump(exclude_none=True)
128 assert dumped["role"] == "assistant"
129 assert dumped["content"] == "hello"
130 assert dumped["citations"][0]["text"] == "hello"
131 assert dumped["citations"][0]["sources"][0]["id"] == "d1"
132 # Round-trip through the subclass to confirm the field validates.
133 m2 = CohereChatMessage.model_validate(dumped)
134 assert m2.citations[0].text == "hello"
135 assert m2.citations[0].sources[0].id == "d1"