Skip to content

vllm.entrypoints.cohere.serving

Cohere Chat v2 API serving handler.

Implements POST /cohere/v2/chat by translating the incoming Cohere v2 request into a standard :class:ChatCompletionRequest and delegating to :class:OpenAIServingChat. The actual prompt rendering is handled by vLLM's renderer pipeline (vllm.renderers):

  • For Cohere Command-family models, set --tokenizer-mode cohere and the :class:vllm.renderers.cohere.CohereRenderer will template the request via the cohere_melody library (render_cmd3 / render_cmd4) and surface citations through the Cohere-scoped :class:CohereChatMessage / :class:CohereDeltaMessage subclasses (see :mod:vllm.entrypoints.cohere.cohere_chat_message).
  • For any other model the default Jinja-based :class:HfRenderer is used, and the endpoint behaves as a plain v2-shaped wrapper around chat completions.

Classes:

CohereServingChatV2

Bases: OpenAIServingChat

Handler for the Cohere Chat v2 API (POST /cohere/v2/chat).

The handler is intentionally thin: it converts the v2 request into a :class:ChatCompletionRequest (preserving Cohere-specific fields such as documents, safety_mode and citation_options via chat_template_kwargs) and delegates to the underlying chat completion machinery. All Cohere-specific templating happens in the renderer (:class:vllm.renderers.cohere.CohereRenderer) when the engine is started with --tokenizer-mode cohere.

Citations flow through CohereChatMessage / CohereDeltaMessage subclasses (see :mod:vllm.entrypoints.cohere.cohere_chat_message) so the OpenAI-shared :class:ChatMessage / :class:DeltaMessage keep their declared schemas unchanged. Non-streaming responses pick up the subclass by overriding :meth:_create_chat_message; streaming deltas are emitted directly by the Cohere reasoning parser.

Methods:

Source code in vllm/entrypoints/cohere/serving.py
 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
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
class CohereServingChatV2(OpenAIServingChat):
    """Handler for the Cohere Chat v2 API (``POST /cohere/v2/chat``).

    The handler is intentionally thin: it converts the v2 request into a
    :class:`ChatCompletionRequest` (preserving Cohere-specific fields such
    as ``documents``, ``safety_mode`` and ``citation_options`` via
    ``chat_template_kwargs``) and delegates to the underlying chat
    completion machinery. All Cohere-specific templating happens in the
    renderer (:class:`vllm.renderers.cohere.CohereRenderer`) when the
    engine is started with ``--tokenizer-mode cohere``.

    Citations flow through ``CohereChatMessage`` / ``CohereDeltaMessage``
    subclasses (see :mod:`vllm.entrypoints.cohere.cohere_chat_message`) so
    the OpenAI-shared :class:`ChatMessage` / :class:`DeltaMessage` keep
    their declared schemas unchanged. Non-streaming responses pick up
    the subclass by overriding :meth:`_create_chat_message`; streaming
    deltas are emitted directly by the Cohere reasoning parser.
    """

    def __init__(
        self,
        engine_client: EngineClient,
        models: OpenAIServingModels,
        response_role: str,
        *,
        online_renderer: OnlineRenderer,
        request_logger: RequestLogger | None,
        chat_template: str | None,
        chat_template_content_format: ChatTemplateContentFormatOption,
        return_tokens_as_token_ids: bool = False,
        reasoning_parser: str = "",
        enable_auto_tools: bool = False,
        tool_parser: str | None = None,
        enable_prompt_tokens_details: bool = False,
        enable_force_include_usage: bool = False,
        default_chat_template_kwargs: dict[str, Any] | None = None,
        is_reasoning_model: bool = True,
    ) -> None:
        super().__init__(
            engine_client=engine_client,
            models=models,
            response_role=response_role,
            online_renderer=online_renderer,
            request_logger=request_logger,
            chat_template=chat_template,
            chat_template_content_format=chat_template_content_format,
            return_tokens_as_token_ids=return_tokens_as_token_ids,
            reasoning_parser=reasoning_parser,
            enable_auto_tools=enable_auto_tools,
            tool_parser=tool_parser,
            enable_prompt_tokens_details=enable_prompt_tokens_details,
            enable_force_include_usage=enable_force_include_usage,
            default_chat_template_kwargs=default_chat_template_kwargs,
        )
        # Controls how the assistant's chain-of-thought is surfaced on
        # turns that also contain tool calls.
        #
        # - ``True`` (default): the model is a reasoning Command-family
        #   model; reasoning is always surfaced as a ``thinking`` content
        #   block (non-streaming) or as ``content-start`` / ``content-
        #   delta`` events for a thinking block (streaming), regardless of
        #   whether tool calls also appear.
        # - ``False``: the model is an older non-reasoning Command model
        #   that uses Cohere's ``tool_plan`` field for its chain-of-
        #   thought before tool calls; reasoning is surfaced as
        #   ``tool_plan`` (non-streaming) or as ``tool-plan-delta`` events
        #   (streaming) on tool-call turns, and the thinking content block
        #   is dropped.
        #
        # TODO: replace this manual flag with automatic detection from the
        # model's capabilities config once that exists.
        self._is_reasoning_model = is_reasoning_model

    # ------------------------------------------------------------------
    # Public entry point
    # ------------------------------------------------------------------

    async def create_chat_v2(
        self,
        request: CohereChatV2Request,
        raw_request: Request | None = None,
    ) -> AsyncGenerator[str, None] | CohereChatV2Response | ErrorResponse:
        """Implements ``POST /cohere/v2/chat``."""
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(
                "Received Cohere v2 chat request %s", request.model_dump_json()
            )

        chat_req = self._convert_v2_to_chat_completion(request)
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(
                "Converted Cohere v2 -> ChatCompletion: %s",
                chat_req.model_dump_json(),
            )

        generator = await self.create_chat_completion(chat_req, raw_request)

        match generator:
            case ErrorResponse():
                return generator
            case ChatCompletionResponse():
                return self._chat_completion_to_v2(generator, request)
            case _:
                return self._chat_completion_stream_to_v2(generator, request)

    # ==================================================================
    # Request conversion: Cohere V2 -> ChatCompletionRequest
    # ==================================================================

    @classmethod
    def _convert_v2_to_chat_completion(
        cls, request: CohereChatV2Request
    ) -> ChatCompletionRequest:
        openai_messages: list[dict[str, Any]] = []
        cls._convert_messages(request.messages, openai_messages)

        chat_req = cls._build_base_chat_completion(request, openai_messages)
        cls._apply_streaming_options(chat_req, request)
        cls._apply_response_format(chat_req, request)
        cls._apply_tools(chat_req, request)
        cls._apply_tool_choice(chat_req, request)
        cls._apply_cohere_template_kwargs(chat_req, request)
        return chat_req

    @classmethod
    def _convert_messages(
        cls,
        messages: list,
        openai_messages: list[dict[str, Any]],
    ) -> None:
        for msg in messages:
            match msg:
                case SystemChatMessageV2():
                    openai_messages.append(
                        {
                            "role": "system",
                            "content": cls._coerce_text_content(msg.content),
                        }
                    )
                case UserChatMessageV2():
                    openai_messages.append(cls._convert_user_message(msg))
                case AssistantChatMessageV2():
                    openai_messages.append(cls._convert_assistant_message(msg))
                case ToolChatMessageV2():
                    openai_messages.append(cls._convert_tool_message(msg))
                case _:  # pragma: no cover - guarded by Pydantic discriminator
                    raise ValueError(f"Unsupported Cohere v2 message: {msg!r}")

    @staticmethod
    def _coerce_text_content(content: str | list[Any]) -> str:
        if isinstance(content, str):
            return content
        parts: list[str] = []
        for block in content:
            text = getattr(block, "text", None)
            if text:
                parts.append(text)
        return "".join(parts)

    @classmethod
    def _convert_user_message(cls, msg: UserChatMessageV2) -> dict[str, Any]:
        if isinstance(msg.content, str):
            return {"role": "user", "content": msg.content}

        # Discriminate by ``type`` rather than isinstance so we don't have
        # to import every individual ``*Content`` variant from the SDK -
        # the union (``UserMessageV2Content``) covers both ``TextContent``
        # and ``ImageUrlContent`` and both expose ``type`` as a Literal.
        content_parts: list[dict[str, Any]] = []
        for block in msg.content:
            if block.type == "text":
                content_parts.append({"type": "text", "text": block.text})
            elif block.type == "image_url":
                image_url: dict[str, Any] = {"url": block.image_url.url}
                if getattr(block.image_url, "detail", None) is not None:
                    image_url["detail"] = block.image_url.detail
                content_parts.append(
                    {
                        "type": "image_url",
                        "image_url": image_url,
                    }
                )
        if len(content_parts) == 1 and content_parts[0]["type"] == "text":
            return {"role": "user", "content": content_parts[0]["text"]}
        return {"role": "user", "content": content_parts}

    @classmethod
    def _convert_assistant_message(cls, msg: AssistantChatMessageV2) -> dict[str, Any]:
        out: dict[str, Any] = {"role": "assistant"}

        # Cohere splits reasoning out into ``thinking`` content blocks. We
        # collapse them back into the OpenAI ``reasoning`` field for the
        # downstream chat template, while text blocks become ``content``.
        text_parts: list[str] = []
        thinking_parts: list[str] = []
        if isinstance(msg.content, str):
            text_parts.append(msg.content)
        elif msg.content is not None:
            for block in msg.content:
                if block.type == "text":
                    text_parts.append(block.text)
                elif block.type == "thinking":
                    thinking_parts.append(block.thinking)

        # ``tool_plan`` is Cohere's chain-of-thought emitted alongside tool
        # calls; preserve it as reasoning so templates that expect a
        # planning block still see it.
        if msg.tool_plan:
            thinking_parts.append(msg.tool_plan)

        if text_parts:
            out["content"] = "".join(text_parts)
        if thinking_parts:
            out["reasoning"] = "".join(thinking_parts)

        if msg.tool_calls:
            out["tool_calls"] = [
                {
                    "id": tc.id,
                    "type": "function",
                    "function": {
                        "name": (tc.function.name if tc.function else "") or "",
                        "arguments": (tc.function.arguments if tc.function else None)
                        or "{}",
                    },
                }
                for tc in msg.tool_calls
            ]
        return out

    @classmethod
    def _convert_tool_message(cls, msg: ToolChatMessageV2) -> dict[str, Any]:
        if isinstance(msg.content, str):
            return {
                "role": "tool",
                "tool_call_id": msg.tool_call_id,
                "content": msg.content,
            }

        # When the tool result is text-only, flatten to a string for
        # maximum compatibility with standard chat templates. When it
        # includes documents, preserve them as structured content parts
        # so the cohere renderer can surface them as grounding sources
        # (the :class:`CohereRenderer` understands
        # ``{type: document, document: {...}}`` blocks). Non-cohere
        # renderers may not honor document blocks, but that matches
        # the broader "documents are no-op for OSS models" contract
        # documented on this endpoint.
        #
        # Tool message content uses ``ToolMessageV2Content``, which is a
        # union of ``TextToolContent`` and ``DocumentToolContent`` -
        # distinct from the user-message text/image union. We discriminate
        # on the ``type`` literal so we don't have to import each variant.
        has_documents = any(block.type == "document" for block in msg.content)
        if not has_documents:
            text = "\n".join(
                block.text for block in msg.content if block.type == "text"
            )
            return {
                "role": "tool",
                "tool_call_id": msg.tool_call_id,
                "content": text,
            }

        parts: list[dict[str, Any]] = []
        for block in msg.content:
            if block.type == "text":
                parts.append({"type": "text", "text": block.text})
            elif block.type == "document":
                parts.append(
                    {
                        "type": "document",
                        "document": block.document.model_dump(exclude_none=True),
                    }
                )
        return {
            "role": "tool",
            "tool_call_id": msg.tool_call_id,
            "content": parts,
        }

    @classmethod
    def _build_base_chat_completion(
        cls,
        request: CohereChatV2Request,
        openai_messages: list[dict[str, Any]],
    ) -> ChatCompletionRequest:
        return ChatCompletionRequest(
            model=request.model,
            messages=openai_messages,
            max_tokens=request.max_tokens,
            max_completion_tokens=request.max_tokens,
            stop=request.stop_sequences,
            temperature=request.temperature,
            top_p=request.p,
            top_k=request.k,
            seed=request.seed,
            frequency_penalty=request.frequency_penalty,
            presence_penalty=request.presence_penalty,
            logprobs=request.logprobs,
            priority=request.priority or 0,
            kv_transfer_params=request.kv_transfer_params,
            chat_template_kwargs=request.chat_template_kwargs,
        )

    @classmethod
    def _apply_streaming_options(
        cls,
        chat_req: ChatCompletionRequest,
        request: CohereChatV2Request,
    ) -> None:
        if request.stream:
            chat_req.stream = True
            chat_req.stream_options = StreamOptions.model_validate(
                {"include_usage": True}
            )

    @classmethod
    def _apply_response_format(
        cls,
        chat_req: ChatCompletionRequest,
        request: CohereChatV2Request,
    ) -> None:
        rf = request.response_format
        if rf is None or rf.type == "text":
            return
        chat_req.response_format = ResponseFormat(
            type="json_schema" if rf.json_schema else "json_object",
            json_schema=(
                JsonSchemaResponseFormat(
                    name="cohere_v2_json_schema",
                    json_schema=rf.json_schema,
                )
                if rf.json_schema
                else None
            ),
        )

    @classmethod
    def _apply_tools(
        cls,
        chat_req: ChatCompletionRequest,
        request: CohereChatV2Request,
    ) -> None:
        if not request.tools:
            return
        # Cohere's ``strict_tools`` is the spec-equivalent of OpenAI's
        # per-function ``strict`` flag: when true the API guarantees tool
        # call arguments match the declared JSON schema. We surface it in
        # both places so OpenAI-shaped consumers see it on the function
        # definition and the cohere renderer can read it back from
        # ``chat_template_kwargs`` for cmd3/cmd4 preamble selection.
        strict = bool(request.strict_tools)
        chat_req.tools = [
            ChatCompletionToolsParam.model_validate(
                {
                    "type": "function",
                    "function": {
                        "name": tool.function.name,
                        "description": tool.function.description,
                        "parameters": tool.function.parameters,
                        **({"strict": True} if strict else {}),
                    },
                }
            )
            for tool in request.tools
        ]

    @classmethod
    def _apply_tool_choice(
        cls,
        chat_req: ChatCompletionRequest,
        request: CohereChatV2Request,
    ) -> None:
        # Cohere v2's ``tool_choice`` only admits ``REQUIRED`` / ``NONE``
        # (see :data:`CohereToolChoice`); named-tool selection is not
        # part of the v2 spec. Map both onto OpenAI's equivalents.
        if request.tool_choice == "REQUIRED":
            chat_req.tool_choice = "required"
        elif request.tool_choice == "NONE":
            chat_req.tool_choice = "none"
        elif chat_req.tools:
            # Mirrors Cohere's "free choice" default when tools are present.
            chat_req.tool_choice = "auto"

    @classmethod
    def _apply_cohere_template_kwargs(
        cls,
        chat_req: ChatCompletionRequest,
        request: CohereChatV2Request,
    ) -> None:
        """Forward Cohere-specific request fields into ``chat_template_kwargs``.

        The :class:`vllm.renderers.cohere.CohereRenderer` consumes these
        kwargs to drive ``cohere_melody.render_cmd3`` / ``render_cmd4``.
        Other renderers ignore unknown kwargs, so this is also a no-op for
        non-cohere ``--tokenizer-mode`` settings.
        """
        kwargs = dict(chat_req.chat_template_kwargs or {})

        if request.documents:
            documents: list[dict[str, Any]] = []
            for idx, doc in enumerate(request.documents):
                if isinstance(doc, str):
                    documents.append({"id": f"doc_{idx}", "data": {"text": doc}})
                else:
                    documents.append(
                        {
                            "id": doc.id or f"doc_{idx}",
                            "data": (
                                doc.data
                                if isinstance(doc.data, dict)
                                else {"text": doc.data}
                            ),
                        }
                    )
            kwargs.setdefault("documents", documents)

        if request.safety_mode is not None:
            kwargs.setdefault("safety_mode", str(request.safety_mode).lower())
        if request.citation_options is not None:
            kwargs.setdefault(
                "citation_options",
                request.citation_options.model_dump(exclude_none=True),
            )
        if request.thinking is not None:
            kwargs.setdefault(
                "thinking",
                request.thinking.model_dump(exclude_none=True),
            )

        # ``strict_tools`` is intentionally NOT forwarded here. It's a
        # decoder-guidance flag that ``_apply_tools`` already maps onto
        # per-function OpenAI ``strict: true`` on ``chat_req.tools``;
        # ``cohere_melody.render_cmd3``/``render_cmd4`` have no
        # ``strict_tools`` knob, and the renderer's residual-forward path
        # would otherwise surface it as a Jinja variable ``{{ strict_tools }}``
        # that templates have no defined use for.

        # Citations on assistant messages in the request don't fit
        # OpenAI's ``ChatMessage`` / ``ConversationMessage`` schemas, so
        # we forward them under the reserved ``MESSAGES_CITATIONS_KEY``
        # chat_template_kwargs entry keyed by request-message index.
        # See ``_sdk_citation_to_melody`` for the id-resolution rules.
        citations_by_index = cls._collect_message_citations(request)
        if citations_by_index:
            kwargs.setdefault(MESSAGES_CITATIONS_KEY, citations_by_index)

        # Forward the ``(bucket, result_idx) -> resolved wire source``
        # map to the reasoning parser via chat_template_kwargs. The
        # parser (constructed with the same kwargs) uses it to attach
        # real document ids / types / payloads to every source it
        # emits, so citation deltas already carry SDK-shape sources by
        # the time they reach the streaming loop. See
        # ``_build_position_to_source`` for the numbering rule and
        # ``_melody_sources_to_vllm`` in the reasoning parser for the
        # consumer.
        position_to_source = cls._build_position_to_source(request)
        if position_to_source:
            kwargs.setdefault(POSITION_TO_SOURCE_KEY, position_to_source)

        if kwargs:
            chat_req.chat_template_kwargs = kwargs

    @classmethod
    def _collect_message_citations(
        cls, request: CohereChatV2Request
    ) -> dict[int, list[dict[str, Any]]]:
        """Extract ``AssistantChatMessageV2.citations`` keyed by message index.

        Returns an empty dict when no assistant message in the request
        carries any resolvable citations, so callers can use truthiness
        as the "should I emit the ``MESSAGES_CITATIONS_KEY`` entry?"
        test. Citations whose sources can't be resolved are dropped
        individually; messages that end up with zero surviving
        citations are omitted from the result.
        """
        doc_positions = cls._build_doc_id_to_prompt_position(
            request.messages, request.documents
        )
        out: dict[int, list[dict[str, Any]]] = {}
        for idx, msg in enumerate(request.messages):
            if not isinstance(msg, AssistantChatMessageV2) or not msg.citations:
                continue
            resolved: list[dict[str, Any]] = []
            for c in msg.citations:
                converted = cls._sdk_citation_to_melody(c, doc_positions)
                if converted is not None:
                    resolved.append(converted)
            if resolved:
                out[idx] = resolved
        return out

    @classmethod
    def _walk_citable_positions(
        cls, messages: list, documents: list | None
    ) -> list[_CitablePosition]:
        """Single-pass implementation of melody's citation numbering rule.

        Mirrors ``PromptRenderIds::from_messages`` in
        ``melody/src/templating/util.rs``. Emits one
        :class:`_CitablePosition` per document-bearing slot melody
        assigns while rendering the prompt:

        * **Bucket numbering.** Bucket ``0`` is reserved for the
          top-level ``documents`` array when present, and each unique
          ``tool_call_id`` claims the next integer on first-seen basis
          -- registered from whichever of the assistant's
          ``tool_calls[].id`` or the tool message's ``tool_call_id``
          appears first in the history.
        * **Per-bucket result-index slots.** Consumed by *every*
          content item, not just documents -- a text block occupies a
          slot too, and the source at that slot carries an id-less
          ``tool_output`` payload synthesized via
          :meth:`_text_to_tool_output_payload`, matching the cohere
          api's behavior for text-only tool content. Strings and
          empty contents each consume exactly one slot because the
          cohere renderer wraps them into a single text block before
          handing the message off to melody. Multiple tool messages
          sharing a ``tool_call_id`` accumulate into the same bucket,
          so later docs' slots are offset by the sum of previous
          messages' content lengths.
        * **Doc ids attached to each position.** Top-level docs
          contribute both the explicit ``id`` (if set) and the
          ``doc_{idx}`` fallback used for inbound addressing; tool
          document blocks contribute their single ``document.id``
          when present. The ``ids`` tuple is empty for slots that
          are only reachable positionally (text-only tool content,
          docs without a client-provided id) -- those slots still
          have a fully-populated :class:`InternalCitationSource`,
          just no client-visible id to cite them by.

        Ids alone key the inbound helper
        (:meth:`_build_doc_id_to_prompt_position`, which drives
        request-side citation resolution); the resolved
        :class:`InternalCitationSource` (with ``type`` / ``id`` /
        payload) keys the outbound helper
        (:meth:`_build_position_to_source`, whose result is forwarded
        to the reasoning parser via
        ``chat_template_kwargs[POSITION_TO_SOURCE_KEY]``).

        Cohere's on-wire semantics -- ``ToolSource.id`` and
        ``DocumentSource.id`` both carry a document id, with ``type``
        acting only as a payload-shape hint -- are the invariant this
        walk is mirroring.
        """
        out: list[_CitablePosition] = []
        tool_call_id_to_bucket: dict[str, int] = {}
        # Next unassigned ``tool_result_index`` per bucket. Advances
        # by ``len(content)`` (or 1 for string / missing content) for
        # every tool message, whether or not the content had any doc
        # ids in it -- matching melody's per-slot accounting.
        bucket_result_len: dict[int, int] = {}
        next_bucket = 0

        if documents:
            for idx, doc in enumerate(documents):
                # Every top-level doc is addressable inbound by a
                # synthetic ``doc_{idx}`` fallback (so clients can cite
                # positional docs). On the *outbound* wire we prefer
                # the client's explicit id when present, and omit the
                # ``id`` field entirely when it's not -- matching
                # cohere's ``DocumentSource`` where ``id`` is optional
                # and preserving "no id in, no id out" symmetry.
                fallback_id = f"doc_{idx}"
                if isinstance(doc, str):
                    payload: dict[str, Any] = {"text": doc}
                    explicit_id: str | None = None
                else:
                    payload = (
                        dict(doc.data)
                        if isinstance(doc.data, dict)
                        else {"text": doc.data}
                    )
                    explicit_id = doc.id or None
                    if explicit_id:
                        payload.setdefault("id", explicit_id)
                # Register explicit id first so it wins over the
                # fallback for ``setdefault``-style inbound lookup.
                ids: tuple[str, ...] = (
                    (explicit_id, fallback_id) if explicit_id else (fallback_id,)
                )
                out.append(
                    _CitablePosition(
                        bucket=0,
                        result_idx=idx,
                        ids=ids,
                        source=InternalCitationSource(
                            type="document", id=explicit_id, document=payload
                        ),
                    )
                )
            next_bucket = 1

        def register_bucket(tool_call_id: str) -> int:
            nonlocal next_bucket
            bucket = tool_call_id_to_bucket.get(tool_call_id)
            if bucket is None:
                bucket = next_bucket
                tool_call_id_to_bucket[tool_call_id] = bucket
                next_bucket += 1
            return bucket

        for msg in messages:
            if isinstance(msg, ToolChatMessageV2):
                if not msg.tool_call_id:
                    continue
                bucket = register_bucket(msg.tool_call_id)
                base = bucket_result_len.get(bucket, 0)
                if isinstance(msg.content, list):
                    for offset, block in enumerate(msg.content):
                        info = cls._tool_content_block_to_source(block)
                        if info is None:
                            continue
                        # Only real document ids get registered in the
                        # inbound-facing ``ids`` tuple; text-only tool
                        # content produces a source with ``id=None``
                        # to match the cohere api -- the text is
                        # exposed as ``tool_output.content`` but
                        # there's no id for clients to cite by.
                        pos_ids: tuple[str, ...] = (info.id,) if info.id else ()
                        out.append(
                            _CitablePosition(
                                bucket=bucket,
                                result_idx=base + offset,
                                ids=pos_ids,
                                source=info,
                            )
                        )
                    bucket_result_len[bucket] = base + len(msg.content)
                else:
                    # A string or missing content is wrapped by the
                    # cohere renderer into a single text block before
                    # melody sees it, so it consumes one slot. For
                    # string content, emit a synthetic id-less tool
                    # source carrying the text as its payload so a
                    # model citation against this slot still has
                    # something to attach on the wire (matching the
                    # id-less tool_output behavior for text-only
                    # ``ToolChatMessageV2`` content blocks). Missing
                    # content still advances the slot cursor but
                    # produces no source -- a citation against it
                    # would be dropped by ``_to_wire_citation``.
                    if isinstance(msg.content, str):
                        payload = cls._text_to_tool_output_payload(msg.content)
                        out.append(
                            _CitablePosition(
                                bucket=bucket,
                                result_idx=base,
                                ids=(),
                                source=InternalCitationSource(
                                    type="tool", id=None, tool_output=payload
                                ),
                            )
                        )
                    bucket_result_len[bucket] = base + 1
            elif isinstance(msg, AssistantChatMessageV2) and msg.tool_calls:
                for tc in msg.tool_calls:
                    if tc.id:
                        register_bucket(tc.id)

        return out

    @classmethod
    def _build_doc_id_to_prompt_position(
        cls, messages: list, documents: list | None
    ) -> dict[str, tuple[int, int]]:
        """Map every citable document id to its ``(bucket, result_index)``.

        Inbound helper: converts request-side citation ``source.id``
        refs into melody's numeric ``FilterCitation`` addressing (see
        :meth:`_sdk_citation_to_melody`). First-seen-id wins so an
        explicit id on a top-level document takes precedence over the
        ``doc_{idx}`` fallback that :meth:`_walk_citable_positions`
        also emits for the same slot.

        Thin wrapper over :meth:`_walk_citable_positions`; the
        numbering rule lives there.
        """
        doc_positions: dict[str, tuple[int, int]] = {}
        for pos in cls._walk_citable_positions(messages, documents):
            for pos_id in pos.ids:
                doc_positions.setdefault(pos_id, (pos.bucket, pos.result_idx))
        return doc_positions

    @staticmethod
    def _sdk_citation_to_melody(
        citation: Any,
        doc_positions: dict[str, tuple[int, int]],
    ) -> dict[str, Any] | None:
        """Convert a Cohere SDK ``Citation`` to melody's ``FilterCitation``.

        Melody's ``Source`` uses a two-level numeric address:
        ``tool_call_index`` picks a bucket (``0`` = top-level
        ``documents`` when present, ``1..N`` = tool results in history
        order) and ``tool_result_indices`` picks positions inside that
        bucket. The Cohere ``Citation.sources`` schema is per-document:
        each entry's ``id`` is the id of the specific document that
        grounds the citation (``type`` distinguishes payload shape, not
        id semantics). We resolve each source id against
        ``doc_positions`` and aggregate hits sharing a bucket into one
        melody ``Source`` so the renderer emits compact
        ``<co>text</co: N:[i,j]>`` markers.

        Returns ``None`` when any source id fails to resolve or when
        the input has no sources at all: emitting a citation with an
        empty ``sources=[]`` would render a malformed
        ``<co>text</co: :>`` marker, and silently attributing a
        citation to the wrong bucket is worse than dropping the
        ``<co>`` markup. This matches the cohere api's behavior.

        ``Source.document_ids`` is intentionally NOT populated: it is a
        parser-*output* lookup table (see ``PromptRenderIds`` in
        melody), and melody's deserializer ignores it on input.

        The Cohere ``type`` literal (``TEXT_CONTENT`` / ``THINKING_CONTENT``
        / ``PLAN``) collapses to melody's boolean ``is_thinking``.
        ``PLAN`` is treated as a thinking-style citation because cmd3 /
        cmd4 templates render both inline with the same ``<co>...</co>``
        markup.
        """
        raw_sources = getattr(citation, "sources", None) or []
        if not raw_sources:
            return None

        grouped: dict[int, list[int]] = {}
        for src in raw_sources:
            src_id = getattr(src, "id", None)
            if not src_id:
                return None
            position = doc_positions.get(src_id)
            if position is None:
                return None
            bucket, result_idx = position
            grouped.setdefault(bucket, []).append(result_idx)

        sources = [
            {"tool_call_index": bucket, "tool_result_indices": indices}
            for bucket, indices in grouped.items()
        ]

        cite_type = str(getattr(citation, "type", "") or "").upper()
        is_thinking = cite_type in ("THINKING_CONTENT", "PLAN")
        return {
            "start_index": getattr(citation, "start", None) or 0,
            "end_index": getattr(citation, "end", None) or 0,
            "text": getattr(citation, "text", None) or "",
            "sources": sources,
            "is_thinking": is_thinking,
        }

    # ==================================================================
    # Response conversion: ChatCompletion -> Cohere V2
    # ==================================================================

    def _chat_completion_to_v2(
        self,
        response: ChatCompletionResponse,
        request: CohereChatV2Request,
    ) -> CohereChatV2Response:
        choice = response.choices[0]
        msg = choice.message

        # Build content blocks as dicts; ``AssistantMessageResponse``
        # validates them into the proper discriminated union variants
        # (``Text/ThinkingAssistantMessageResponseContentItem``).
        content_blocks: list[dict[str, Any]] = []
        if msg.reasoning:
            content_blocks.append(
                {"type": ContentBlockType.THINKING, "thinking": msg.reasoning}
            )
        if msg.content:
            content_blocks.append({"type": ContentBlockType.TEXT, "text": msg.content})

        tool_calls: list[ToolCallV2] | None = None
        if msg.tool_calls:
            tool_calls = [
                ToolCallV2(
                    id=tc.id,
                    function=ToolCallV2Function(
                        name=tc.function.name,
                        arguments=tc.function.arguments,
                    ),
                )
                for tc in msg.tool_calls
            ]

        # Cohere's ``tool_plan`` is the planning text emitted before tool
        # calls on older, non-reasoning Command models. For those models
        # we surface ``reasoning`` as ``tool_plan`` and drop the thinking
        # block. Reasoning Command models emit a regular thinking block
        # alongside tool calls, so for them we leave the thinking block
        # in place and never set ``tool_plan``.
        tool_plan: str | None = None
        if not self._is_reasoning_model and tool_calls and msg.reasoning:
            tool_plan = msg.reasoning
            content_blocks = [
                blk
                for blk in content_blocks
                if blk.get("type") != ContentBlockType.THINKING
            ]

        assistant_msg = AssistantMessageResponse(
            content=content_blocks or None,
            tool_calls=tool_calls,
            tool_plan=tool_plan,
            citations=self._extract_citations_if_any(msg),
        )

        usage = self._build_usage(response)

        return CohereChatV2Response(
            id=response.id or f"chat_{int(time.time() * 1000)}",
            finish_reason=_map_finish_reason(choice.finish_reason),
            message=assistant_msg,
            usage=usage,
            kv_transfer_params=response.kv_transfer_params,
        )

    def _create_chat_message(self, *args: Any, **kwargs: Any) -> ChatMessage:
        """Route response construction through the citation-carrying subclass.

        Overrides :meth:`OpenAIServingChat._create_chat_message` so every
        non-streaming construction site in the base full-generator
        produces a :class:`CohereChatMessage`. That lets
        :meth:`_finalize_response_message` below set citations on
        ``message.citations`` directly, without relying on
        ``extra="allow"`` attribute injection.
        """
        return CohereChatMessage(*args, **kwargs)

    def _finalize_response_message(
        self,
        message: ChatMessage,
        *,
        parser: Parser | None,
    ) -> ChatMessage:
        """Copy grounding citations off the reasoning parser onto the message.

        The Cohere reasoning parser
        (:mod:`vllm.reasoning.cohere_command_reasoning_parser`) caches the
        citations produced by its most recent unary ``extract_reasoning``
        call on ``last_unary_citations``. We surface them here on
        :class:`CohereChatMessage` so downstream response conversion can
        pick them up without the base :class:`OpenAIServingChat` having to
        know about citations.
        """
        # ``_create_chat_message`` above guarantees the concrete type at
        # runtime. ``cast`` narrows the declared ``ChatMessage`` for
        # mypy without adding a runtime check we don't need: even in the
        # invariant-violated case (a subclass reset the factory back to
        # plain ``ChatMessage``), ``OpenAIBaseModel``'s ``extra="allow"``
        # config lets the ``.citations`` write survive into
        # ``model_dump`` via the extras bucket, so the wire is correct
        # either way.
        message = cast(CohereChatMessage, message)
        citations = getattr(
            getattr(parser, "reasoning_parser", None),
            "last_unary_citations",
            None,
        )
        if citations:
            message.citations = citations
        return message

    def _extract_citations_if_any(self, msg: Any) -> list[Citation] | None:
        """Coerce ``CohereChatMessage.citations`` into the Cohere v2 wire shape.

        :class:`CohereChatMessage` carries a
        ``citations: list[vllm...Citation] | None`` field populated by
        :meth:`_finalize_response_message` (which reads it off the
        reasoning parser's ``last_unary_citations`` cache). Sources are
        already fully resolved by the parser (see
        :func:`_melody_sources_to_vllm` in
        :mod:`vllm.reasoning.cohere_command_reasoning_parser`) using
        the position map forwarded via ``chat_template_kwargs``. This
        method's remaining job is:

        * Drop citations whose sources all failed to resolve (empty
          ``sources`` list) -- fail-closed policy matching the inbound
          ``_sdk_citation_to_melody``.
        * Rewrite ``THINKING_CONTENT`` -> ``PLAN`` for non-reasoning
          models, since ``_chat_completion_to_v2`` surfaces
          ``msg.reasoning`` as ``tool_plan`` on the same
          ``is_reasoning_model`` flag.
        * Coerce to :class:`cohere.types.Citation` for the wire.
        """
        raw = getattr(msg, "citations", None)
        if not raw:
            return None
        out: list[Citation] = []
        for c in raw:
            resolved = self._to_wire_citation(c)
            if resolved is not None:
                out.append(resolved)
        return out or None

    def _to_wire_citation(self, citation: Any) -> Citation | None:
        """Coerce one parser-produced citation into the SDK wire shape.

        Accepts both :class:`InternalCitation` instances (what the
        unary path receives directly from the parser via
        ``last_unary_citations``) and plain ``dict`` payloads (what
        the streaming path sees, because ``DeltaMessage.citations`` is
        an untyped extras field on the OpenAI wire protocol and dict
        shapes survive ``model_validate_json`` unchanged). Both flows
        end up carrying the same key names, so a small ``_get`` helper
        handles both without a shape-specific code path.
        """

        def _get(obj: Any, key: str, default: Any = None) -> Any:
            if isinstance(obj, dict):
                return obj.get(key, default)
            return getattr(obj, key, default)

        raw_sources = _get(citation, "sources") or []
        wire_sources: list[dict[str, Any]] = []
        for src in raw_sources:
            # Sources arrive pre-resolved from the parser (or as trusted
            # dicts on the streaming path). Match the cohere api's
            # behavior of preserving id-less sources (e.g. text-only
            # tool results): cohere's ``Source`` types treat ``id`` as
            # optional, so we simply drop the field via
            # ``exclude_none=True`` instead of dropping the source.
            if isinstance(src, dict):
                wire = {k: v for k, v in src.items() if v is not None}
                if wire:
                    wire_sources.append(wire)
            elif isinstance(src, InternalCitationSource):
                wire_sources.append(src.model_dump(exclude_none=True))
        if not wire_sources:
            return None

        cite_type = _get(citation, "type")
        if cite_type == "THINKING_CONTENT" and not self._is_reasoning_model:
            # Non-reasoning Command models surface the reasoning block
            # as ``tool_plan`` (see ``_chat_completion_to_v2``), so any
            # ``is_thinking`` citation from melody is actually a PLAN
            # citation on the wire.
            cite_type = "PLAN"

        payload = {
            "start": _get(citation, "start"),
            "end": _get(citation, "end"),
            "text": _get(citation, "text"),
            "sources": wire_sources,
            "type": cite_type,
        }
        try:
            return Citation.model_validate(payload)
        except Exception:  # pragma: no cover - defensive
            logger.warning(
                "Skipping malformed citation payload: %r", payload, exc_info=True
            )
            return None

    @classmethod
    def _build_position_to_source(
        cls, request: CohereChatV2Request
    ) -> dict[tuple[int, int], InternalCitationSource]:
        """Invert the request-side numbering into ``(bucket, idx) -> source``.

        Outbound helper: forwarded to the reasoning parser via
        ``chat_template_kwargs[POSITION_TO_SOURCE_KEY]`` so
        :func:`_melody_sources_to_vllm` can attach real document ids /
        types / payloads to each melody source it emits. Each entry
        carries the fully-resolved wire-shape info: ``type``
        (``"document"`` for the reserved top-level bucket 0, ``"tool"``
        for any tool-call bucket), ``id`` (explicit doc id, the
        ``doc_{idx}`` fallback, or ``None`` for id-less sources), and
        ``document`` / ``tool_output`` payload -- matching the cohere
        api's ``Source`` shape.

        Thin wrapper over :meth:`_walk_citable_positions`; the
        numbering rule lives there.
        """
        return {
            (pos.bucket, pos.result_idx): pos.source
            for pos in cls._walk_citable_positions(request.messages, request.documents)
        }

    @staticmethod
    def _text_to_tool_output_payload(text: str) -> dict[str, Any]:
        """Wrap a bare tool-content text into a ``tool_output`` payload.

        Matches the cohere api's handling of text-only tool content:
        attempt to interpret ``text`` as a JSON object first; fall
        back to ``{"content": <text>}`` so a text-only tool result
        still has a structured payload we can attach to a citation
        source.
        """
        if not text:
            return {}
        try:
            parsed = json.loads(text)
        except (json.JSONDecodeError, TypeError):
            return {"content": text}
        if isinstance(parsed, dict):
            return parsed
        return {"content": text}

    @classmethod
    def _tool_content_block_to_source(
        cls,
        block: Any,
    ) -> InternalCitationSource | None:
        """Extract the wire-shape source for one tool-message content block.

        Handles both cohere ``DocumentToolContent`` and
        ``TextToolContent`` blocks:

        * ``document`` blocks with an explicit id produce a
          ``tool`` source keyed by that id (payload is the document's
          ``data`` dict, or ``{"text": ...}`` for scalar data).
        * ``document`` blocks *without* an id and ``text`` blocks
          produce an id-less ``tool`` source: the cohere api emits
          the payload with an empty id in this case, and cohere's
          SDK treats ``ToolSource.id`` as optional -- so we simply
          omit ``id`` on the wire instead of inventing a synthetic
          one.

        Returns ``None`` only for shapes we don't recognise at all;
        that slot still gets counted (callers advance the bucket
        cursor over the block regardless) so numbering stays aligned
        with melody.
        """
        block_type = getattr(block, "type", None)
        if block_type == "document":
            doc: Document | None = getattr(block, "document", None)
            if doc is None:
                return None
            doc_id = doc.id or None
            data = doc.data
            if isinstance(data, dict):
                payload: dict[str, Any] = dict(data)
            elif data is None:
                payload = {}
            else:
                # Match the top-level ``documents`` handling (see
                # ``_apply_cohere_template_kwargs``): non-dict payloads
                # get wrapped as ``{"text": ...}`` so clients get a
                # renderable body instead of a bare id.
                payload = {"text": data}
            if doc_id:
                payload.setdefault("id", doc_id)
            return InternalCitationSource(
                type="tool",
                id=doc_id,
                tool_output=payload,
            )
        if block_type == "text":
            text = getattr(block, "text", None) or ""
            payload = cls._text_to_tool_output_payload(text)
            return InternalCitationSource(
                type="tool",
                id=None,
                tool_output=payload,
            )
        return None

    @staticmethod
    def _build_usage(response: ChatCompletionResponse) -> CohereUsage | None:
        if response.usage is None:
            return None
        prompt = response.usage.prompt_tokens
        completion = response.usage.completion_tokens or 0
        cached: int | None = None
        if response.usage.prompt_tokens_details is not None:
            cached = response.usage.prompt_tokens_details.cached_tokens
        return CohereUsage(
            billed_units=CohereUsageBilledUnits(
                input_tokens=prompt,
                output_tokens=completion,
            ),
            tokens=CohereUsageTokens(
                input_tokens=prompt,
                output_tokens=completion,
            ),
            cached_tokens=cached,
        )

    # ==================================================================
    # Stream conversion: chat completion stream -> Cohere V2 SSE events
    # ==================================================================

    async def _chat_completion_stream_to_v2(
        self,
        generator: AsyncGenerator[str, None],
        request: CohereChatV2Request,
    ) -> AsyncGenerator[str, None]:
        """Translate an OpenAI-style chat completion SSE stream into Cohere's
        v2 stream-event format.

        Cohere's v2 stream lifecycle is:

            message-start
              [content-start, content-delta..., content-end]*
              [tool-plan-delta]*
              [tool-call-start, tool-call-delta..., tool-call-end]*
              message-end
        """
        state = _StreamState()

        try:
            async for item in generator:
                if not item.startswith("data:"):
                    continue
                data_str = item[len("data:") :].strip().rstrip("\n")
                if not data_str:
                    continue
                if data_str == "[DONE]":
                    # OpenAI's stream terminator. Fall through to the
                    # post-loop cleanup so we always emit ``message-end``
                    # even if the usage-only chunk was skipped.
                    break

                chunk = ChatCompletionStreamResponse.model_validate_json(data_str)
                state.last_chunk_id = chunk.id

                if not state.started:
                    yield _emit(
                        MessageStartEvent(
                            id=chunk.id,
                            delta={"message": {"role": "assistant"}},
                        )
                    )
                    state.started = True

                # The final OpenAI chunk has no choices and only carries usage.
                if not chunk.choices:
                    for ev in self._close_open_blocks(state):
                        yield ev
                    yield self._build_message_end_event(
                        chunk_id=chunk.id,
                        finish_reason=state.finish_reason,
                        usage_chunk=chunk,
                    )
                    state.ended = True
                    continue

                choice = chunk.choices[0]
                if choice.finish_reason is not None:
                    state.finish_reason = choice.finish_reason

                delta = choice.delta

                # Reasoning -> thinking content block
                reasoning = getattr(delta, "reasoning", None) or getattr(
                    delta, "reasoning_content", None
                )
                if reasoning:
                    for ev in self._handle_thinking_delta(state, reasoning):
                        yield ev

                if delta.content:
                    for ev in self._handle_text_delta(state, delta.content):
                        yield ev

                if delta.tool_calls:
                    for ev in self._handle_tool_call_deltas(state, delta.tool_calls):
                        yield ev

                # Citations: a Cohere-specific extension on DeltaMessage that
                # the cohere renderer/parsers may populate.
                delta_citations = getattr(delta, "citations", None)
                if delta_citations:
                    for ev in self._handle_citation_deltas(state, delta_citations):
                        yield ev

        except Exception as exc:
            logger.exception("Error converting chat completion stream to v2")
            if state.started and not state.ended:
                yield _sse(
                    json.dumps(
                        {
                            "type": "message-end",
                            "delta": {
                                "error": sanitize_message(str(exc)),
                                "finish_reason": "ERROR",
                            },
                        }
                    )
                )
                state.ended = True
            yield _DONE_FRAME
            return

        # Normal completion or ``[DONE]``: ensure ``message-end`` is always
        # emitted. Upstream may close the stream without sending the final
        # usage-only chunk (e.g. on shutdown, or when ``[DONE]`` is the only
        # terminator); without this fallback Cohere clients would hang
        # waiting for the closing event.
        if state.started and not state.ended:
            for ev in self._close_open_blocks(state):
                yield ev
            yield self._build_message_end_event(
                chunk_id=state.last_chunk_id,
                finish_reason=state.finish_reason,
                usage_chunk=None,
            )
            state.ended = True

        # Stream terminator. Cohere's v2 SSE protocol ends every stream
        # with ``data: [DONE]\n\n`` after ``message-end``; Fern-generated
        # clients (Go/Java) and cohere-python all key their read loop off
        # this sentinel.
        yield _DONE_FRAME

    # -- per-delta helpers --------------------------------------------

    def _handle_thinking_delta(self, state: _StreamState, delta_text: str) -> list[str]:
        # Non-reasoning Command models: emit ``tool-plan-delta`` events
        # directly instead of opening a thinking content block. The
        # ``tool-plan-delta`` event has no start/end pair around it.
        if not self._is_reasoning_model:
            events: list[str] = list(self._close_open_blocks(state))
            events.append(
                _emit(
                    ToolPlanDeltaEvent(
                        delta={"message": {"tool_plan": delta_text}},
                    )
                )
            )
            return events

        # Reasoning model (default): open / continue a thinking block.
        events = []
        if state.active_block != ContentBlockType.THINKING:
            events.extend(self._close_open_blocks(state))
            idx = state.next_content_index()
            state.active_block = ContentBlockType.THINKING
            state.active_block_index = idx
            events.append(
                _emit(
                    ContentStartEvent(
                        index=idx,
                        delta={
                            "message": {
                                "content": {
                                    "type": ContentBlockType.THINKING,
                                    "thinking": "",
                                }
                            }
                        },
                    )
                )
            )
        events.append(
            _emit(
                ContentDeltaEvent(
                    index=state.active_block_index,
                    delta={"message": {"content": {"thinking": delta_text}}},
                )
            )
        )
        return events

    def _handle_text_delta(self, state: _StreamState, delta_text: str) -> list[str]:
        events: list[str] = []
        if state.active_block != ContentBlockType.TEXT:
            events.extend(self._close_open_blocks(state))
            idx = state.next_content_index()
            state.active_block = ContentBlockType.TEXT
            state.active_block_index = idx
            events.append(
                _emit(
                    ContentStartEvent(
                        index=idx,
                        delta={
                            "message": {
                                "content": {
                                    "type": ContentBlockType.TEXT,
                                    "text": "",
                                }
                            }
                        },
                    )
                )
            )
        events.append(
            _emit(
                ContentDeltaEvent(
                    index=state.active_block_index,
                    delta={"message": {"content": {"text": delta_text}}},
                )
            )
        )
        return events

    def _handle_tool_call_deltas(self, state: _StreamState, deltas: list) -> list[str]:
        events: list[str] = []
        for tc in deltas:
            tc_index = tc.index
            fn = tc.function

            if tc_index not in state.tool_calls_seen:
                # New tool call. Close any open content/tool block first.
                events.extend(self._close_open_blocks(state))
                state.tool_calls_seen.add(tc_index)
                state.active_tool_index = tc_index
                state.active_block = ContentBlockType.TOOL_CALL
                events.append(
                    _emit(
                        ToolCallStartEvent(
                            index=tc_index,
                            delta={
                                "message": {
                                    "tool_calls": {
                                        "id": tc.id or "",
                                        "type": "function",
                                        "function": {
                                            "name": (fn.name if fn else "") or "",
                                            "arguments": (fn.arguments if fn else None)
                                            or "",
                                        },
                                    }
                                }
                            },
                        )
                    )
                )
                continue

            if fn and fn.arguments:
                events.append(
                    _emit(
                        ToolCallDeltaEvent(
                            index=tc_index,
                            delta={
                                "message": {
                                    "tool_calls": {
                                        "function": {
                                            "arguments": fn.arguments,
                                        }
                                    }
                                }
                            },
                        )
                    )
                )
        return events

    def _handle_citation_deltas(
        self,
        state: _StreamState,
        citations: list,
    ) -> list[str]:
        """Emit ``citation-start`` / ``citation-end`` events for a delta.

        The reasoning parser populates
        :class:`CohereDeltaMessage.citations` (see
        :mod:`vllm.entrypoints.cohere.cohere_chat_message`) with
        sources whose ``id`` / ``type`` / ``document`` / ``tool_output``
        are already resolved against the request's document context
        (see :func:`_melody_sources_to_vllm` in the parser). This
        method only coerces to the SDK wire shape, applies the
        ``THINKING_CONTENT`` -> ``PLAN`` rewrite for non-reasoning
        models, and drops citations whose sources didn't resolve --
        the shared :meth:`_to_wire_citation` handles all three.
        """
        events: list[str] = []
        for c in citations:
            citation = self._to_wire_citation(c)
            if citation is None:
                continue
            idx = state.next_citation_index()
            events.append(
                _emit(
                    CitationStartEvent(
                        index=idx,
                        delta={
                            "message": {
                                "citations": citation.model_dump(exclude_none=True)
                            }
                        },
                    )
                )
            )
            events.append(_emit(CitationEndEvent(index=idx)))
        return events

    # -- block lifecycle helpers --------------------------------------

    def _close_open_blocks(self, state: _StreamState) -> list[str]:
        """Emit ``content-end`` / ``tool-call-end`` for the currently open
        block (if any) and reset the corresponding ``_StreamState`` slots.
        """
        events: list[str] = []
        if state.active_block in (ContentBlockType.TEXT, ContentBlockType.THINKING):
            events.append(_emit(ContentEndEvent(index=state.active_block_index)))
        elif state.active_block == ContentBlockType.TOOL_CALL:
            events.append(_emit(ToolCallEndEvent(index=state.active_tool_index)))
        state.active_block = None
        state.active_block_index = None
        state.active_tool_index = None
        return events

    def _build_message_end_event(
        self,
        chunk_id: str,
        finish_reason: str | None,
        usage_chunk: ChatCompletionStreamResponse | None = None,
    ) -> str:
        delta: dict[str, Any] = {
            "finish_reason": _map_finish_reason(finish_reason),
        }
        if usage_chunk is not None and usage_chunk.usage is not None:
            prompt = usage_chunk.usage.prompt_tokens
            completion = usage_chunk.usage.completion_tokens or 0
            usage_block: dict[str, Any] = {
                "billed_units": {
                    "input_tokens": prompt,
                    "output_tokens": completion,
                },
                "tokens": {
                    "input_tokens": prompt,
                    "output_tokens": completion,
                },
            }
            if usage_chunk.usage.prompt_tokens_details is not None:
                cached = usage_chunk.usage.prompt_tokens_details.cached_tokens
                if cached is not None:
                    usage_block["cached_tokens"] = cached
            delta["usage"] = usage_block
        return _emit(MessageEndEvent(id=chunk_id, delta=delta))

    # ==================================================================
    # Helpers for the router
    # ==================================================================

    @staticmethod
    def create_error_response(
        message: str | Exception,
        err_type: str = "bad_request",
        status_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
        param: str | None = None,
    ) -> ErrorResponse:
        # Override of :meth:`BaseServing.create_error_response` that uses
        # Cohere-flavored defaults (``type="bad_request"``, ``code=400``)
        # so the router can translate the envelope uniformly. ``param``
        # is accepted for signature parity with the base class but is
        # not surfaced in the Cohere wire format.
        from vllm.entrypoints.openai.engine.protocol import ErrorInfo

        del param  # unused; kept for signature compatibility
        return ErrorResponse(
            error=ErrorInfo(
                message=str(message),
                type=err_type,
                code=int(status_code),
            )
        )

_apply_cohere_template_kwargs(chat_req, request) classmethod

Forward Cohere-specific request fields into chat_template_kwargs.

The :class:vllm.renderers.cohere.CohereRenderer consumes these kwargs to drive cohere_melody.render_cmd3 / render_cmd4. Other renderers ignore unknown kwargs, so this is also a no-op for non-cohere --tokenizer-mode settings.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _apply_cohere_template_kwargs(
    cls,
    chat_req: ChatCompletionRequest,
    request: CohereChatV2Request,
) -> None:
    """Forward Cohere-specific request fields into ``chat_template_kwargs``.

    The :class:`vllm.renderers.cohere.CohereRenderer` consumes these
    kwargs to drive ``cohere_melody.render_cmd3`` / ``render_cmd4``.
    Other renderers ignore unknown kwargs, so this is also a no-op for
    non-cohere ``--tokenizer-mode`` settings.
    """
    kwargs = dict(chat_req.chat_template_kwargs or {})

    if request.documents:
        documents: list[dict[str, Any]] = []
        for idx, doc in enumerate(request.documents):
            if isinstance(doc, str):
                documents.append({"id": f"doc_{idx}", "data": {"text": doc}})
            else:
                documents.append(
                    {
                        "id": doc.id or f"doc_{idx}",
                        "data": (
                            doc.data
                            if isinstance(doc.data, dict)
                            else {"text": doc.data}
                        ),
                    }
                )
        kwargs.setdefault("documents", documents)

    if request.safety_mode is not None:
        kwargs.setdefault("safety_mode", str(request.safety_mode).lower())
    if request.citation_options is not None:
        kwargs.setdefault(
            "citation_options",
            request.citation_options.model_dump(exclude_none=True),
        )
    if request.thinking is not None:
        kwargs.setdefault(
            "thinking",
            request.thinking.model_dump(exclude_none=True),
        )

    # ``strict_tools`` is intentionally NOT forwarded here. It's a
    # decoder-guidance flag that ``_apply_tools`` already maps onto
    # per-function OpenAI ``strict: true`` on ``chat_req.tools``;
    # ``cohere_melody.render_cmd3``/``render_cmd4`` have no
    # ``strict_tools`` knob, and the renderer's residual-forward path
    # would otherwise surface it as a Jinja variable ``{{ strict_tools }}``
    # that templates have no defined use for.

    # Citations on assistant messages in the request don't fit
    # OpenAI's ``ChatMessage`` / ``ConversationMessage`` schemas, so
    # we forward them under the reserved ``MESSAGES_CITATIONS_KEY``
    # chat_template_kwargs entry keyed by request-message index.
    # See ``_sdk_citation_to_melody`` for the id-resolution rules.
    citations_by_index = cls._collect_message_citations(request)
    if citations_by_index:
        kwargs.setdefault(MESSAGES_CITATIONS_KEY, citations_by_index)

    # Forward the ``(bucket, result_idx) -> resolved wire source``
    # map to the reasoning parser via chat_template_kwargs. The
    # parser (constructed with the same kwargs) uses it to attach
    # real document ids / types / payloads to every source it
    # emits, so citation deltas already carry SDK-shape sources by
    # the time they reach the streaming loop. See
    # ``_build_position_to_source`` for the numbering rule and
    # ``_melody_sources_to_vllm`` in the reasoning parser for the
    # consumer.
    position_to_source = cls._build_position_to_source(request)
    if position_to_source:
        kwargs.setdefault(POSITION_TO_SOURCE_KEY, position_to_source)

    if kwargs:
        chat_req.chat_template_kwargs = kwargs

_build_doc_id_to_prompt_position(messages, documents) classmethod

Map every citable document id to its (bucket, result_index).

Inbound helper: converts request-side citation source.id refs into melody's numeric FilterCitation addressing (see :meth:_sdk_citation_to_melody). First-seen-id wins so an explicit id on a top-level document takes precedence over the doc_{idx} fallback that :meth:_walk_citable_positions also emits for the same slot.

Thin wrapper over :meth:_walk_citable_positions; the numbering rule lives there.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _build_doc_id_to_prompt_position(
    cls, messages: list, documents: list | None
) -> dict[str, tuple[int, int]]:
    """Map every citable document id to its ``(bucket, result_index)``.

    Inbound helper: converts request-side citation ``source.id``
    refs into melody's numeric ``FilterCitation`` addressing (see
    :meth:`_sdk_citation_to_melody`). First-seen-id wins so an
    explicit id on a top-level document takes precedence over the
    ``doc_{idx}`` fallback that :meth:`_walk_citable_positions`
    also emits for the same slot.

    Thin wrapper over :meth:`_walk_citable_positions`; the
    numbering rule lives there.
    """
    doc_positions: dict[str, tuple[int, int]] = {}
    for pos in cls._walk_citable_positions(messages, documents):
        for pos_id in pos.ids:
            doc_positions.setdefault(pos_id, (pos.bucket, pos.result_idx))
    return doc_positions

_build_position_to_source(request) classmethod

Invert the request-side numbering into (bucket, idx) -> source.

Outbound helper: forwarded to the reasoning parser via chat_template_kwargs[POSITION_TO_SOURCE_KEY] so :func:_melody_sources_to_vllm can attach real document ids / types / payloads to each melody source it emits. Each entry carries the fully-resolved wire-shape info: type ("document" for the reserved top-level bucket 0, "tool" for any tool-call bucket), id (explicit doc id, the doc_{idx} fallback, or None for id-less sources), and document / tool_output payload -- matching the cohere api's Source shape.

Thin wrapper over :meth:_walk_citable_positions; the numbering rule lives there.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _build_position_to_source(
    cls, request: CohereChatV2Request
) -> dict[tuple[int, int], InternalCitationSource]:
    """Invert the request-side numbering into ``(bucket, idx) -> source``.

    Outbound helper: forwarded to the reasoning parser via
    ``chat_template_kwargs[POSITION_TO_SOURCE_KEY]`` so
    :func:`_melody_sources_to_vllm` can attach real document ids /
    types / payloads to each melody source it emits. Each entry
    carries the fully-resolved wire-shape info: ``type``
    (``"document"`` for the reserved top-level bucket 0, ``"tool"``
    for any tool-call bucket), ``id`` (explicit doc id, the
    ``doc_{idx}`` fallback, or ``None`` for id-less sources), and
    ``document`` / ``tool_output`` payload -- matching the cohere
    api's ``Source`` shape.

    Thin wrapper over :meth:`_walk_citable_positions`; the
    numbering rule lives there.
    """
    return {
        (pos.bucket, pos.result_idx): pos.source
        for pos in cls._walk_citable_positions(request.messages, request.documents)
    }

_chat_completion_stream_to_v2(generator, request) async

Translate an OpenAI-style chat completion SSE stream into Cohere's v2 stream-event format.

Cohere's v2 stream lifecycle is:

message-start
  [content-start, content-delta..., content-end]*
  [tool-plan-delta]*
  [tool-call-start, tool-call-delta..., tool-call-end]*
  message-end
Source code in vllm/entrypoints/cohere/serving.py
async def _chat_completion_stream_to_v2(
    self,
    generator: AsyncGenerator[str, None],
    request: CohereChatV2Request,
) -> AsyncGenerator[str, None]:
    """Translate an OpenAI-style chat completion SSE stream into Cohere's
    v2 stream-event format.

    Cohere's v2 stream lifecycle is:

        message-start
          [content-start, content-delta..., content-end]*
          [tool-plan-delta]*
          [tool-call-start, tool-call-delta..., tool-call-end]*
          message-end
    """
    state = _StreamState()

    try:
        async for item in generator:
            if not item.startswith("data:"):
                continue
            data_str = item[len("data:") :].strip().rstrip("\n")
            if not data_str:
                continue
            if data_str == "[DONE]":
                # OpenAI's stream terminator. Fall through to the
                # post-loop cleanup so we always emit ``message-end``
                # even if the usage-only chunk was skipped.
                break

            chunk = ChatCompletionStreamResponse.model_validate_json(data_str)
            state.last_chunk_id = chunk.id

            if not state.started:
                yield _emit(
                    MessageStartEvent(
                        id=chunk.id,
                        delta={"message": {"role": "assistant"}},
                    )
                )
                state.started = True

            # The final OpenAI chunk has no choices and only carries usage.
            if not chunk.choices:
                for ev in self._close_open_blocks(state):
                    yield ev
                yield self._build_message_end_event(
                    chunk_id=chunk.id,
                    finish_reason=state.finish_reason,
                    usage_chunk=chunk,
                )
                state.ended = True
                continue

            choice = chunk.choices[0]
            if choice.finish_reason is not None:
                state.finish_reason = choice.finish_reason

            delta = choice.delta

            # Reasoning -> thinking content block
            reasoning = getattr(delta, "reasoning", None) or getattr(
                delta, "reasoning_content", None
            )
            if reasoning:
                for ev in self._handle_thinking_delta(state, reasoning):
                    yield ev

            if delta.content:
                for ev in self._handle_text_delta(state, delta.content):
                    yield ev

            if delta.tool_calls:
                for ev in self._handle_tool_call_deltas(state, delta.tool_calls):
                    yield ev

            # Citations: a Cohere-specific extension on DeltaMessage that
            # the cohere renderer/parsers may populate.
            delta_citations = getattr(delta, "citations", None)
            if delta_citations:
                for ev in self._handle_citation_deltas(state, delta_citations):
                    yield ev

    except Exception as exc:
        logger.exception("Error converting chat completion stream to v2")
        if state.started and not state.ended:
            yield _sse(
                json.dumps(
                    {
                        "type": "message-end",
                        "delta": {
                            "error": sanitize_message(str(exc)),
                            "finish_reason": "ERROR",
                        },
                    }
                )
            )
            state.ended = True
        yield _DONE_FRAME
        return

    # Normal completion or ``[DONE]``: ensure ``message-end`` is always
    # emitted. Upstream may close the stream without sending the final
    # usage-only chunk (e.g. on shutdown, or when ``[DONE]`` is the only
    # terminator); without this fallback Cohere clients would hang
    # waiting for the closing event.
    if state.started and not state.ended:
        for ev in self._close_open_blocks(state):
            yield ev
        yield self._build_message_end_event(
            chunk_id=state.last_chunk_id,
            finish_reason=state.finish_reason,
            usage_chunk=None,
        )
        state.ended = True

    # Stream terminator. Cohere's v2 SSE protocol ends every stream
    # with ``data: [DONE]\n\n`` after ``message-end``; Fern-generated
    # clients (Go/Java) and cohere-python all key their read loop off
    # this sentinel.
    yield _DONE_FRAME

_close_open_blocks(state)

Emit content-end / tool-call-end for the currently open block (if any) and reset the corresponding _StreamState slots.

Source code in vllm/entrypoints/cohere/serving.py
def _close_open_blocks(self, state: _StreamState) -> list[str]:
    """Emit ``content-end`` / ``tool-call-end`` for the currently open
    block (if any) and reset the corresponding ``_StreamState`` slots.
    """
    events: list[str] = []
    if state.active_block in (ContentBlockType.TEXT, ContentBlockType.THINKING):
        events.append(_emit(ContentEndEvent(index=state.active_block_index)))
    elif state.active_block == ContentBlockType.TOOL_CALL:
        events.append(_emit(ToolCallEndEvent(index=state.active_tool_index)))
    state.active_block = None
    state.active_block_index = None
    state.active_tool_index = None
    return events

_collect_message_citations(request) classmethod

Extract AssistantChatMessageV2.citations keyed by message index.

Returns an empty dict when no assistant message in the request carries any resolvable citations, so callers can use truthiness as the "should I emit the MESSAGES_CITATIONS_KEY entry?" test. Citations whose sources can't be resolved are dropped individually; messages that end up with zero surviving citations are omitted from the result.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _collect_message_citations(
    cls, request: CohereChatV2Request
) -> dict[int, list[dict[str, Any]]]:
    """Extract ``AssistantChatMessageV2.citations`` keyed by message index.

    Returns an empty dict when no assistant message in the request
    carries any resolvable citations, so callers can use truthiness
    as the "should I emit the ``MESSAGES_CITATIONS_KEY`` entry?"
    test. Citations whose sources can't be resolved are dropped
    individually; messages that end up with zero surviving
    citations are omitted from the result.
    """
    doc_positions = cls._build_doc_id_to_prompt_position(
        request.messages, request.documents
    )
    out: dict[int, list[dict[str, Any]]] = {}
    for idx, msg in enumerate(request.messages):
        if not isinstance(msg, AssistantChatMessageV2) or not msg.citations:
            continue
        resolved: list[dict[str, Any]] = []
        for c in msg.citations:
            converted = cls._sdk_citation_to_melody(c, doc_positions)
            if converted is not None:
                resolved.append(converted)
        if resolved:
            out[idx] = resolved
    return out

_create_chat_message(*args, **kwargs)

Route response construction through the citation-carrying subclass.

Overrides :meth:OpenAIServingChat._create_chat_message so every non-streaming construction site in the base full-generator produces a :class:CohereChatMessage. That lets :meth:_finalize_response_message below set citations on message.citations directly, without relying on extra="allow" attribute injection.

Source code in vllm/entrypoints/cohere/serving.py
def _create_chat_message(self, *args: Any, **kwargs: Any) -> ChatMessage:
    """Route response construction through the citation-carrying subclass.

    Overrides :meth:`OpenAIServingChat._create_chat_message` so every
    non-streaming construction site in the base full-generator
    produces a :class:`CohereChatMessage`. That lets
    :meth:`_finalize_response_message` below set citations on
    ``message.citations`` directly, without relying on
    ``extra="allow"`` attribute injection.
    """
    return CohereChatMessage(*args, **kwargs)

_extract_citations_if_any(msg)

Coerce CohereChatMessage.citations into the Cohere v2 wire shape.

:class:CohereChatMessage carries a citations: list[vllm...Citation] | None field populated by :meth:_finalize_response_message (which reads it off the reasoning parser's last_unary_citations cache). Sources are already fully resolved by the parser (see :func:_melody_sources_to_vllm in :mod:vllm.reasoning.cohere_command_reasoning_parser) using the position map forwarded via chat_template_kwargs. This method's remaining job is:

  • Drop citations whose sources all failed to resolve (empty sources list) -- fail-closed policy matching the inbound _sdk_citation_to_melody.
  • Rewrite THINKING_CONTENT -> PLAN for non-reasoning models, since _chat_completion_to_v2 surfaces msg.reasoning as tool_plan on the same is_reasoning_model flag.
  • Coerce to :class:cohere.types.Citation for the wire.
Source code in vllm/entrypoints/cohere/serving.py
def _extract_citations_if_any(self, msg: Any) -> list[Citation] | None:
    """Coerce ``CohereChatMessage.citations`` into the Cohere v2 wire shape.

    :class:`CohereChatMessage` carries a
    ``citations: list[vllm...Citation] | None`` field populated by
    :meth:`_finalize_response_message` (which reads it off the
    reasoning parser's ``last_unary_citations`` cache). Sources are
    already fully resolved by the parser (see
    :func:`_melody_sources_to_vllm` in
    :mod:`vllm.reasoning.cohere_command_reasoning_parser`) using
    the position map forwarded via ``chat_template_kwargs``. This
    method's remaining job is:

    * Drop citations whose sources all failed to resolve (empty
      ``sources`` list) -- fail-closed policy matching the inbound
      ``_sdk_citation_to_melody``.
    * Rewrite ``THINKING_CONTENT`` -> ``PLAN`` for non-reasoning
      models, since ``_chat_completion_to_v2`` surfaces
      ``msg.reasoning`` as ``tool_plan`` on the same
      ``is_reasoning_model`` flag.
    * Coerce to :class:`cohere.types.Citation` for the wire.
    """
    raw = getattr(msg, "citations", None)
    if not raw:
        return None
    out: list[Citation] = []
    for c in raw:
        resolved = self._to_wire_citation(c)
        if resolved is not None:
            out.append(resolved)
    return out or None

_finalize_response_message(message, *, parser)

Copy grounding citations off the reasoning parser onto the message.

The Cohere reasoning parser (:mod:vllm.reasoning.cohere_command_reasoning_parser) caches the citations produced by its most recent unary extract_reasoning call on last_unary_citations. We surface them here on :class:CohereChatMessage so downstream response conversion can pick them up without the base :class:OpenAIServingChat having to know about citations.

Source code in vllm/entrypoints/cohere/serving.py
def _finalize_response_message(
    self,
    message: ChatMessage,
    *,
    parser: Parser | None,
) -> ChatMessage:
    """Copy grounding citations off the reasoning parser onto the message.

    The Cohere reasoning parser
    (:mod:`vllm.reasoning.cohere_command_reasoning_parser`) caches the
    citations produced by its most recent unary ``extract_reasoning``
    call on ``last_unary_citations``. We surface them here on
    :class:`CohereChatMessage` so downstream response conversion can
    pick them up without the base :class:`OpenAIServingChat` having to
    know about citations.
    """
    # ``_create_chat_message`` above guarantees the concrete type at
    # runtime. ``cast`` narrows the declared ``ChatMessage`` for
    # mypy without adding a runtime check we don't need: even in the
    # invariant-violated case (a subclass reset the factory back to
    # plain ``ChatMessage``), ``OpenAIBaseModel``'s ``extra="allow"``
    # config lets the ``.citations`` write survive into
    # ``model_dump`` via the extras bucket, so the wire is correct
    # either way.
    message = cast(CohereChatMessage, message)
    citations = getattr(
        getattr(parser, "reasoning_parser", None),
        "last_unary_citations",
        None,
    )
    if citations:
        message.citations = citations
    return message

_handle_citation_deltas(state, citations)

Emit citation-start / citation-end events for a delta.

The reasoning parser populates :class:CohereDeltaMessage.citations (see :mod:vllm.entrypoints.cohere.cohere_chat_message) with sources whose id / type / document / tool_output are already resolved against the request's document context (see :func:_melody_sources_to_vllm in the parser). This method only coerces to the SDK wire shape, applies the THINKING_CONTENT -> PLAN rewrite for non-reasoning models, and drops citations whose sources didn't resolve -- the shared :meth:_to_wire_citation handles all three.

Source code in vllm/entrypoints/cohere/serving.py
def _handle_citation_deltas(
    self,
    state: _StreamState,
    citations: list,
) -> list[str]:
    """Emit ``citation-start`` / ``citation-end`` events for a delta.

    The reasoning parser populates
    :class:`CohereDeltaMessage.citations` (see
    :mod:`vllm.entrypoints.cohere.cohere_chat_message`) with
    sources whose ``id`` / ``type`` / ``document`` / ``tool_output``
    are already resolved against the request's document context
    (see :func:`_melody_sources_to_vllm` in the parser). This
    method only coerces to the SDK wire shape, applies the
    ``THINKING_CONTENT`` -> ``PLAN`` rewrite for non-reasoning
    models, and drops citations whose sources didn't resolve --
    the shared :meth:`_to_wire_citation` handles all three.
    """
    events: list[str] = []
    for c in citations:
        citation = self._to_wire_citation(c)
        if citation is None:
            continue
        idx = state.next_citation_index()
        events.append(
            _emit(
                CitationStartEvent(
                    index=idx,
                    delta={
                        "message": {
                            "citations": citation.model_dump(exclude_none=True)
                        }
                    },
                )
            )
        )
        events.append(_emit(CitationEndEvent(index=idx)))
    return events

_sdk_citation_to_melody(citation, doc_positions) staticmethod

Convert a Cohere SDK Citation to melody's FilterCitation.

Melody's Source uses a two-level numeric address: tool_call_index picks a bucket (0 = top-level documents when present, 1..N = tool results in history order) and tool_result_indices picks positions inside that bucket. The Cohere Citation.sources schema is per-document: each entry's id is the id of the specific document that grounds the citation (type distinguishes payload shape, not id semantics). We resolve each source id against doc_positions and aggregate hits sharing a bucket into one melody Source so the renderer emits compact <co>text</co: N:[i,j]> markers.

Returns None when any source id fails to resolve or when the input has no sources at all: emitting a citation with an empty sources=[] would render a malformed <co>text</co: :> marker, and silently attributing a citation to the wrong bucket is worse than dropping the <co> markup. This matches the cohere api's behavior.

Source.document_ids is intentionally NOT populated: it is a parser-output lookup table (see PromptRenderIds in melody), and melody's deserializer ignores it on input.

The Cohere type literal (TEXT_CONTENT / THINKING_CONTENT / PLAN) collapses to melody's boolean is_thinking. PLAN is treated as a thinking-style citation because cmd3 / cmd4 templates render both inline with the same <co>...</co> markup.

Source code in vllm/entrypoints/cohere/serving.py
@staticmethod
def _sdk_citation_to_melody(
    citation: Any,
    doc_positions: dict[str, tuple[int, int]],
) -> dict[str, Any] | None:
    """Convert a Cohere SDK ``Citation`` to melody's ``FilterCitation``.

    Melody's ``Source`` uses a two-level numeric address:
    ``tool_call_index`` picks a bucket (``0`` = top-level
    ``documents`` when present, ``1..N`` = tool results in history
    order) and ``tool_result_indices`` picks positions inside that
    bucket. The Cohere ``Citation.sources`` schema is per-document:
    each entry's ``id`` is the id of the specific document that
    grounds the citation (``type`` distinguishes payload shape, not
    id semantics). We resolve each source id against
    ``doc_positions`` and aggregate hits sharing a bucket into one
    melody ``Source`` so the renderer emits compact
    ``<co>text</co: N:[i,j]>`` markers.

    Returns ``None`` when any source id fails to resolve or when
    the input has no sources at all: emitting a citation with an
    empty ``sources=[]`` would render a malformed
    ``<co>text</co: :>`` marker, and silently attributing a
    citation to the wrong bucket is worse than dropping the
    ``<co>`` markup. This matches the cohere api's behavior.

    ``Source.document_ids`` is intentionally NOT populated: it is a
    parser-*output* lookup table (see ``PromptRenderIds`` in
    melody), and melody's deserializer ignores it on input.

    The Cohere ``type`` literal (``TEXT_CONTENT`` / ``THINKING_CONTENT``
    / ``PLAN``) collapses to melody's boolean ``is_thinking``.
    ``PLAN`` is treated as a thinking-style citation because cmd3 /
    cmd4 templates render both inline with the same ``<co>...</co>``
    markup.
    """
    raw_sources = getattr(citation, "sources", None) or []
    if not raw_sources:
        return None

    grouped: dict[int, list[int]] = {}
    for src in raw_sources:
        src_id = getattr(src, "id", None)
        if not src_id:
            return None
        position = doc_positions.get(src_id)
        if position is None:
            return None
        bucket, result_idx = position
        grouped.setdefault(bucket, []).append(result_idx)

    sources = [
        {"tool_call_index": bucket, "tool_result_indices": indices}
        for bucket, indices in grouped.items()
    ]

    cite_type = str(getattr(citation, "type", "") or "").upper()
    is_thinking = cite_type in ("THINKING_CONTENT", "PLAN")
    return {
        "start_index": getattr(citation, "start", None) or 0,
        "end_index": getattr(citation, "end", None) or 0,
        "text": getattr(citation, "text", None) or "",
        "sources": sources,
        "is_thinking": is_thinking,
    }

_text_to_tool_output_payload(text) staticmethod

Wrap a bare tool-content text into a tool_output payload.

Matches the cohere api's handling of text-only tool content: attempt to interpret text as a JSON object first; fall back to {"content": <text>} so a text-only tool result still has a structured payload we can attach to a citation source.

Source code in vllm/entrypoints/cohere/serving.py
@staticmethod
def _text_to_tool_output_payload(text: str) -> dict[str, Any]:
    """Wrap a bare tool-content text into a ``tool_output`` payload.

    Matches the cohere api's handling of text-only tool content:
    attempt to interpret ``text`` as a JSON object first; fall
    back to ``{"content": <text>}`` so a text-only tool result
    still has a structured payload we can attach to a citation
    source.
    """
    if not text:
        return {}
    try:
        parsed = json.loads(text)
    except (json.JSONDecodeError, TypeError):
        return {"content": text}
    if isinstance(parsed, dict):
        return parsed
    return {"content": text}

_to_wire_citation(citation)

Coerce one parser-produced citation into the SDK wire shape.

Accepts both :class:InternalCitation instances (what the unary path receives directly from the parser via last_unary_citations) and plain dict payloads (what the streaming path sees, because DeltaMessage.citations is an untyped extras field on the OpenAI wire protocol and dict shapes survive model_validate_json unchanged). Both flows end up carrying the same key names, so a small _get helper handles both without a shape-specific code path.

Source code in vllm/entrypoints/cohere/serving.py
def _to_wire_citation(self, citation: Any) -> Citation | None:
    """Coerce one parser-produced citation into the SDK wire shape.

    Accepts both :class:`InternalCitation` instances (what the
    unary path receives directly from the parser via
    ``last_unary_citations``) and plain ``dict`` payloads (what
    the streaming path sees, because ``DeltaMessage.citations`` is
    an untyped extras field on the OpenAI wire protocol and dict
    shapes survive ``model_validate_json`` unchanged). Both flows
    end up carrying the same key names, so a small ``_get`` helper
    handles both without a shape-specific code path.
    """

    def _get(obj: Any, key: str, default: Any = None) -> Any:
        if isinstance(obj, dict):
            return obj.get(key, default)
        return getattr(obj, key, default)

    raw_sources = _get(citation, "sources") or []
    wire_sources: list[dict[str, Any]] = []
    for src in raw_sources:
        # Sources arrive pre-resolved from the parser (or as trusted
        # dicts on the streaming path). Match the cohere api's
        # behavior of preserving id-less sources (e.g. text-only
        # tool results): cohere's ``Source`` types treat ``id`` as
        # optional, so we simply drop the field via
        # ``exclude_none=True`` instead of dropping the source.
        if isinstance(src, dict):
            wire = {k: v for k, v in src.items() if v is not None}
            if wire:
                wire_sources.append(wire)
        elif isinstance(src, InternalCitationSource):
            wire_sources.append(src.model_dump(exclude_none=True))
    if not wire_sources:
        return None

    cite_type = _get(citation, "type")
    if cite_type == "THINKING_CONTENT" and not self._is_reasoning_model:
        # Non-reasoning Command models surface the reasoning block
        # as ``tool_plan`` (see ``_chat_completion_to_v2``), so any
        # ``is_thinking`` citation from melody is actually a PLAN
        # citation on the wire.
        cite_type = "PLAN"

    payload = {
        "start": _get(citation, "start"),
        "end": _get(citation, "end"),
        "text": _get(citation, "text"),
        "sources": wire_sources,
        "type": cite_type,
    }
    try:
        return Citation.model_validate(payload)
    except Exception:  # pragma: no cover - defensive
        logger.warning(
            "Skipping malformed citation payload: %r", payload, exc_info=True
        )
        return None

_tool_content_block_to_source(block) classmethod

Extract the wire-shape source for one tool-message content block.

Handles both cohere DocumentToolContent and TextToolContent blocks:

  • document blocks with an explicit id produce a tool source keyed by that id (payload is the document's data dict, or {"text": ...} for scalar data).
  • document blocks without an id and text blocks produce an id-less tool source: the cohere api emits the payload with an empty id in this case, and cohere's SDK treats ToolSource.id as optional -- so we simply omit id on the wire instead of inventing a synthetic one.

Returns None only for shapes we don't recognise at all; that slot still gets counted (callers advance the bucket cursor over the block regardless) so numbering stays aligned with melody.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _tool_content_block_to_source(
    cls,
    block: Any,
) -> InternalCitationSource | None:
    """Extract the wire-shape source for one tool-message content block.

    Handles both cohere ``DocumentToolContent`` and
    ``TextToolContent`` blocks:

    * ``document`` blocks with an explicit id produce a
      ``tool`` source keyed by that id (payload is the document's
      ``data`` dict, or ``{"text": ...}`` for scalar data).
    * ``document`` blocks *without* an id and ``text`` blocks
      produce an id-less ``tool`` source: the cohere api emits
      the payload with an empty id in this case, and cohere's
      SDK treats ``ToolSource.id`` as optional -- so we simply
      omit ``id`` on the wire instead of inventing a synthetic
      one.

    Returns ``None`` only for shapes we don't recognise at all;
    that slot still gets counted (callers advance the bucket
    cursor over the block regardless) so numbering stays aligned
    with melody.
    """
    block_type = getattr(block, "type", None)
    if block_type == "document":
        doc: Document | None = getattr(block, "document", None)
        if doc is None:
            return None
        doc_id = doc.id or None
        data = doc.data
        if isinstance(data, dict):
            payload: dict[str, Any] = dict(data)
        elif data is None:
            payload = {}
        else:
            # Match the top-level ``documents`` handling (see
            # ``_apply_cohere_template_kwargs``): non-dict payloads
            # get wrapped as ``{"text": ...}`` so clients get a
            # renderable body instead of a bare id.
            payload = {"text": data}
        if doc_id:
            payload.setdefault("id", doc_id)
        return InternalCitationSource(
            type="tool",
            id=doc_id,
            tool_output=payload,
        )
    if block_type == "text":
        text = getattr(block, "text", None) or ""
        payload = cls._text_to_tool_output_payload(text)
        return InternalCitationSource(
            type="tool",
            id=None,
            tool_output=payload,
        )
    return None

_walk_citable_positions(messages, documents) classmethod

Single-pass implementation of melody's citation numbering rule.

Mirrors PromptRenderIds::from_messages in melody/src/templating/util.rs. Emits one :class:_CitablePosition per document-bearing slot melody assigns while rendering the prompt:

  • Bucket numbering. Bucket 0 is reserved for the top-level documents array when present, and each unique tool_call_id claims the next integer on first-seen basis -- registered from whichever of the assistant's tool_calls[].id or the tool message's tool_call_id appears first in the history.
  • Per-bucket result-index slots. Consumed by every content item, not just documents -- a text block occupies a slot too, and the source at that slot carries an id-less tool_output payload synthesized via :meth:_text_to_tool_output_payload, matching the cohere api's behavior for text-only tool content. Strings and empty contents each consume exactly one slot because the cohere renderer wraps them into a single text block before handing the message off to melody. Multiple tool messages sharing a tool_call_id accumulate into the same bucket, so later docs' slots are offset by the sum of previous messages' content lengths.
  • Doc ids attached to each position. Top-level docs contribute both the explicit id (if set) and the doc_{idx} fallback used for inbound addressing; tool document blocks contribute their single document.id when present. The ids tuple is empty for slots that are only reachable positionally (text-only tool content, docs without a client-provided id) -- those slots still have a fully-populated :class:InternalCitationSource, just no client-visible id to cite them by.

Ids alone key the inbound helper (:meth:_build_doc_id_to_prompt_position, which drives request-side citation resolution); the resolved :class:InternalCitationSource (with type / id / payload) keys the outbound helper (:meth:_build_position_to_source, whose result is forwarded to the reasoning parser via chat_template_kwargs[POSITION_TO_SOURCE_KEY]).

Cohere's on-wire semantics -- ToolSource.id and DocumentSource.id both carry a document id, with type acting only as a payload-shape hint -- are the invariant this walk is mirroring.

Source code in vllm/entrypoints/cohere/serving.py
@classmethod
def _walk_citable_positions(
    cls, messages: list, documents: list | None
) -> list[_CitablePosition]:
    """Single-pass implementation of melody's citation numbering rule.

    Mirrors ``PromptRenderIds::from_messages`` in
    ``melody/src/templating/util.rs``. Emits one
    :class:`_CitablePosition` per document-bearing slot melody
    assigns while rendering the prompt:

    * **Bucket numbering.** Bucket ``0`` is reserved for the
      top-level ``documents`` array when present, and each unique
      ``tool_call_id`` claims the next integer on first-seen basis
      -- registered from whichever of the assistant's
      ``tool_calls[].id`` or the tool message's ``tool_call_id``
      appears first in the history.
    * **Per-bucket result-index slots.** Consumed by *every*
      content item, not just documents -- a text block occupies a
      slot too, and the source at that slot carries an id-less
      ``tool_output`` payload synthesized via
      :meth:`_text_to_tool_output_payload`, matching the cohere
      api's behavior for text-only tool content. Strings and
      empty contents each consume exactly one slot because the
      cohere renderer wraps them into a single text block before
      handing the message off to melody. Multiple tool messages
      sharing a ``tool_call_id`` accumulate into the same bucket,
      so later docs' slots are offset by the sum of previous
      messages' content lengths.
    * **Doc ids attached to each position.** Top-level docs
      contribute both the explicit ``id`` (if set) and the
      ``doc_{idx}`` fallback used for inbound addressing; tool
      document blocks contribute their single ``document.id``
      when present. The ``ids`` tuple is empty for slots that
      are only reachable positionally (text-only tool content,
      docs without a client-provided id) -- those slots still
      have a fully-populated :class:`InternalCitationSource`,
      just no client-visible id to cite them by.

    Ids alone key the inbound helper
    (:meth:`_build_doc_id_to_prompt_position`, which drives
    request-side citation resolution); the resolved
    :class:`InternalCitationSource` (with ``type`` / ``id`` /
    payload) keys the outbound helper
    (:meth:`_build_position_to_source`, whose result is forwarded
    to the reasoning parser via
    ``chat_template_kwargs[POSITION_TO_SOURCE_KEY]``).

    Cohere's on-wire semantics -- ``ToolSource.id`` and
    ``DocumentSource.id`` both carry a document id, with ``type``
    acting only as a payload-shape hint -- are the invariant this
    walk is mirroring.
    """
    out: list[_CitablePosition] = []
    tool_call_id_to_bucket: dict[str, int] = {}
    # Next unassigned ``tool_result_index`` per bucket. Advances
    # by ``len(content)`` (or 1 for string / missing content) for
    # every tool message, whether or not the content had any doc
    # ids in it -- matching melody's per-slot accounting.
    bucket_result_len: dict[int, int] = {}
    next_bucket = 0

    if documents:
        for idx, doc in enumerate(documents):
            # Every top-level doc is addressable inbound by a
            # synthetic ``doc_{idx}`` fallback (so clients can cite
            # positional docs). On the *outbound* wire we prefer
            # the client's explicit id when present, and omit the
            # ``id`` field entirely when it's not -- matching
            # cohere's ``DocumentSource`` where ``id`` is optional
            # and preserving "no id in, no id out" symmetry.
            fallback_id = f"doc_{idx}"
            if isinstance(doc, str):
                payload: dict[str, Any] = {"text": doc}
                explicit_id: str | None = None
            else:
                payload = (
                    dict(doc.data)
                    if isinstance(doc.data, dict)
                    else {"text": doc.data}
                )
                explicit_id = doc.id or None
                if explicit_id:
                    payload.setdefault("id", explicit_id)
            # Register explicit id first so it wins over the
            # fallback for ``setdefault``-style inbound lookup.
            ids: tuple[str, ...] = (
                (explicit_id, fallback_id) if explicit_id else (fallback_id,)
            )
            out.append(
                _CitablePosition(
                    bucket=0,
                    result_idx=idx,
                    ids=ids,
                    source=InternalCitationSource(
                        type="document", id=explicit_id, document=payload
                    ),
                )
            )
        next_bucket = 1

    def register_bucket(tool_call_id: str) -> int:
        nonlocal next_bucket
        bucket = tool_call_id_to_bucket.get(tool_call_id)
        if bucket is None:
            bucket = next_bucket
            tool_call_id_to_bucket[tool_call_id] = bucket
            next_bucket += 1
        return bucket

    for msg in messages:
        if isinstance(msg, ToolChatMessageV2):
            if not msg.tool_call_id:
                continue
            bucket = register_bucket(msg.tool_call_id)
            base = bucket_result_len.get(bucket, 0)
            if isinstance(msg.content, list):
                for offset, block in enumerate(msg.content):
                    info = cls._tool_content_block_to_source(block)
                    if info is None:
                        continue
                    # Only real document ids get registered in the
                    # inbound-facing ``ids`` tuple; text-only tool
                    # content produces a source with ``id=None``
                    # to match the cohere api -- the text is
                    # exposed as ``tool_output.content`` but
                    # there's no id for clients to cite by.
                    pos_ids: tuple[str, ...] = (info.id,) if info.id else ()
                    out.append(
                        _CitablePosition(
                            bucket=bucket,
                            result_idx=base + offset,
                            ids=pos_ids,
                            source=info,
                        )
                    )
                bucket_result_len[bucket] = base + len(msg.content)
            else:
                # A string or missing content is wrapped by the
                # cohere renderer into a single text block before
                # melody sees it, so it consumes one slot. For
                # string content, emit a synthetic id-less tool
                # source carrying the text as its payload so a
                # model citation against this slot still has
                # something to attach on the wire (matching the
                # id-less tool_output behavior for text-only
                # ``ToolChatMessageV2`` content blocks). Missing
                # content still advances the slot cursor but
                # produces no source -- a citation against it
                # would be dropped by ``_to_wire_citation``.
                if isinstance(msg.content, str):
                    payload = cls._text_to_tool_output_payload(msg.content)
                    out.append(
                        _CitablePosition(
                            bucket=bucket,
                            result_idx=base,
                            ids=(),
                            source=InternalCitationSource(
                                type="tool", id=None, tool_output=payload
                            ),
                        )
                    )
                bucket_result_len[bucket] = base + 1
        elif isinstance(msg, AssistantChatMessageV2) and msg.tool_calls:
            for tc in msg.tool_calls:
                if tc.id:
                    register_bucket(tc.id)

    return out

create_chat_v2(request, raw_request=None) async

Implements POST /cohere/v2/chat.

Source code in vllm/entrypoints/cohere/serving.py
async def create_chat_v2(
    self,
    request: CohereChatV2Request,
    raw_request: Request | None = None,
) -> AsyncGenerator[str, None] | CohereChatV2Response | ErrorResponse:
    """Implements ``POST /cohere/v2/chat``."""
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug(
            "Received Cohere v2 chat request %s", request.model_dump_json()
        )

    chat_req = self._convert_v2_to_chat_completion(request)
    if logger.isEnabledFor(logging.DEBUG):
        logger.debug(
            "Converted Cohere v2 -> ChatCompletion: %s",
            chat_req.model_dump_json(),
        )

    generator = await self.create_chat_completion(chat_req, raw_request)

    match generator:
        case ErrorResponse():
            return generator
        case ChatCompletionResponse():
            return self._chat_completion_to_v2(generator, request)
        case _:
            return self._chat_completion_stream_to_v2(generator, request)

ContentBlockType

Bases: str, Enum

Wire-format / internal discriminator for chat content blocks.

THINKING and TEXT are the documented Cohere v2 content-block type discriminators on the wire. TOOL_CALL is reserved for the internal stream state machine (see :class:_StreamState) when a tool call is the currently open block; it is never serialized.

Source code in vllm/entrypoints/cohere/serving.py
class ContentBlockType(str, Enum):
    """Wire-format / internal discriminator for chat content blocks.

    ``THINKING`` and ``TEXT`` are the documented Cohere v2 content-block
    type discriminators on the wire. ``TOOL_CALL`` is reserved for the
    internal stream state machine (see :class:`_StreamState`) when a
    tool call is the currently open block; it is never serialized.
    """

    THINKING = "thinking"
    TEXT = "text"
    TOOL_CALL = "tool_call"

_CitablePosition

Bases: NamedTuple

One entry in melody's citation numbering scheme, resolved to wire shape.

Populated by :meth:CohereServingChatV2._walk_citable_positions (the single source of truth for melody's numbering rule) and consumed by both the inbound (:meth:CohereServingChatV2._build_doc_id_to_prompt_position, which drives citation resolution on the request side) and outbound (:meth:CohereServingChatV2._build_position_to_source, whose result is forwarded to the reasoning parser via chat_template_kwargs[POSITION_TO_SOURCE_KEY]) helpers -- so the numbering rule only lives in one place.

ids lists every id under which this position is addressable: top-level documents contribute both their explicit id (if set) and the doc_{idx} synthetic fallback, in that order so setdefault-style resolution prefers the explicit id; tool-message documents contribute their single document.id. source is the fully-resolved wire-shape source (type / id / document or tool_output payload) for the outbound direction.

Source code in vllm/entrypoints/cohere/serving.py
class _CitablePosition(NamedTuple):
    """One entry in melody's citation numbering scheme, resolved to wire shape.

    Populated by :meth:`CohereServingChatV2._walk_citable_positions`
    (the single source of truth for melody's numbering rule) and
    consumed by both the inbound
    (:meth:`CohereServingChatV2._build_doc_id_to_prompt_position`,
    which drives citation resolution on the request side) and outbound
    (:meth:`CohereServingChatV2._build_position_to_source`, whose
    result is forwarded to the reasoning parser via
    ``chat_template_kwargs[POSITION_TO_SOURCE_KEY]``) helpers -- so
    the numbering rule only lives in one place.

    ``ids`` lists every id under which this position is addressable:
    top-level documents contribute both their explicit id (if set) and
    the ``doc_{idx}`` synthetic fallback, in that order so
    ``setdefault``-style resolution prefers the explicit id;
    tool-message documents contribute their single ``document.id``.
    ``source`` is the fully-resolved wire-shape source (``type`` /
    ``id`` / ``document`` or ``tool_output`` payload) for the
    outbound direction.
    """

    bucket: int
    result_idx: int
    ids: tuple[str, ...]
    source: InternalCitationSource

_StreamState

Tracks which Cohere v2 stream block (if any) is currently open.

Source code in vllm/entrypoints/cohere/serving.py
class _StreamState:
    """Tracks which Cohere v2 stream block (if any) is currently open."""

    def __init__(self) -> None:
        self.started: bool = False
        self.ended: bool = False
        self.finish_reason: str | None = None
        self.last_chunk_id: str = ""
        self.active_block: ContentBlockType | None = None
        self.active_block_index: int | None = None
        self.active_tool_index: int | None = None
        self._next_index: int = 0
        self._next_citation_index: int = 0
        self.tool_calls_seen: set[int] = set()

    def next_content_index(self) -> int:
        idx = self._next_index
        self._next_index += 1
        return idx

    def next_citation_index(self) -> int:
        idx = self._next_citation_index
        self._next_citation_index += 1
        return idx

_emit(event)

Serialize a typed stream event into an SSE frame.

The typed event classes in vllm.entrypoints.cohere.protocol (MessageStartEvent, ContentStartEvent, CitationStartEvent, ...) bake the wire-format type field into the model definition, so a plain model_dump_json() already carries the discriminator.

Source code in vllm/entrypoints/cohere/serving.py
def _emit(event: BaseModel) -> str:
    """Serialize a typed stream event into an SSE frame.

    The typed event classes in ``vllm.entrypoints.cohere.protocol``
    (``MessageStartEvent``, ``ContentStartEvent``, ``CitationStartEvent``,
    ...) bake the wire-format ``type`` field into the model definition,
    so a plain ``model_dump_json()`` already carries the discriminator.
    """
    return _sse(event.model_dump_json(exclude_none=True))

_sse(data)

Wrap a JSON payload in a Server-Sent Event frame.

Cohere's stream uses bare data: lines (no event: prefix); each JSON object's type field carries the event discriminator.

Source code in vllm/entrypoints/cohere/serving.py
def _sse(data: str) -> str:
    """Wrap a JSON payload in a Server-Sent Event frame.

    Cohere's stream uses bare ``data:`` lines (no ``event:`` prefix); each
    JSON object's ``type`` field carries the event discriminator.
    """
    return f"data: {data}\n\n"