Skip to content

Commit 7cedaef

Browse files
fix(anthropic): echo thinking-bearing assistant turns verbatim in tool loops
With adaptive thinking, the Messages API rejects tool-loop follow-ups whose latest assistant message differs from the original response (thinking signatures bind block positions; text/tool_use siblings were rebuilt and thinking blocks hoisted). The signature payload now carries the full native content array when thinking blocks are present — both parse paths — and _init_request re-emits it unchanged. Thinking-only signature payloads (from older versions) keep the rebuild path.
1 parent 8dbbef4 commit 7cedaef

2 files changed

Lines changed: 15 additions & 19 deletions

File tree

src/celeste/modalities/text/providers/anthropic/client.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ def _aggregate_signature(
9292
) -> list[dict[str, Any]]:
9393
"""Return the full native content array when the turn carries thinking blocks."""
9494
blocks = self._aggregate_content_blocks()
95-
if any(b.get("type") in {"thinking", "redacted_thinking"} for b in blocks):
96-
return blocks
97-
return []
95+
has_thinking = any(
96+
b.get("type") in {"thinking", "redacted_thinking"} for b in blocks
97+
)
98+
return blocks if has_thinking else []
9899

99100
def _aggregate_grounding(
100101
self, chunks: list[TextChunk], raw_events: list[dict[str, Any]]
@@ -272,17 +273,14 @@ def _parse_reasoning(
272273
) -> tuple[str | None, list[dict[str, Any]]]:
273274
"""Parse thinking from Anthropic response (signature = full content array)."""
274275
blocks = response_data.get("content", [])
275-
reasoning_parts: list[str] = []
276-
has_thinking = False
277-
for block in blocks:
278-
block_type = block.get("type")
279-
if block_type == "thinking":
280-
has_thinking = True
281-
thinking_text = block.get("thinking", "")
282-
if thinking_text:
283-
reasoning_parts.append(thinking_text)
284-
elif block_type == "redacted_thinking":
285-
has_thinking = True
276+
reasoning_parts = [
277+
b["thinking"]
278+
for b in blocks
279+
if b.get("type") == "thinking" and b.get("thinking")
280+
]
281+
has_thinking = any(
282+
b.get("type") in {"thinking", "redacted_thinking"} for b in blocks
283+
)
286284
text = "\n".join(reasoning_parts) if reasoning_parts else None
287285
return text, list(blocks) if has_thinking else []
288286

src/celeste/providers/anthropic/messages/streaming.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@ def _parse_chunk(self, event_data: dict[str, Any]) -> Any: # noqa: ANN401
6161
if delta_type == "input_json_delta":
6262
if block and block.get("type") in {"server_tool_use", "tool_use"}:
6363
block["input_json"] += delta.get("partial_json", "")
64-
elif delta_type == "thinking_delta":
64+
elif delta_type in {"thinking_delta", "signature_delta"}:
65+
key = delta_type.removesuffix("_delta")
6566
if block and block.get("type") == "thinking":
66-
block["thinking"] += delta.get("thinking", "")
67-
elif delta_type == "signature_delta":
68-
if block and block.get("type") == "thinking":
69-
block["signature"] += delta.get("signature", "")
67+
block[key] += delta.get(key, "")
7068
elif delta_type in {"text_delta", "citations_delta"}:
7169
block = self._content_blocks.setdefault(
7270
idx, {"type": "text", "text": "", "citations": []}

0 commit comments

Comments
 (0)