Skip to content

fix(providers): keep the assistant text when the turn also calls a tool - #1325

Merged
javiermtorres merged 3 commits into
mozilla-ai:mainfrom
JamMaster1999:fix/replay-assistant-text-with-tool-calls
Sep 1, 2026
Merged

javiermtorres merged 3 commits into
mozilla-ai:mainfrom
JamMaster1999:fix/replay-assistant-text-with-tool-calls

Conversation

@JamMaster1999

@JamMaster1999 JamMaster1999 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

When a model writes a sentence and then calls a tool in the same turn, any-llm loses the sentence on Gemini and Anthropic. Two seams, same shape:

  • gemini/utils.py::_convert_response_to_response_dict set content to None whenever the response carried a function call, so the text part never reached the caller (the streaming converter already kept it). _convert_messages then rebuilt a tool-call turn from tool_calls alone, so even a caller who had the text could not replay it.
  • anthropic/utils.py::_convert_messages_for_anthropic built the tool-call turn from the thinking block plus the tool_use blocks and never read content.

The model cannot see that it already announced its plan, so it announces it again instead of answering. No API error, so nothing in the suite noticed. Bedrock's converter already does this right (bedrock/utils.py:377, text before toolUse), so the fix follows that shape. Gemini reports content and tool_calls together and replays the text Part ahead of the function-call parts; a message-level thought_signature rides on that part, which covers the limitation noted on #1320 for the only shape Gemini 3 has been seen to produce (a non-empty text part). Anthropic inserts the text block between thinking and tool_use. Only a non-empty string adds a part or block: tool-call turns with content None or "" still emit only call parts, so no empty text is ever sent, and list-shaped assistant content keeps today's behaviour on both providers. A side effect on Anthropic: an assistant turn with text and tool_calls: [] used to produce content: [], which the API rejects; it now carries its text.

Live, through acompletion, prompt "Before calling any tool, write one sentence telling me your plan. Then get the weather in Paris.", tool result fed back:

model before: turn 1 content / turn 2 after: turn 1 content / turn 2
gemini-3.1-pro-preview None / "I will use the weather tool to find the current weather conditions in Paris. The current..." "I will use the weather API tool to..." / "The current weather in Paris is 18°C and cloudy."
gemini-3-flash-preview None / "OK. The weather in Paris is currently 18°C and cloudy." "I will call the weather tool to..." / "The current weather in Paris is 18°C and cloudy."
claude-sonnet-5 text / "I'll check the current weather conditions in Paris for you using the weather tool. The we..." text / "The weather in Paris is currently 18°C and cloudy."
claude-haiku-4-5 text / "My plan is to call the get_weather function with Paris as the city parameter. The weather..." text / "The weather in Paris is currently 18°C and cloudy."

Streamed first turns, accumulated and replayed, behave the same on both providers. tests/integration for gemini and anthropic on this branch: 50 passed, 13 skipped, both agent-loop tests included. Unit tests: the response keeps text next to a function call; Gemini replays [text, function_call] with each signature on its own part and round-trips response -> ChatCompletionMessage -> dump -> model turn; a malformed message-level signature on a tool-call turn is rejected like a text-only one; Anthropic replays [thinking, text, tool_use]; tool-call turns with None/"" content still emit only call parts. The positive tests fail on main, the negative ones pin what must not change.

PR Type

  • 🐛 Bug Fix

Relevant issues

Follow-up to #1320 (signed text part beside a function call). None open.

Checklist

  • I understand the code I am submitting.
  • I have added unit tests that prove my fix/feature works
  • I have run this code locally and verified it fixes the issue.
  • New and existing tests pass locally
  • Documentation was updated where necessary
  • I have read and followed the contribution guidelines
  • AI Usage:
    • No AI was used.
    • AI was used for drafting/refactoring.
    • This is fully AI-generated.

AI Usage Information

  • AI Model used: Claude (Fable 5)

  • AI Developer Tool used: Claude Code

  • Any other info you'd like to share:

  • I am an AI Agent filling out this form (check box if true)

Summary by CodeRabbit

  • Bug Fixes
    • Preserved assistant text alongside tool calls for Anthropic and Gemini.
    • Gemini responses now retain text content and finish reasons when function calls are present.
    • Prevented empty text blocks from being emitted for tool-only assistant turns.
  • Tests
    • Added coverage for text and tool-call ordering, signatures, absent content, and response conversion.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bc69a7ae-277a-4fa7-846f-cdb4d3001c36

📥 Commits

Reviewing files that changed from the base of the PR and between f8bd6f8 and c223097.

📒 Files selected for processing (1)
  • tests/unit/providers/test_anthropic_provider.py

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

Anthropic conversion now preserves assistant text before tool-use blocks. Gemini conversion preserves text, function calls, and thought signatures in messages and non-streaming responses. Tests cover ordering, empty content, signature validation, finish reasons, and round-tripping.

Changes

Assistant content preservation

Layer / File(s) Summary
Anthropic assistant block ordering
src/any_llm/providers/anthropic/utils.py, tests/unit/providers/test_anthropic_provider.py
Anthropic conversion retains non-empty text between thinking and tool_use blocks. Empty or absent content emits no empty text block.
Gemini response content
src/any_llm/providers/gemini/utils.py, tests/unit/providers/test_gemini_provider.py
Non-streaming responses expose accumulated text alongside function calls and report the tool_calls finish reason.
Gemini message replay
src/any_llm/providers/gemini/utils.py, tests/unit/providers/test_gemini_provider.py
Gemini conversion retains assistant text before function calls and attaches thought signatures. Tests cover absent content, invalid signatures, and round-tripping.

Suggested reviewers: javiermtorres

Merge Risk: ⚪ Minimal · up to c2230

The change preserves assistant text when a turn also includes tool calls without changing tool authority or deployment behavior. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: preserving assistant text when the same turn calls a tool.
Description check ✅ Passed The description follows the repository template. It explains the bug and implementation, identifies the bug-fix type, references the related issue, records completed checklist items, and documents AI …
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 4 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description follows the repository template. It explains the bug and implementation, identifies the bug-fix type, references the related issue, records completed checklist items, and documents AI usage. Optional AI information fields are left blank, which does not prevent approval.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/any_llm/providers/anthropic/utils.py 93.72% <100.00%> (+0.07%) ⬆️
src/any_llm/providers/gemini/utils.py 89.00% <100.00%> (+0.08%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@javiermtorres
javiermtorres force-pushed the fix/replay-assistant-text-with-tool-calls branch from 238a40a to dcec609 Compare August 25, 2026 13:52
@javiermtorres
javiermtorres requested a balanced review from Copilot August 25, 2026 14:23

Copilot AI 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.

Pull request overview

Preserves assistant text when Gemini or Anthropic responses also invoke tools.

Changes:

  • Retains Gemini text alongside function calls and thought signatures.
  • Replays Anthropic text before tool-use blocks.
  • Adds regression tests for mixed text and tool-call turns.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/any_llm/providers/gemini/utils.py Preserves and replays mixed text and function calls.
src/any_llm/providers/anthropic/utils.py Adds assistant text to tool-call turns.
tests/unit/providers/test_gemini_provider.py Tests mixed turns, signatures, and round trips.
tests/unit/providers/test_anthropic_provider.py Tests text ordering and empty content handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +158 to +159
if isinstance(content, str) and content:
content_blocks.append({"type": "text", "text": content})

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.

@JamMaster1999 please take a look at this suggestion.

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.

@javiermtorres added test_convert_messages_keeps_text_when_tool_calls_is_empty in c223097. An assistant turn with text and tool_calls: [] now converts to a single text block; against main the same input produces content: [], so the test fails there and passes here.

@javiermtorres
javiermtorres force-pushed the fix/replay-assistant-text-with-tool-calls branch from dcec609 to f8bd6f8 Compare August 26, 2026 15:17
The non-streaming converter set content to None whenever the response
carried a function call, and the message converter rebuilt a tool-call
turn from tool_calls alone. Both halves now keep the text: the response
reports content and tool_calls together (as the streaming path already
did), and the replay emits a non-empty text as its own part ahead of the
function-call parts, carrying the message-level thought_signature.
…ol_use blocks

The tool-call branch of _convert_messages_for_anthropic rebuilt the turn
from the thinking block and the tool_use blocks and never read content,
so the text Claude wrote before calling a tool vanished from history.
@javiermtorres
javiermtorres force-pushed the fix/replay-assistant-text-with-tool-calls branch from c223097 to c44bbda Compare September 1, 2026 07:34
@javiermtorres
javiermtorres merged commit 3ee84f6 into mozilla-ai:main Sep 1, 2026
14 checks passed
@github-actions github-actions Bot added the 1.27.0 Included in release 1.27.0 label Sep 3, 2026
JamMaster1999 added a commit to JamMaster1999/any-llm that referenced this pull request Sep 3, 2026
…mozilla-ai#1319

Upstream has merged mozilla-ai#1291, mozilla-ai#1292, mozilla-ai#1308, mozilla-ai#1309, mozilla-ai#1310, mozilla-ai#1317, mozilla-ai#1318, mozilla-ai#1320,
mozilla-ai#1325 and mozilla-ai#1352 in their final form, so the fork's own copies are dropped in
favour of upstream's. The tree is exactly upstream main plus the two fixes
still open there: gemini reasoning_effort="none" (mozilla-ai#1294) and closing the
provider stream when the wrapped stream closes (mozilla-ai#1319).

Claude-Session: https://claude.ai/code/session_01MmJSSofg7Lk7nBKZZyKV7w
@JamMaster1999
JamMaster1999 deleted the fix/replay-assistant-text-with-tool-calls branch September 3, 2026 20:13
HareeshBahuleyan added a commit that referenced this pull request Sep 14, 2026
…#1393)

## Description

Sibling gap from #1325, which fixed this exact shape in Gemini and
Anthropic and left Bedrock with it.

`_convert_response` collects every `text` content block into
`content_parts`, then in the `stopReason == "tool_use"` branch builds
the message with `content=None` and discards all of it. Converse returns
the model's own text as a separate text block in the same message as the
`toolUse` block, so a turn like "I will look up the weather in Paris."
followed by a `get_weather` call reached the caller as a bare tool call
with no text.

Three things keep it quiet rather than loud:

1. The streaming converter already forwards those deltas
(`_create_openai_chunk_from_aws_chunk` sets `content = delta["text"]`),
so streaming and non-streaming disagreed about the same response.
2. `_convert_message_to_bedrock_format` already replays an assistant
turn as a text block followed by `toolUse` blocks, so the request
direction was fine. There was simply never any text to replay, because
the response direction had dropped it.
3. There is no API error. The model just cannot see that it already
announced its plan, so on the next turn it announces it again, which is
the failure #1325 describes.

The non-tool branch of the same function joins `content_parts`
correctly. Only the tool branch threw them away.

`"".join(content_parts) or None` preserves the existing contract for a
bare tool call: joining an empty list yields `""`, and reporting an
empty string where the model genuinely said nothing would be a behaviour
change of its own. This matches Gemini, whose `text_content` stays
`None` until a text part appears.

I swept the rest of the provider family for the same pattern while I was
in here. `grep -rn 'content=None' src/any_llm/providers/*/utils.py
src/any_llm/providers/*/base.py` returns this one site and nothing else,
so Bedrock was the last one.

### Tests

3 added. The first two fail against unpatched sources with:

```
assert None == 'I will look up the weather in Paris.'
```

The third covers a tool call carrying no text and passes both before and
after, deliberately: it is the guard that the `or None` did not turn
every bare tool call into an empty string.

## PR Type

- 🐛 Bug Fix

## Relevant issues

No open issue; found by sweeping the provider family for the pattern
#1325 fixed.

## Checklist

- [x] I understand the code I am submitting.
- [x] I have added unit tests that prove my fix/feature works
- [x] I have run this code locally and verified it fixes the issue.
- [x] New and existing tests pass locally
- [x] Documentation was updated where necessary
- [x] I have read and followed the [contribution
guidelines](https://github.com/mozilla-ai/any-llm/blob/main/CONTRIBUTING.md)
- [x] **AI Usage:**
    - [ ] No AI was used.
    - [x] AI was used for drafting/refactoring.
    - [ ] This is fully AI-generated.

## AI Usage Information

- AI Model used: Opus 5
- AI Developer Tool used: Claude Code
- Any other info you'd like to share: `tests/unit` 2542 passed, 66
skipped; pre-commit clean.

When answering questions by the reviewer, please respond yourself, do
not copy/paste the reviewer comments into an AI system and paste back
its answer. We want to discuss with you, not your AI :)

- [ ] I am an AI Agent filling out this form (check box if true)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Non-streaming Bedrock responses now preserve model-generated text when
it appears alongside a tool call.
* Multiple text sections are combined correctly, while tool calls
without accompanying text continue to show no message content.

* **Tests**
* Added coverage for text before tool calls, multiple text sections, and
tool calls without text.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Hareesh <hareeshbahuleyan@gmail.com>

This branch was previously deployed

1 inactive deployment
integration-tests — c44bbda2 Deployed Sep 1, 2026 by javiermtorres via run-docs-tests #2746
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.27.0 Included in release 1.27.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants