# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Cohere prompt renderer. Templates the Cohere Command-family prompt formats (cmd3 / cmd4) using the ``cohere_melody`` Rust bindings instead of Jinja. Enabled by passing ``--tokenizer-mode cohere`` on the engine; tokenization itself still flows through the cached HuggingFace tokenizer. The renderer's client surface is ``chat_template_kwargs`` (vLLM's generic passthrough for template-time inputs). It is fed by two different code paths, which speak different vocabularies: 1. :class:`vllm.entrypoints.cohere.serving.CohereServingChatV2` -- translates a native `Cohere Chat v2 request `__ into ``chat_template_kwargs``. 2. :class:`OpenAIServingChat` -- forwards whatever the client puts in ``request.chat_template_kwargs`` directly, without translation. Accordingly the renderer accepts keys from both vocabularies. **Cohere Chat v2 request fields** (populated automatically by ``CohereServingChatV2``; a direct ``chat_template_kwargs`` caller may also pass them): * ``documents``: list of documents (v2 ``documents``) * ``tools``: list of tools (v2 ``tools``) * ``safety_mode``: v2 ``safety_mode`` (``CONTEXTUAL`` / ``STRICT`` / ``NONE``) -- forwarded to melody's cmd3 ``safety_mode`` slot; lowercased. * ``citation_options``: v2 ``citation_options``. Its ``.mode`` (``ENABLED`` / ``DISABLED`` / ``FAST`` / ``ACCURATE`` / ``OFF``) is normalized into melody's cmd3 ``citation_quality`` (``on`` / ``off``) and cmd4 ``grounding`` (``enabled`` / ``disabled`` / ``unknown``) slots. cmd4 has no fast/accurate distinction at the prompt-template layer -- both mean grounding-on. * ``response_format``: v2 ``response_format`` (``json_object`` / ``json_schema``) -- mapped onto melody's ``json_mode`` / ``json_schema`` config slots. * ``thinking``: v2 ``thinking``; its ``.type`` (``enabled`` / ``disabled``) becomes melody's ``reasoning_type``. **Melody template-config knobs** (accepted for direct-passthrough callers; not part of the Cohere Chat v2 surface): * ``cohere_format``: renderer selector, ``cmd3`` or ``cmd4`` (default ``cmd4``). * ``template_id``: pick one of melody's built-in template variants. * ``available_tools``: melody's own name for tools; takes precedence over ``tools`` when both are set. * ``reasoning_type``: direct ``enabled`` / ``disabled`` toggle; overrides derivation from ``thinking.type``. * ``dev_instruction``: developer-instruction override. * ``json_mode`` / ``json_schema``: direct structured-output toggles; override derivation from ``response_format``. * cmd3-only: ``citation_quality`` (``on`` / ``off``), ``skip_preamble``. * cmd4-only: ``grounding`` (``enabled`` / ``disabled`` / ``unknown``; overrides derivation from ``citation_options.mode``), ``platform_instruction``. **Rejected inputs**: ``template_jinja`` and ``template`` are Cohere's own inlets for raw Jinja template source. They are valid at Cohere's API surface but not at vLLM's -- raw template source must flow through the standard ``chat_template`` request field (guarded by ``--trust-request-chat-template``), which this renderer forwards to melody as ``template_jinja``. Setting these keys explicitly in ``chat_template_kwargs`` raises ``ValueError`` so client misconfiguration surfaces loudly. **Everything else** in ``chat_template_kwargs`` is forwarded verbatim to melody as ``additional_template_fields`` and becomes accessible as Jinja variables inside the template. This matches vLLM's documented contract for ``chat_template_kwargs`` ("kwargs accessible by the template"), so e.g. ``chat_template_kwargs={"reasoning_effort": "low"}`` resolves ``{{ reasoning_effort }}`` inside cmd3 / cmd4 templates. Citations produced by Cohere models are surfaced through the Cohere-scoped ``CohereChatMessage.citations`` / ``CohereDeltaMessage.citations`` fields (see :mod:`vllm.entrypoints.cohere.cohere_chat_message`), populated by the ``cohere2`` reasoning parser. The base OpenAI ``ChatMessage`` / ``DeltaMessage`` keep their declared schemas unchanged; the response envelope declares them as ``SerializeAsAny[...]`` so the subclass fields survive JSON serialization. """ from __future__ import annotations import copy import json from enum import Enum from typing import Any from vllm.config import VllmConfig from vllm.entrypoints.chat_utils import ( ChatCompletionMessageParam, ConversationMessage, parse_chat_messages, parse_chat_messages_async, ) from vllm.logger import init_logger from vllm.tokenizers.hf import HfTokenizer from vllm.utils.async_utils import make_async from .base import BaseRenderer from .inputs import DictPrompt from .inputs.preprocess import parse_dec_only_prompt from .params import ChatParams logger = init_logger(__name__) _DEFAULT_FORMAT = "cmd4" _VALID_FORMATS = ("cmd3", "cmd4") class MelodyContentType(str, Enum): """Wire-format discriminator for melody content blocks. These strings are what the cmd3 / cmd4 Jinja templates check against in ``message.content[0].type`` (e.g. the ``thinking`` branch in ``cmd4-v1.jinja``). Keep new values in sync with melody's template schema. """ TEXT = "text" THINKING = "thinking" IMAGE = "image" DOCUMENT = "document" # Reserved ``chat_template_kwargs`` key that carries # ``AssistantChatMessageV2.citations`` from a Cohere v2 request through # to the renderer, keyed by request-message index. Populated by # ``CohereServingChatV2._apply_cohere_template_kwargs`` and consumed by # ``_conversation_to_melody_messages``. The value is a # ``dict[int, list[dict]]`` of melody-shape ``FilterCitation`` dicts. # The leading underscore signals "internal, not part of the public # ``chat_template_kwargs`` surface" -- clients should not set this key # directly. MESSAGES_CITATIONS_KEY = "_messages_citations" # Reserved ``chat_template_kwargs`` key carrying a request-side # ``dict[tuple[int, int], CitationSource]`` map from melody's numeric # ``(tool_call_index, tool_result_index)`` addressing to fully-resolved # wire-shape sources (type, id, document/tool_output payload). # Populated by :meth:`CohereServingChatV2._apply_cohere_template_kwargs` # and consumed by :class:`BaseCohereCommandReasoningParser` at request # start so the parser can attach real document ids on each melody source # it emits -- eliminating the numeric-coord round-trip through the # internal streaming pipeline. Leading underscore signals "internal, # not part of the public ``chat_template_kwargs`` surface" -- clients # should not set this key directly. POSITION_TO_SOURCE_KEY = "_position_to_source" # Reserved ``chat_template_kwargs`` key carrying the original Cohere v2 # tool-message content, keyed by request-message index. Populated by # :meth:`CohereServingChatV2._apply_cohere_template_kwargs` and consumed # by :func:`_conversation_to_melody_messages`, which runs each block # through :func:`_v2_tool_content_to_melody_block` to produce the # melody-shape content list the renderer emits. # # Motivation: OpenAI's ``ChatCompletionContentPartParam`` union (in # ``vllm/entrypoints/chat_utils.py``) only allows ``text`` / ``image_url`` # / ``audio`` / ``video`` / ``file`` content parts on tool messages, so # ``{"type": "document", "document": {...}}`` blocks -- valid in Cohere # v2 -- get rejected by vLLM's shared chat validator before they can # reach the renderer. # ``CohereServingChatV2._convert_tool_message`` works around that by # rewriting each content block into a plain ``text`` part (with document # blocks JSON-serialized inline), but that flattening loses the # structural distinction the cohere renderer needs to shape tool-result # payloads for cmd3/cmd4. The serving layer therefore forwards the # original v2 content blocks verbatim (as plain dicts) under this key, # and the renderer owns the v2-shape → melody-shape mapping. # # Value type: ``dict[int, list[dict[str, Any]]]``. Each list entry is a # ``model_dump``'d ``TextToolContent`` / ``DocumentToolContent`` block # (e.g. ``{"type": "text", "text": ...}`` or ``{"type": "document", # "document": {"id": ..., "data": {...}}}``). Index is the position of # the tool message in the request's ``messages`` list, which -- since # ``_convert_messages`` is 1-to-1 -- also matches its position in the # ``ConversationMessage`` list handed to the renderer. Leading # underscore signals "internal, not part of the public # ``chat_template_kwargs`` surface" -- clients should not set this key # directly. TOOL_MESSAGE_V2_CONTENT_KEY = "_tool_message_v2_content" # Keys this renderer interprets directly from ``chat_template_kwargs`` and # maps onto typed melody render-config fields. Everything *not* in this # set is forwarded verbatim to melody as ``additional_template_fields`` # (i.e. as Jinja template variables), so callers can write # ``chat_template_kwargs = {"my_var": "..."}`` and have ``{{ my_var }}`` # resolve inside the template -- matching vLLM's documented contract # for ``chat_template_kwargs``. _RENDERER_CONSUMED_KEYS = frozenset( { "cohere_format", "template_id", # ``template_jinja`` / ``template`` are Cohere-only inputs for # raw Jinja source -- valid at Cohere's own API surface but not # at vLLM's. In vLLM, raw template source must flow through the # standard ``chat_template`` request field, so a non-``None`` # value here is rejected in ``_build_render_config``. The keys # stay in the consumed set so an explicit ``None`` (e.g. from # ``.model_dump(exclude_none=False)``) is dropped rather than # surfacing as a stray ``{{ template_jinja }}`` Jinja variable. "template_jinja", "template", "use_jinja", "documents", "available_tools", "tools", "reasoning_type", "thinking", "dev_instruction", "response_format", "json_schema", "json_mode", "safety_mode", "citation_quality", "citation_options", "skip_preamble", "grounding", "platform_instruction", MESSAGES_CITATIONS_KEY, POSITION_TO_SOURCE_KEY, TOOL_MESSAGE_V2_CONTENT_KEY, } ) def _try_import_melody(): try: import cohere_melody # type: ignore return cohere_melody except ImportError as e: # pragma: no cover - exercised at runtime raise ImportError( "The `cohere` tokenizer/renderer mode requires the " "`cohere_melody` package. Install it via " "`pip install 'cohere-melody>=0.11.1'` or build from " "https://github.com/cohere-ai/melody." ) from e _MELODY_ROLES = frozenset({"system", "user", "chatbot", "tool"}) # Cohere v2 ``citation_options.mode`` -> melody cmd4 ``grounding``. # v2 surfaces five modes (``ENABLED`` / ``DISABLED`` / ``FAST`` / # ``ACCURATE`` / ``OFF``) but cmd4's template doesn't differentiate # fast vs accurate at the prompt layer -- both just turn grounding on, # and ``ENABLED`` / ``DISABLED`` are direct aliases. Melody itself # accepts ``unknown`` / ``enabled`` / ``disabled``. _CMD4_GROUNDING_FROM_MODE = { "fast": "enabled", "accurate": "enabled", "on": "enabled", "enabled": "enabled", "off": "disabled", "disabled": "disabled", "unknown": "unknown", } def _normalize_cmd4_grounding(value: Any) -> str: """Coerce a user-facing grounding/citation mode to melody's vocab. Raises ``ValueError`` rather than letting an unrecognized value slip through to ``render_cmd4`` and surface as a generic ``Invalid config: grounding`` from melody. """ out = _CMD4_GROUNDING_FROM_MODE.get(value.lower()) if out is None: raise ValueError( f"Unrecognized cmd4 grounding value: {value!r}. Expected one " f"of ENABLED / DISABLED / FAST / ACCURATE / OFF (from " f"citation_options.mode), or the direct melody values " f"enabled / disabled / unknown." ) return out def _role_to_melody(role: str) -> str: """Map an OpenAI role to the role string melody expects. The cmd3 / cmd4 jinja templates only recognize ``system``, ``user``, ``assistant``/``chatbot``, and ``tool``: any other role is silently dropped by the template's role-dispatch chain (no fallback branch), which produces a malformed prompt without any error. We therefore refuse unknown roles up front rather than letting them disappear. Aliases: * ``assistant`` -> ``chatbot`` (Cohere's historical assistant role name used by the templates). * ``developer`` -> ``system`` (OpenAI's ``developer`` role is documented as high-priority instructions, which maps onto the ``system`` slot in Cohere's prompt format). """ role = role.lower() if role == "assistant": return "chatbot" if role == "developer": return "system" if role in _MELODY_ROLES: return role raise ValueError( f"Unsupported message role for the cohere renderer: {role!r}. " "Expected one of: system, developer, user, assistant, chatbot, tool." ) def _normalize_tool_call(tc: dict[str, Any] | Any) -> dict[str, Any]: """Normalize an OpenAI tool call dict into melody's tool call shape. melody expects ``{id, name, parameters: }`` whereas OpenAI delivers ``{id, type, function: {name, arguments: }}``. """ # Pydantic objects if hasattr(tc, "model_dump"): tc = tc.model_dump() if not isinstance(tc, dict): raise TypeError(f"Unexpected tool_call value: {tc!r}") fn = tc.get("function") or {} name = fn.get("name") or tc.get("name") or "" args = fn.get("arguments") if args is None: args = tc.get("arguments", {}) # melody expects a JSON-encoded string if not isinstance(args, str): args = json.dumps(args, ensure_ascii=False) return { "id": tc.get("id") or "", "name": name, "parameters": args, } def _content_blocks(content: Any) -> list[dict[str, Any]]: """Convert OpenAI ``ConversationMessage.content`` into melody content blocks. The chat_utils ``content_format="openai"`` produces a list of ``{type: text, text: ...}`` dicts already; we pass those through with minimal coercion. Plain string content is wrapped in a single text block. Image / multimodal placeholder dicts (``{"type": "image"}``) are forwarded as-is so that templates can reference image placeholders that the upstream tokenizer expands separately. """ if content is None: return [] if isinstance(content, str): return [{"type": MelodyContentType.TEXT, "text": content}] blocks: list[dict[str, Any]] = [] for part in content: if isinstance(part, str): blocks.append({"type": MelodyContentType.TEXT, "text": part}) continue if not isinstance(part, dict): raise TypeError(f"Unexpected content part: {part!r}") part_type = part.get("type", MelodyContentType.TEXT) if part_type in ("text", "input_text", "output_text", "refusal"): blocks.append( {"type": MelodyContentType.TEXT, "text": part.get("text", "")} ) elif part_type == MelodyContentType.THINKING: blocks.append( { "type": MelodyContentType.THINKING, "thinking": part.get("thinking", ""), } ) elif part_type == MelodyContentType.IMAGE: blocks.append( { "type": MelodyContentType.IMAGE, "image": { "template_placeholder": part.get( "template_placeholder", "" ), }, } ) elif part_type == MelodyContentType.DOCUMENT: doc = part.get("document") if isinstance(doc, dict): blocks.append({"type": MelodyContentType.DOCUMENT, "document": doc}) else: # Fall back to wrapping arbitrary string as text. blocks.append({"type": MelodyContentType.TEXT, "text": json.dumps(doc)}) elif part_type == "tool_reference": # Tool references are rendered by name; emit as text so the # renderer downstream is content-format agnostic. blocks.append( { "type": MelodyContentType.TEXT, "text": part.get("name") or part.get("text", ""), } ) else: # Unknown block type: render as text fallback. text = part.get("text") or part.get(part_type) or "" text_str = text if isinstance(text, str) else json.dumps(text) blocks.append({"type": MelodyContentType.TEXT, "text": text_str}) return blocks def _v2_tool_content_to_melody_block(block: dict[str, Any]) -> dict[str, Any] | None: """Convert one Cohere v2 tool-message content block into a melody ``document`` content block. Cohere's chat v2 admits two shapes for tool-message content parts: * ``TextToolContent`` -- ``{"type": "text", "text": "..."}``. * ``DocumentToolContent`` -- ``{"type": "document", "document": {"id"?: str, "data": {...}}}``. Both are emitted as melody ``document`` blocks, mirroring the cohere api, which routes every tool content part through the same "tool output as document" conversion. The rules: * Text that parses to a JSON object becomes that object directly (so structured tool output flows through as first-class fields). * Text that doesn't parse as an object -- empty, non-JSON, or JSON scalar/array -- gets wrapped as ``{"content": }`` so templates have a stable field name to look up. * ``document`` blocks unwrap ``document.data`` (a flat map) into the melody payload; ``document.id`` is intentionally dropped here because outbound citation binding tracks it separately via ``POSITION_TO_SOURCE_KEY``. Only the JSON-object case actually changes what gets rendered. The cmd3/cmd4 templates already wrap a bare ``text`` content block as ``{"content": }`` themselves, so routing plain text through here is a no-op on the prompt -- it is the JSON-object unwrap that promotes structured tool output from an escaped string blob into individually addressable (and citable) result fields. Note the payload is the *flat* field map: melody's ``Content`` carries ``document`` as a plain map, so re-nesting it under a ``data`` key would render a spurious ``{"data": {...}}`` wrapper into the tool-result block. Returns ``None`` for unrecognised shapes; the caller should skip those blocks (the citation-numbering walk in ``CohereServingChatV2._walk_citable_positions`` advances its own cursor over unknown blocks, so slot alignment is preserved). """ block_type = block.get("type") if block_type == "text": text = block.get("text") or "" try: parsed = json.loads(text) except (json.JSONDecodeError, TypeError): parsed = None if isinstance(parsed, dict): payload: dict[str, Any] = dict(parsed) else: payload = {"content": text} return {"type": MelodyContentType.DOCUMENT, "document": payload} if block_type == "document": doc = block.get("document") if not isinstance(doc, dict): return None data = doc.get("data") payload = dict(data) if isinstance(data, dict) else {} return {"type": MelodyContentType.DOCUMENT, "document": payload} return None def _document_to_melody(doc: Any) -> dict[str, Any]: """Coerce a Cohere v2 document into melody's ``Document`` (dict) shape.""" match doc: case str(): return {"text": doc} # Cohere v2 wraps documents in {id, data: {...}}; the ``**rest`` # capture keeps the outer ``id`` (and any other outer keys) so we # can lift them onto the payload without losing them. case {"data": dict() as data, **rest}: payload = dict(data) if (outer_id := rest.get("id")) is not None and "id" not in payload: payload["id"] = outer_id return payload case dict(): return dict(doc) case _: raise TypeError(f"Unsupported document type: {type(doc).__name__}") def _tool_to_melody(tool: Any) -> dict[str, Any]: """Coerce a chat completions tool definition into melody's ``Tool`` shape. Accepts either the raw OpenAI tool wrapper ``{type:"function", function: {name, description, parameters}}`` or a flat ``{name, description, parameters}`` dict (which is what melody itself expects). """ if hasattr(tool, "model_dump"): tool = tool.model_dump() if not isinstance(tool, dict): raise TypeError(f"Unsupported tool type: {type(tool).__name__}") if "function" in tool and isinstance(tool["function"], dict): fn = tool["function"] else: fn = tool return { "name": fn.get("name", ""), "description": fn.get("description", "") or "", "parameters": fn.get("parameters") or {}, } def _conversation_to_melody_messages( conversation: list[ConversationMessage], messages_citations: dict[int, list[dict[str, Any]]] | None = None, tool_message_v2_content: dict[int, list[dict[str, Any]]] | None = None, ) -> list[dict[str, Any]]: """Convert vLLM ``ConversationMessage``s into melody's message dict shape. ``messages_citations`` is an optional index-keyed lookup of per-message citations from the request, populated by ``CohereServingChatV2`` under the :data:`MESSAGES_CITATIONS_KEY` chat_template_kwargs entry. Values must already be in melody's ``FilterCitation`` dict shape (see ``CohereServingChatV2._sdk_citation_to_melody``). ``tool_message_v2_content`` is an optional index-keyed lookup of the original Cohere v2 tool-message content blocks (as plain dicts), populated by ``CohereServingChatV2`` under the :data:`TOOL_MESSAGE_V2_CONTENT_KEY` entry. When set for a given index, its blocks -- run through :func:`_v2_tool_content_to_melody_block` to apply the v2 → melody mapping -- replace the block list this function would otherwise derive from ``msg["content"]``. This lets the renderer own the v2-shape → melody-shape conversion for the structured ``document`` blocks cohere v2 allows on tool messages (which don't survive OpenAI's content-parts validator on the way in, so ``_convert_tool_message`` rewrites them into ``text`` parts). Both index lookups key by position in the *input* ``conversation`` list, which currently maps 1-to-1 with ``CohereChatV2Request.messages`` for text-only Cohere requests. This mapping breaks silently if ``parse_chat_messages`` ever expands a single input message into multiple entries (e.g. multi-modal splitting), which is not the case today. """ out: list[dict[str, Any]] = [] for i, msg in enumerate(conversation): role = _role_to_melody(msg.get("role", "user")) if ( tool_message_v2_content is not None and (v2_blocks := tool_message_v2_content.get(i)) is not None ): content_blocks = [] for v2_block in v2_blocks: mapped = _v2_tool_content_to_melody_block(v2_block) if mapped is not None: content_blocks.append(mapped) else: content_blocks = _content_blocks(msg.get("content")) # Treat reasoning content as a thinking block on assistant turns # so multi-turn reasoning is preserved in the rendered prompt. # # Cohere models output thought as either a ``thinking`` # block (reasoning models) or a ``tool_plan`` field (older non- # reasoning Command models), but vLLM's ConversationMessage has a # single unified ``reasoning`` field that drops that difference. # The cmd3 / cmd4 jinja templates render ``thinking`` blocks in both # the case of tool calls and regular thinking blocks, so this is fine # from the renderer's perspective. reasoning = msg.get("reasoning") or msg.get("reasoning_content") if role == "chatbot" and reasoning: content_blocks.insert( 0, {"type": MelodyContentType.THINKING, "thinking": reasoning}, ) tool_calls = [_normalize_tool_call(tc) for tc in (msg.get("tool_calls") or [])] out_msg: dict[str, Any] = { "role": role, "content": content_blocks, "tool_calls": tool_calls, } tool_call_id = msg.get("tool_call_id") if tool_call_id: out_msg["tool_call_id"] = tool_call_id if messages_citations and (cites := messages_citations.get(i)): out_msg["citations"] = cites out.append(out_msg) return out def _build_render_config( conversation: list[ConversationMessage], chat_template_kwargs: dict[str, Any], chat_template: str | None = None, ) -> tuple[str, dict[str, Any]]: """Build the ``render_cmd3`` / ``render_cmd4`` config dict. ``chat_template`` is the standard vLLM per-request template override (from ``ChatCompletionRequest.chat_template`` / the server-side ``--chat-template`` flag). When non-``None`` it is forwarded to melody as ``template_jinja`` so a single vLLM-native path drives template source through the trust guard. Returns ``(format, config_dict)`` where ``format`` is either ``"cmd3"`` or ``"cmd4"``. """ fmt = chat_template_kwargs.get("cohere_format", _DEFAULT_FORMAT) if fmt not in _VALID_FORMATS: raise ValueError( f"Invalid cohere_format={fmt!r}; expected one of {_VALID_FORMATS}" ) # Reject Cohere-only inlets for raw Jinja source. These keys are # valid at Cohere's own API surface but not at vLLM's: callers must # use the standard vLLM ``chat_template`` request field so the # ``--trust-request-chat-template`` guard applies uniformly. for cohere_only_key in ("template_jinja", "template"): if chat_template_kwargs.get(cohere_only_key) is not None: raise ValueError( f"chat_template_kwargs.{cohere_only_key!r} is not accepted " "in vLLM (it is a Cohere-only field); pass the template " "body via the standard 'chat_template' request field " "instead (requires --trust-request-chat-template)." ) config: dict[str, Any] = { "messages": _conversation_to_melody_messages( conversation, chat_template_kwargs.get(MESSAGES_CITATIONS_KEY), chat_template_kwargs.get(TOOL_MESSAGE_V2_CONTENT_KEY), ), } # ``template_id`` is a selector for one of melody's built-in template # variants (not raw source), so it is safe to accept from the client. if template_id := chat_template_kwargs.get("template_id"): config["template_id"] = template_id # Raw template source flows exclusively through the standard vLLM # ``chat_template`` field so it is subject to the # ``--trust-request-chat-template`` guard in OnlineRenderer. if chat_template: config["template_jinja"] = chat_template # Only support Jinja with vllm config["use_jinja"] = True # Documents documents = chat_template_kwargs.get("documents") or [] if documents: config["documents"] = [_document_to_melody(d) for d in documents] # Tools - prefer explicit ``available_tools``, fall back to OpenAI ``tools`` tools = ( chat_template_kwargs.get("available_tools") or chat_template_kwargs.get("tools") or [] ) if tools: config["available_tools"] = [_tool_to_melody(t) for t in tools] # Reasoning toggle (cmd3 + cmd4) if (rt := chat_template_kwargs.get("reasoning_type")) is not None: config["reasoning_type"] = str(rt) elif "thinking" in chat_template_kwargs: # Cohere v2 ``thinking: {type: enabled|disabled}`` shorthand thinking = chat_template_kwargs["thinking"] t = thinking.get("type") if isinstance(thinking, dict) else thinking if t in ("enabled", "disabled"): config["reasoning_type"] = t if (di := chat_template_kwargs.get("dev_instruction")) is not None: config["dev_instruction"] = str(di) # JSON / structured outputs if (rf := chat_template_kwargs.get("response_format")) is not None: rf = rf.model_dump() if hasattr(rf, "model_dump") else dict(rf) rf_type = rf.get("type") if rf_type == "json_object": config["json_mode"] = True elif rf_type in ("json_schema", "json"): schema = rf.get("schema") or rf.get("json_schema") if isinstance(schema, dict) and "schema" in schema: schema = schema["schema"] if schema is not None: config["json_schema"] = ( schema if isinstance(schema, str) else json.dumps(schema) ) if (js := chat_template_kwargs.get("json_schema")) is not None: config["json_schema"] = js if isinstance(js, str) else json.dumps(js) if "json_mode" in chat_template_kwargs: config["json_mode"] = bool(chat_template_kwargs["json_mode"]) if fmt == "cmd3": if (sm := chat_template_kwargs.get("safety_mode")) is not None: config["safety_mode"] = str(sm).lower() # citation_quality: ``on`` / ``off`` cq = chat_template_kwargs.get("citation_quality") if cq is None and (co := chat_template_kwargs.get("citation_options")): mode = co.get("mode") if isinstance(co, dict) else None if mode is not None: cq = "on" if str(mode).lower() != "off" else "off" if cq is not None: config["citation_quality"] = str(cq).lower() if "skip_preamble" in chat_template_kwargs: config["skip_preamble"] = bool(chat_template_kwargs["skip_preamble"]) else: # cmd4 # cmd4 uses ``grounding`` rather than safety_mode/citation_quality. # melody's cmd4 only accepts ``unknown`` / ``enabled`` / ``disabled``, # so the Cohere v2 ``citation_options.mode`` values # (``FAST`` / ``ACCURATE`` / ``OFF``) have to be normalized -- a # raw lowercased passthrough would raise from # ``render_cmd4`` (cmd4 has no fast/accurate distinction at the # prompt-template layer; both request grounding-on). if (gr := chat_template_kwargs.get("grounding")) is not None: config["grounding"] = _normalize_cmd4_grounding(gr) elif co := chat_template_kwargs.get("citation_options"): mode = co.get("mode") if isinstance(co, dict) else None if mode is not None: config["grounding"] = _normalize_cmd4_grounding(mode) if (pi := chat_template_kwargs.get("platform_instruction")) is not None: config["platform_instruction"] = str(pi) # Anything we didn't explicitly interpret above is forwarded to melody # as a Jinja template variable. This matches vLLM's documented contract # for ``chat_template_kwargs`` ("kwargs accessible by the template") # and lets callers write e.g. ``{"reasoning_effort": "low"}`` directly # without a nested ``additional_template_fields`` wrapper. extra = { k: v for k, v in chat_template_kwargs.items() if k not in _RENDERER_CONSUMED_KEYS } if extra: config["additional_template_fields"] = extra return fmt, config class CohereRenderer(BaseRenderer[HfTokenizer]): """Renderer that templates Cohere prompts via the melody Rust bindings. Tokenization is delegated to the standard HF tokenizer; only the chat-template step is replaced with ``cohere_melody.render_cmd3`` / ``render_cmd4``. Enabled via ``--tokenizer-mode cohere``. """ def __init__( self, config: VllmConfig, tokenizer: HfTokenizer | None, ) -> None: # Match HfRenderer in not mutating the cached tokenizer instance tokenizer = copy.copy(tokenizer) super().__init__(config, tokenizer) # Lazy import to keep `cohere_melody` an optional dependency self._melody = _try_import_melody() # ``render_cmd3`` / ``render_cmd4`` are pure CPU work; cache the # thread-pool wrapper once so the async path doesn't allocate a new # adapter on every request. self._render_async = make_async(self._render, executor=self._executor) def _render(self, fmt: str, config_dict: dict[str, Any]) -> str: if fmt == "cmd3": return self._melody.render_cmd3(config_dict) return self._melody.render_cmd4(config_dict) def render_messages( self, messages: list[ChatCompletionMessageParam], params: ChatParams, ) -> tuple[list[ConversationMessage], DictPrompt]: conversation, mm_data, mm_uuids = parse_chat_messages( messages, self.model_config, content_format="openai", media_io_kwargs=params.media_io_kwargs, mm_processor_kwargs=params.mm_processor_kwargs, ) chat_template_kwargs = dict(params.chat_template_kwargs) fmt, config_dict = _build_render_config( conversation, chat_template_kwargs, params.chat_template ) prompt_text = self._render(fmt, config_dict) prompt = parse_dec_only_prompt(prompt_text) if mm_data is not None: prompt["multi_modal_data"] = mm_data if mm_uuids is not None: prompt["multi_modal_uuids"] = mm_uuids return conversation, prompt async def render_messages_async( self, messages: list[ChatCompletionMessageParam], params: ChatParams, ) -> tuple[list[ConversationMessage], DictPrompt]: conversation, mm_data, mm_uuids = await parse_chat_messages_async( messages, self.model_config, content_format="openai", media_io_kwargs=params.media_io_kwargs, mm_processor_kwargs=params.mm_processor_kwargs, ) chat_template_kwargs = dict(params.chat_template_kwargs) fmt, config_dict = _build_render_config( conversation, chat_template_kwargs, params.chat_template ) prompt_text = await self._render_async(fmt, config_dict) prompt = parse_dec_only_prompt(prompt_text) if mm_data is not None: prompt["multi_modal_data"] = mm_data if mm_uuids is not None: prompt["multi_modal_uuids"] = mm_uuids return conversation, prompt