Skip to content

fix(api-server): preserve tool_call_id in conversation_history - #17632

Open
yzxuyang wants to merge 1 commit into
NousResearch:mainfrom
yzxuyang:fix/api-server-conversation-history-preserve-tool-call-id
Open

fix(api-server): preserve tool_call_id in conversation_history#17632
yzxuyang wants to merge 1 commit into
NousResearch:mainfrom
yzxuyang:fix/api-server-conversation-history-preserve-tool-call-id

Conversation

@yzxuyang

Copy link
Copy Markdown

Problem

When sending follow-up messages via the Hermes Web UI through the API Server, DeepSeek (and other strict OpenAI-format providers) returns a 400 error:

messages[3]: missing field `tool_call_id`

Root Cause

_handle_runs() in gateway/platforms/api_server.py reconstructs conversation_history messages with only role and content fields, dropping tool_call_id, tool_calls, and other metadata.

This affects two code paths:

  1. Explicit conversation_history from request body (web UI) [line 2409]
  2. Multi-message input array mode [line 2438]

Fix

Copy all fields from the incoming message dict instead of creating a new dict with only role + content. This preserves tool_call_id and other metadata while still ensuring role/content values are stringified.

Related

Fixes tool-calling conversations via Hermes Web UI when using providers that enforce the OpenAI message schema strictly (DeepSeek, OpenAI, etc.).

When the API server receives conversation_history from the web UI
(via POST /v1/runs), it was reconstructing each message with only 'role'
and 'content' fields, dropping tool_call_id, tool_calls, and other
metadata. This caused a 400 error from strict providers (DeepSeek, etc.)
on follow-up messages, since tool role messages must include tool_call_id.

Fix: copy all fields from the incoming entry, preserving tool_call_id
and other metadata while still ensuring role/content are stringified.
Applies to both explicit conversation_history and multi-message input
array paths.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery provider/deepseek DeepSeek API labels Apr 29, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #17422 — same root cause: api_server.py strips tool_call_id when reconstructing conversation_history messages.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #17422 — same root cause: api_server.py strips tool_call_id when reconstructing conversation_history messages.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating the two /v1/runs history projections. The underlying defect is still present on current main: explicit history is reduced to {role, content} at gateway/platforms/api_server.py:4217, and multi-message input is reduced the same way at gateway/platforms/api_server.py:4243. Those histories are passed to AIAgent.run_conversation() at gateway/platforms/api_server.py:4348-4351, so dropping tool_calls and tool_call_id can break the required assistant→tool pairing.

Problems

  • There is no regression test for either preservation path. tests/gateway/test_api_server_runs.py:156-165 only covers an invalid history shape.

Suggested changes

  • Add tests for explicit conversation_history and multi-message input that assert the mock agent receives both assistant tool_calls and tool tool_call_id unchanged.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 12, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Three PRs address the reported history-normalization defect. #17422 preserves tool metadata in /v1/responses and /v1/runs; #17632 preserves metadata in the two /v1/runs projections; #19415 broadens the fix across chat completions, responses, and runs with a role-aware allowlist plus regression tests for tool and reasoning continuity.

Related pull requests

Duplicates

#17422 and #17632 substantially overlap on preserving tool metadata in api_server.py; #19415 covers that same root cause as a broader allowlisted implementation. The duplicate chain is #17422#17632#19415, with #19415 retaining the broader salvageable scope.

Suggested consolidation

Keep open #19415 with a salvage path: retain its shared role-aware allowlist and regression tests, and verify the full gateway history-normalization behavior before further triage. Close #17632 as duplicate of #19415 despite the MAINTAINER-BOT VERDICT recommending keep_open with high salvageability, because #19415's diff contains the same /v1/runs coverage plus responses/chat paths and tests; #17422 is already closed and remains the earlier overlapping reference.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    subgraph Dup17422 ["PRs duplicating each other"]
        P17422["PR #17422 (closed)"]
        P17632["PR #17632 (open)"]
        P19415["PR #19415 (open)"]
    end
    class P17422 closed
    class P17632 open
    class P19415 open
    class P17632 target
    click P17422 "https://github.com/NousResearch/hermes-agent/pull/17422"
    click P17632 "https://github.com/NousResearch/hermes-agent/pull/17632"
    click P19415 "https://github.com/NousResearch/hermes-agent/pull/19415"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 39 kB of PR diffs, 7 kB of issue/PR text, 2 kB of discussion (7 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants