Documentation: N/A — backend logic fix, no new user-facing API or config.
Dependencies: N/A — no new dependencies.
Testing: Logic validated by code review; issue root cause confirmed.
Agentic AI Code: Human reviewed and manually verified.
Code review: Performed self-review.
Design & Architecture: Minimal, targeted fix; no new settings or state introduced.
Git Hygiene: Single atomic commit.
Title Prefix:fix
Description
When resuming a chat that previously triggered tool calls, the conversation can fail with a 400 error from strict LLM providers (AWS Bedrock, Anthropic). The root cause is in convert_output_to_messages() in misc.py:
output[] traversal:
'function_call' → accumulates in pending_tool_calls
'function_call_output' → flush_pending() → appends tool role message,
clears pending_tool_calls
...
final flush_pending() → if no function_call_output was ever encountered,
emits an assistant message with tool_calls but no
trailing tool role message
Strict providers enforce that every tool_use id has a matching tool_result block immediately after. If function_call_output is missing from stored history (e.g., KB state changed between turns), the orphaned tool_calls cause:
ValidationException: messages.N: tool_use ids were found without tool_result blocks
immediately after: tooluse_xxx, tooluse_yyy.
Each tool_use block must have a corresponding tool_result block in the next message.
Fix
After flush_pending() at the end of convert_output_to_messages, scan the produced messages and collect all tool_call_id values from tool role messages. Then walk the messages again and strip any tool_calls whose id has no match. If stripping leaves an assistant message with empty content and no tool_calls, drop it entirely. Well-formed sequences are unaffected since the check is a no-op when all ids match.
Root Cause
convert_output_to_messages assumes every function_call will be followed by a function_call_output before the function returns. When that assumption is violated (DB never wrote it, KB update cleared it, etc.), the final flush_pending() emits a malformed assistant message.
Validation
ruff format --check backend/open_webui/utils/misc.py
ruff check backend/open_webui/utils/misc.py
Changelog Entry
Fixed
convert_output_to_messages now strips orphan tool_calls that lack a matching tool role message, preventing 400 errors from strict providers (AWS Bedrock, Anthropic) when chat history is resumed with missing function_call_output entries.
Screenshots or Videos
Not applicable; backend logic fix, no UI change.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/open-webui/open-webui/pull/24834
**Author:** [@kindredzhang](https://github.com/kindredzhang)
**Created:** 5/17/2026
**Status:** ❌ Closed
**Base:** `main` ← **Head:** `fix/24758-orphan-tool-calls`
---
### 📝 Commits (10+)
- [`18ff358`](https://github.com/open-webui/open-webui/commit/18ff358c15f4fa7986be48e2282ba437342b48ea) refac
- [`90a02e6`](https://github.com/open-webui/open-webui/commit/90a02e623f05027dec3ae28a394b256497b9d70d) refac
- [`070ab26`](https://github.com/open-webui/open-webui/commit/070ab2650128b6af03d72be29f90f78cbf3584bc) refac: reorganize scripts and ci workflows
- [`9c18e70`](https://github.com/open-webui/open-webui/commit/9c18e707ce764ed64655d7681f5b5be0f466885d) chore: remove unused cypress tests
- [`4527c74`](https://github.com/open-webui/open-webui/commit/4527c747b42d823b363f357c565c8bcafcf45cb7) chore: remove unused legacy test suite
- [`2c2d06c`](https://github.com/open-webui/open-webui/commit/2c2d06c31b463a87d4ece2a4eb4c1d2f73cd3980) refac: deprecate peewee migration layer
- [`c79ff81`](https://github.com/open-webui/open-webui/commit/c79ff81f551f37391e27a6219c25112dccb0f68b) refac: remove dead peewee connection wrappers
- [`1413ce4`](https://github.com/open-webui/open-webui/commit/1413ce4a5260f600b0602e730212a487b680cd82) refac
- [`4cee7c2`](https://github.com/open-webui/open-webui/commit/4cee7c29a5909e87ab420c459680084ed353f9e7) refac
- [`83736e8`](https://github.com/open-webui/open-webui/commit/83736e8dbd9956b67108ca6983405bb2d17756e5) style: ruff format
### 📊 Changes
**297 files changed** (+16375 additions, -101553 deletions)
<details>
<summary>View changed files</summary>
📝 `.github/ISSUE_TEMPLATE/feature_request.yaml` (+34 -40)
📝 `.github/dependabot.yml` (+10 -5)
📝 `.github/pull_request_template.md` (+33 -34)
➕ `.github/workflows/backend.yaml` (+40 -0)
➖ `.github/workflows/build-release.yml` (+0 -61)
➖ `.github/workflows/docker-build.yaml` (+0 -917)
➕ `.github/workflows/docker.yaml` (+311 -0)
➖ `.github/workflows/format-backend.yaml` (+0 -46)
➖ `.github/workflows/format-build-frontend.yaml` (+0 -65)
➕ `.github/workflows/frontend.yaml` (+63 -0)
➖ `.github/workflows/integration-test.disabled` (+0 -255)
➕ `.github/workflows/release.yml` (+69 -0)
📝 `backend/open_webui/config.py` (+3001 -3236)
📝 `backend/open_webui/constants.py` (+5 -3)
📝 `backend/open_webui/env.py` (+408 -472)
📝 `backend/open_webui/functions.py` (+15 -21)
➕ `backend/open_webui/internal/config.py` (+258 -0)
📝 `backend/open_webui/internal/db.py` (+19 -54)
➖ `backend/open_webui/internal/migrations/001_initial_schema.py` (+0 -253)
➖ `backend/open_webui/internal/migrations/002_add_local_sharing.py` (+0 -45)
_...and 80 more files_
</details>
### 📄 Description
# Pull Request Checklist
- [x] **Linked Issue/Discussion:** `Closes #24758`
- [x] **Target branch:** `dev`
- [x] **Description:** Concise description down below.
- [x] **Changelog:** Added at the bottom.
- [x] **Documentation:** N/A — backend logic fix, no new user-facing API or config.
- [x] **Dependencies:** N/A — no new dependencies.
- [x] **Testing:** Logic validated by code review; issue root cause confirmed.
- [x] **Agentic AI Code:** Human reviewed and manually verified.
- [x] **Code review:** Performed self-review.
- [x] **Design & Architecture:** Minimal, targeted fix; no new settings or state introduced.
- [x] **Git Hygiene:** Single atomic commit.
- [x] **Title Prefix:** `fix`
---
## Description
When resuming a chat that previously triggered tool calls, the conversation can fail with a 400 error from strict LLM providers (AWS Bedrock, Anthropic). The root cause is in `convert_output_to_messages()` in `misc.py`:
```
output[] traversal:
'function_call' → accumulates in pending_tool_calls
'function_call_output' → flush_pending() → appends tool role message,
clears pending_tool_calls
...
final flush_pending() → if no function_call_output was ever encountered,
emits an assistant message with tool_calls but no
trailing tool role message
```
Strict providers enforce that every `tool_use` id has a matching `tool_result` block immediately after. If `function_call_output` is missing from stored history (e.g., KB state changed between turns), the orphaned `tool_calls` cause:
```
ValidationException: messages.N: tool_use ids were found without tool_result blocks
immediately after: tooluse_xxx, tooluse_yyy.
Each tool_use block must have a corresponding tool_result block in the next message.
```
### Fix
After `flush_pending()` at the end of `convert_output_to_messages`, scan the produced messages and collect all `tool_call_id` values from tool role messages. Then walk the messages again and strip any `tool_calls` whose id has no match. If stripping leaves an assistant message with empty content and no `tool_calls`, drop it entirely. Well-formed sequences are unaffected since the check is a no-op when all ids match.
### Root Cause
`convert_output_to_messages` assumes every `function_call` will be followed by a `function_call_output` before the function returns. When that assumption is violated (DB never wrote it, KB update cleared it, etc.), the final `flush_pending()` emits a malformed assistant message.
### Validation
```bash
ruff format --check backend/open_webui/utils/misc.py
ruff check backend/open_webui/utils/misc.py
```
---
## Changelog Entry
### Fixed
- `convert_output_to_messages` now strips orphan `tool_calls` that lack a matching `tool` role message, preventing 400 errors from strict providers (AWS Bedrock, Anthropic) when chat history is resumed with missing `function_call_output` entries.
---
## Screenshots or Videos
Not applicable; backend logic fix, no UI change.
---
## Contributor License Agreement
- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24834
Author: @kindredzhang
Created: 5/17/2026
Status: ❌ Closed
Base:
main← Head:fix/24758-orphan-tool-calls📝 Commits (10+)
18ff358refac90a02e6refac070ab26refac: reorganize scripts and ci workflows9c18e70chore: remove unused cypress tests4527c74chore: remove unused legacy test suite2c2d06crefac: deprecate peewee migration layerc79ff81refac: remove dead peewee connection wrappers1413ce4refac4cee7c2refac83736e8style: ruff format📊 Changes
297 files changed (+16375 additions, -101553 deletions)
View changed files
📝
.github/ISSUE_TEMPLATE/feature_request.yaml(+34 -40)📝
.github/dependabot.yml(+10 -5)📝
.github/pull_request_template.md(+33 -34)➕
.github/workflows/backend.yaml(+40 -0)➖
.github/workflows/build-release.yml(+0 -61)➖
.github/workflows/docker-build.yaml(+0 -917)➕
.github/workflows/docker.yaml(+311 -0)➖
.github/workflows/format-backend.yaml(+0 -46)➖
.github/workflows/format-build-frontend.yaml(+0 -65)➕
.github/workflows/frontend.yaml(+63 -0)➖
.github/workflows/integration-test.disabled(+0 -255)➕
.github/workflows/release.yml(+69 -0)📝
backend/open_webui/config.py(+3001 -3236)📝
backend/open_webui/constants.py(+5 -3)📝
backend/open_webui/env.py(+408 -472)📝
backend/open_webui/functions.py(+15 -21)➕
backend/open_webui/internal/config.py(+258 -0)📝
backend/open_webui/internal/db.py(+19 -54)➖
backend/open_webui/internal/migrations/001_initial_schema.py(+0 -253)➖
backend/open_webui/internal/migrations/002_add_local_sharing.py(+0 -45)...and 80 more files
📄 Description
Pull Request Checklist
Closes #24758devfixDescription
When resuming a chat that previously triggered tool calls, the conversation can fail with a 400 error from strict LLM providers (AWS Bedrock, Anthropic). The root cause is in
convert_output_to_messages()inmisc.py:Strict providers enforce that every
tool_useid has a matchingtool_resultblock immediately after. Iffunction_call_outputis missing from stored history (e.g., KB state changed between turns), the orphanedtool_callscause:Fix
After
flush_pending()at the end ofconvert_output_to_messages, scan the produced messages and collect alltool_call_idvalues from tool role messages. Then walk the messages again and strip anytool_callswhose id has no match. If stripping leaves an assistant message with empty content and notool_calls, drop it entirely. Well-formed sequences are unaffected since the check is a no-op when all ids match.Root Cause
convert_output_to_messagesassumes everyfunction_callwill be followed by afunction_call_outputbefore the function returns. When that assumption is violated (DB never wrote it, KB update cleared it, etc.), the finalflush_pending()emits a malformed assistant message.Validation
Changelog Entry
Fixed
convert_output_to_messagesnow strips orphantool_callsthat lack a matchingtoolrole message, preventing 400 errors from strict providers (AWS Bedrock, Anthropic) when chat history is resumed with missingfunction_call_outputentries.Screenshots or Videos
Not applicable; backend logic fix, no UI change.
Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.