Repository navigation
Conversation
…ges on openai-compatible @ai-sdk/openai-compatible serializes assistant messages without any non-empty text as content: "" or null — tool-call-only turns, the per-step pieces produced by step-start splitting, and aborted reasoning-only turns all hit this. Strict providers (Kimi/Moonshot via chat/completions) reject the whole request with 400 "the message at position N with role 'assistant' must not be empty", permanently bricking the session as each failed retry appends another empty artifact message. Run a post-pass over the final ModelMessage list in toModelMessagesEffect when the target model uses @ai-sdk/openai-compatible and unshift a single-space text part into any assistant message lacking non-empty text, so every split piece is covered individually. Scoped to openai-compatible: OpenAI and Anthropic accept textless tool-call turns, and Anthropic signed thinking is handled before conversion. Fixes anomalyco#37946 Tests: 43/43 in test/session/message-v2.test.ts (7 new: tool-call-only, step-start split pieces, empty-text-only, reasoning-only passthrough, anthropic unaffected, step-start-only still dropped, existing text untouched)
|
The following comment was made by an LLM, it may be inaccurate: I found a related PR that's worth reviewing: PR #33899 - This PR appears to address a similar issue of ensuring non-empty assistant content for the openai-compatible provider. It may have already tackled part of this problem or provide relevant context for the current PR. The other results are either the current PR itself (#38270) or tangentially related PRs about empty content, reasoning parts, and provider fixes. You should verify whether PR #33899 already solves the issue or if this PR (#38270) is extending/improving upon that previous fix. |
|
Verified the overlap with #33899 — thanks for surfacing it. Short version: #33899 does not fix #37946; this PR covers a superset of #33899's cases. #33899 (still open, unmerged) patches #37946's failing shape is exactly that untouched case: an aborted turn persisted with only Coverage comparison:
Layer note: this PR runs the guard in |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
|
This remains an active fix for the still-open #37946. I rebased the branch onto the current |
Issue for this PR
Fixes #37946
Related: #37651 / #37624 (empty reasoning parts — complementary; that fix skips empty reasoning parts, while this PR covers assistant wire messages that still end up with no non-empty text at all, e.g. aborted reasoning-only turns and step-start split pieces). Also related: #31046 (empty text parts), #28507, #37852, #37372.
Type of change
What does this PR do?
@ai-sdk/openai-compatibleserializes assistant messages without any non-empty text ascontent: ""ornull. Strict providers (Kimi/Moonshot via chat/completions) reject the whole request with400 "the message at position N with role 'assistant' must not be empty". Once a session contains such a turn it is permanently bricked: every retry fails the same way and appends another empty artifact message.Shapes that produce an empty assistant wire message:
step-start/reasoning/patchparts (Aborted assistant turn persisted with zero content bricks the session: empty assistant message replayed to provider (400 "must not be empty") #37946)convertToModelMessagessplits a multi-step turn atstep-start(a guard applied only at the UIMessage stage covers the first piece but not the later ones)This PR runs a post-pass over the final
ModelMessagelist intoModelMessagesEffect: when the target model uses@ai-sdk/openai-compatible, any assistant message lacking non-empty text gets a single-space text part unshifted, so every split piece is covered individually.Why inject instead of dropping the empty message: dropping an assistant turn that carries tool calls would break tool-call/tool-result pairing, and dropping a reasoning-only turn would discard interleaved
reasoning_contentthat providers like Kimi use as history context. A single-space placeholder keeps history semantics intact while satisfying the non-empty-content requirement.Scoped to openai-compatible on purpose: OpenAI and Anthropic accept textless tool-call turns, and Anthropic signed thinking is handled before conversion.
How did you verify your code works?
test/session/message-v2.test.ts(tool-call-only, step-start split pieces, empty-text-only, reasoning-only passthrough, anthropic unaffected, step-start-only still dropped, existing text untouched): 43/43 pass.tsgo --noEmitclean.Screenshots / recordings
Not applicable.
Checklist
Prepared with AI assistance (OpenCode agent); reviewed and verified by the author before submission.