openclaw - ✅(Solved) Fix meridian/anthropic-messages stream reader drops text content blocks when response is [thinking, text] [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74410Fetched 2026-04-30 06:24:12
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
2
Timeline (top)
cross-referenced ×2closed ×1commented ×1

When a model accessed via meridian (or anthropic direct) returns content in [thinking, text] shape via the anthropic-messages streaming API, the openclaw stream reader drops the text block entirely. The persisted assistant message ends up with content: [] and usage.output: 0, despite the upstream having streamed real output tokens.

This was the cost driver in a recent overnight incident — meridian (Claude Max) routed responses repeatedly mis-parsed as empty, triggering the empty-response → fallback cascade → ~$44 of anthropic cache_write thrash overnight.

Root Cause

When a model accessed via meridian (or anthropic direct) returns content in [thinking, text] shape via the anthropic-messages streaming API, the openclaw stream reader drops the text block entirely. The persisted assistant message ends up with content: [] and usage.output: 0, despite the upstream having streamed real output tokens.

This was the cost driver in a recent overnight incident — meridian (Claude Max) routed responses repeatedly mis-parsed as empty, triggering the empty-response → fallback cascade → ~$44 of anthropic cache_write thrash overnight.

Fix Action

Fixed

PR fix notes

PR #74476: fix(providers): guard anthropic-messages reader against empty-content/token-mismatch turns

Description (problem / solution / changelog)

Problem

When the anthropic-messages SSE stream reports output_tokens > 0 in message_delta but no content_block_start events were processed (stream format mismatch, or a proxy like meridian using a subtly different SSE format), the stream reader silently produced:

content: []
usage.output: 0
stopReason: "stop"

This empty-content turn triggered resolveEmptyResponseRetryInstruction → fallback cascade:

  1. Retry with meridian/anthropic (same reader, same empty result)
  2. Retry to codex (400s on the broken-payload JSONL state)
  3. ~$44 of cache_write thrash overnight (confirmed in issue #74410)

Fix

Add a guard between the stream loop and finalizeTransportStream:

if (output.usage.output > 0 && output.content.length === 0) {
  throw new Error(
    `anthropic-messages: ${output.usage.output} output token(s) reported but no content blocks parsed — stream format mismatch`,
  );
}

When triggered, failTransportStream sets stopReason: "error". The "error" stop reason short-circuits isEmptyResponseAssistantTurn (returns false at the stopReason === "error" guard), breaking the cascade.

Tests

Two new tests in src/agents/anthropic-transport-stream.test.ts:

  1. [thinking, text] shaped response (#74410 regression): verifies both the thinking block and text block are preserved in result.content with correct text/signature values and usage.output = 9.

  2. Guard test: verifies that when message_delta reports output_tokens: 9 but no content_block_start events were sent, the result has stopReason: "error" with a descriptive mismatch message.

Acceptance criteria

  • pnpm test src/agents/anthropic-transport-stream.test.ts — 44/44 pass
  • pnpm test src/agents/pi-embedded-runner/run.incomplete-turn.test.ts — 166/166 pass
  • npx oxlint --threads=1 src/agents/anthropic-transport-stream.ts src/agents/anthropic-transport-stream.test.ts — 0 warnings, 0 errors

Closes #74410

Changed files

  • CHANGELOG.md (modified, +156/-1)
  • extensions/telegram/src/action-runtime.ts (modified, +16/-5)
  • extensions/telegram/src/send.test.ts (modified, +63/-0)
  • extensions/telegram/src/send.ts (modified, +24/-2)
  • src/agents/anthropic-transport-stream.test.ts (modified, +71/-0)
  • src/agents/anthropic-transport-stream.ts (modified, +5/-0)

PR #74557: fix(agents): preserve seeded Anthropic text blocks

Description (problem / solution / changelog)

Summary

  • Problem: Anthropic-compatible anthropic-messages streams can seed text/thinking content on content_block_start; the transport only applied later deltas, so a [thinking, text] response could persist empty blocks.
  • Why it matters: Meridian/Anthropic responses with visible text could be treated as empty assistant turns, triggering empty-response fallback behavior despite upstream output.
  • What changed: Seeded text and thinking content is now copied into the mutable assistant output and emitted as matching stream deltas; signature_delta replaces the seeded thinking signature instead of concatenating it.
  • What did NOT change (scope boundary): No fallback policy, replay policy, provider selection, or non-Anthropic transport behavior changed.

AI-assisted: yes.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #74410
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: The Anthropic stream parser initialized text and thinking blocks with empty strings on content_block_start and only appended later content_block_delta payloads. Some compatible streams include non-empty content in the start block itself.
  • Missing detection / guardrail: There was no unit coverage for a thinking block followed by a text block where the visible text is seeded on content_block_start.
  • Contributing context (if known): Meridian and Anthropic-compatible Messages streams can produce [thinking, text] content where the start event carries the initial block payload.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/agents/anthropic-transport-stream.test.ts
  • Scenario the test should lock in: A Meridian-style Anthropic Messages SSE response starts a thinking block and then a text block with seeded NO_REPLY; the final assistant content and stream events preserve the text.
  • Why this is the smallest reliable guardrail: The bug is in the transport parser, so a mocked SSE unit test exercises the exact event shape without live Anthropic/Meridian credentials.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Anthropic-compatible streamed replies that include visible text in content_block_start are no longer treated as empty assistant turns.

Diagram (if applicable)

Before:
[thinking start with content] -> [text start with content] -> persisted empty blocks

After:
[thinking start with content] -> [text start with content] -> persisted thinking + text

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local Node/pnpm checkout
  • Model/provider: mocked meridian model with api: "anthropic-messages"
  • Integration/channel (if any): N/A
  • Relevant config (redacted): N/A

Steps

  1. Stream Anthropic Messages SSE events with content_block_start index 0 containing { type: "thinking", thinking: "checking", signature: "sig_1" }.
  2. Emit a signature_delta for index 0.
  3. Stream content_block_start index 1 containing { type: "text", text: "NO_REPLY" }.
  4. Complete the message with non-zero output_tokens.

Expected

  • The assistant message contains both the thinking block and { type: "text", text: "NO_REPLY" }.
  • The stream emits text_delta and text_end for NO_REPLY.
  • The thinking signature is the final signature_delta value.

Actual

  • Before this patch, the seeded start-block content was ignored and the assistant content could persist as empty blocks.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: targeted Anthropic transport stream unit test; changed-gate for core/coreTests; local Codex review.
  • Edge cases checked: seeded text after thinking; seeded thinking with later signature_delta replacement.
  • What you did not verify: Live Anthropic or Meridian API traffic.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Some providers may emit both seeded start content and later deltas for the same text/thinking block.
    • Mitigation: The transport appends later deltas after the seeded content, matching Anthropic SDK snapshot behavior for text/thinking accumulation; signature deltas replace the seeded signature.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/anthropic-transport-stream.test.ts (modified, +76/-0)
  • src/agents/anthropic-transport-stream.ts (modified, +33/-6)
RAW_BUFFERClick to expand / collapse

Summary

When a model accessed via meridian (or anthropic direct) returns content in [thinking, text] shape via the anthropic-messages streaming API, the openclaw stream reader drops the text block entirely. The persisted assistant message ends up with content: [] and usage.output: 0, despite the upstream having streamed real output tokens.

This was the cost driver in a recent overnight incident — meridian (Claude Max) routed responses repeatedly mis-parsed as empty, triggering the empty-response → fallback cascade → ~$44 of anthropic cache_write thrash overnight.

Concrete repro / evidence

Same conversation, same heartbeat ping, two different providers, same response shape upstream:

providerpersisted contentpersisted usage.output
meridian/claude-opus-4-7[]0
openai-codex/gpt-5.5[{type:"thinking",encrypted:"…"},{type:"text",text:"NO_REPLY"}]65–117

Meanwhile meridian's own proxy log for the meridian rows shows usage: input=6 output=9 cache_read=128k cache=99% — i.e. the model produced real output tokens, but openclaw stored content: [].

The codex/openai-responses API path preserves both the thinking block and the text block. The anthropic-messages path drops the text block.

Why it cascades

get-reply.ts sees payloadCount === 0, treats the turn as empty, retries with "visible-answer continuation" (which injects an empty user message into the JSONL), still empty, falls back to anthropic (same reader → same drop), then to codex (which 400s on the broken-payload state) — surfacing One of "input" or "previous_response_id" or 'prompt' or 'conversation_id' must be provided.

Likely related

  • #71891 (closed) — vLLM/Nemotron response stored as thinking-only; assistantTexts empty despite successful completion. Same shape of bug; that fix landed for the vLLM/Nemotron adapter but seemingly didn't cover the anthropic-messages path used by meridian and anthropic providers.

Environment

  • openclaw 2026.4.26 (be8c246)
  • meridian 1.40.0 (@rynfar/meridian)
  • Adapter: meridian plugin pointing at http://127.0.0.1:3456 with api: "anthropic-messages"
  • Affects both provider=meridian and provider=anthropic (both go through the same anthropic-messages reader)

extent analysis

TL;DR

The openclaw stream reader is dropping the text block from the response when using the anthropic-messages streaming API with meridian or anthropic providers, resulting in empty persisted content.

Guidance

  • Review the get-reply.ts file to understand how it handles empty responses and retries, as this may be contributing to the cascade of failures.
  • Investigate the anthropic-messages API path to determine why it is dropping the text block, and compare it to the codex/openai-responses API path which preserves both blocks.
  • Check the adapter configuration for the meridian plugin to ensure it is correctly set up to handle the anthropic-messages API.
  • Consider applying the fix from #71891 to the anthropic-messages path to see if it resolves the issue.

Example

No code snippet is provided as the issue does not include specific code that can be modified to fix the problem.

Notes

The issue seems to be specific to the anthropic-messages API path and the meridian and anthropic providers. The fix from #71891 may not have been applied to this specific path, which could be the cause of the problem.

Recommendation

Apply the workaround by modifying the get-reply.ts file to handle empty responses differently, or apply the fix from #71891 to the anthropic-messages path, as this is likely to resolve the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix meridian/anthropic-messages stream reader drops text content blocks when response is [thinking, text] [2 pull requests, 1 comments, 2 participants]