vllm-project/vllm · critical

tool response messages require a tool_call_id; use ChatMessa

Error message

tool response messages require a tool_call_id; use ChatMessage::tool_response() instead

What it means

This is a `panic!`, not a Result error: `ChatMessage::text()` (request.rs:228) refuses to build a ToolResponse message because tool responses require a `tool_call_id` linking them to the assistant's tool call. The API deliberately has no sensible default, so misuse fails loudly. Use `ChatMessage::tool_response(...)` which takes the id.

Source

Thrown at rust/src/chat/src/request.rs:228

    /// Tool response content associated with one prior assistant tool call.
    ToolResponse {
        content: ChatContent,
        tool_call_id: String,
    },
}

impl ChatMessage {
    /// Construct one chat message with plain string content.
    pub fn text(role: ChatRole, text: impl Into<String>) -> Self {
        let content: String = text.into();

        match role {
            ChatRole::System => Self::system(content),
            ChatRole::Developer => Self::developer(content, None),
            ChatRole::User => Self::user(content),
            ChatRole::Assistant => Self::assistant_text(content),
            ChatRole::ToolResponse => {
                panic!(
                    "tool response messages require a tool_call_id; \
                     use ChatMessage::tool_response() instead"
                )
            }
        }
    }

    /// Construct one chat message with system role.
    pub fn system(content: impl Into<ChatContent>) -> Self {
        Self::System {
            content: content.into(),
        }
    }

    /// Construct one chat message with developer role.
    pub fn developer(content: impl Into<ChatContent>, tools: Option<Vec<ChatTool>>) -> Self {
        Self::Developer {
            content: content.into(),

View on GitHub (pinned to c794754062)

Solutions

  1. Replace the call with `ChatMessage::tool_response(tool_call_id, content)`.
  2. Handle `ChatRole::ToolResponse` as a distinct arm in any role-matching code instead of funneling it through `text()`.
  3. If replaying a history, look up the tool_call_id from the preceding assistant message's tool_calls.

Example fix

// before
let msg = ChatMessage::text(ChatRole::ToolResponse, "42"); // panics

// after
let msg = ChatMessage::tool_response(tool_call_id.clone(), "42");
Defensive patterns

Strategy: type-guard

Validate before calling

for msg in &incoming_messages {
    if msg.role == ChatRole::ToolResponse {
        let id = resolve_tool_call_id(&assistant_msg, msg)?; // must exist
        built.push(ChatMessage::tool_response(id, msg.content_text()));
    }
}

Type guard

fn needs_tool_call_id(role: ChatRole) -> bool {
    matches!(role, ChatRole::ToolResponse)
}

Try / catch

std::panic::set_hook is not a fix — this is a panic by design. Match on the role before constructing:
let msg = match role {
    ChatRole::ToolResponse => ChatMessage::tool_response(tool_call_id, content),
    other => ChatMessage::text(other, content),
};

Prevention

When it happens

Trigger: Calling `ChatMessage::text(ChatRole::ToolResponse, "result body")` — e.g. generic code that maps roles in a loop and routes ToolResponse through the text constructor.

Common situations: Deserializing/echoing conversation histories role-by-role with a match on role; porting client code where ToolResponse was treated as a plain role; replaying OpenAI-format histories.

Related errors


AI-assisted analysis of vllm-project/vllm@c794754062 (2026-08-14). Data as JSON: /api/errors/7cd7713c9087023f. Report an issue: GitHub.