Skip to content

fix: feed tool results to open ai compat providers as role tool with id - #1505

Open
AngeloDanducci wants to merge 2 commits into
generative-computing:mainfrom
AngeloDanducci:ad-1389
Open

fix: feed tool results to open ai compat providers as role tool with id#1505
AngeloDanducci wants to merge 2 commits into
generative-computing:mainfrom
AngeloDanducci:ad-1389

Conversation

@AngeloDanducci

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Fixes #1389

Description

Feed tool results back to OpenAI-compatible providers as role: "tool" messages with a matching tool_call_id, instead of as user text.

Spec-strict providers may still mishandle the result prior to this change (as a string).

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code was added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

Attribution

  • AI coding assistants used

Adding a new component, requirement, sampling strategy, or tool?

If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.

Signed-off-by: AngeloDanducci <angelo.danducci.ii@ibm.com>
@AngeloDanducci
AngeloDanducci requested a review from a team as a code owner August 6, 2026 01:09
@github-actions github-actions Bot added the bug Something isn't working label Aug 6, 2026
tool_call = getattr(msg, "_tool", None)
if tool_call is not None and getattr(tool_call, "tool_call_id", None) is not None:
result["tool_call_id"] = tool_call.tool_call_id
name = getattr(msg, "name", None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If name isn't part of the openai spec, will it cause any issues when passed? Or does it get silently ignored?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't collide and extraneous fields should be dropped, but it's legacy and not required.

…-result name

Signed-off-by: AngeloDanducci <angelo.danducci.ii@ibm.com>

@planetf1 planetf1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great fix for #1389. Correct, surgical, and well-tested. LGTM. A few inline suggestions.

# `role: "tool"` message to reference the assistant's originating tool call
# via `tool_call_id` (issue #1389). Emit it when the ToolMessage carries a
# provider-supplied id. Duck-type on `_tool` to avoid importing ToolMessage
# (circular import) and to leave plain `role="tool"` Messages, and ids-less

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The circular-import claim appears incorrect — I tested a module-level from mellea.stdlib.components import ToolMessage and confirmed all entry points (mellea, cli/, every backend) import cleanly. The cycle is already broken by chat.py:180's deferred import of build_tool_calls. A deferred import + isinstance inside this function would be a clean alternative (this file already does that pattern at lines 64–65).

@@ -415,8 +431,12 @@ def build_tool_calls(output: ModelOutputThunk) -> list[ToolCallDict] | None:
assert output.tool_calls is not None
tool_calls: list[ToolCallDict] = []
for model_tool_call in output.tool_calls:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the reuse exists so "the replayed assistant turn and its tool-result turn reference the same call via _tool.tool_call_id". On the openai/litellm paths, the assistant turn's tool calls come from the provider payload (chat.py:203), not from build_tool_calls. The live case is cli/serve/{app,streaming} forwarding upstream ids downstream. Worth adjusting the comment to match the actual call path.

# required to satisfy the result-turn contract.
tool_call = getattr(msg, "_tool", None)
if tool_call is not None and getattr(tool_call, "tool_call_id", None) is not None:
result["tool_call_id"] = tool_call.tool_call_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this gates on is not None while build_tool_calls (line 439) uses truthiness (or). Currently no backend returns "" for tool_call_id, but aligning the checks adds a tiny bit more defense in depth.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support role: "tool" / tool_call_id feedback for tool results

3 participants