Problem
LLM responses are deserialized with unwrap_or("") and never validated:
- Anthropic:
crates/llm/src/lib.rs:210 — body["content"][0]["text"].as_str().unwrap_or("")
- OpenAI-compatible:
crates/llm/src/lib.rs:298 — body["choices"][0]["message"]["content"].as_str().unwrap_or("")
A truncated/empty/aborted response (connection died mid-turn, upstream stream ended without a finish frame, API returned an unexpected shape) is persisted as a successful assistant message and the prompt returns status: "completed" (crates/server/src/handler/session.rs:271-279).
This mirrors upstream anomalyco/opencode#40061 (closes #39968): when an SSE connection dies without a finish frame, the AI SDK synthesizes finishReason "other"/undefined which opencode mapped to benign "unknown", so truncated output was persisted as success and opencode run exited 0. Upstream now maps "other"/undefined → "error" and fails the turn while keeping partial output.
Impact
- Silent data loss: user believes the model answered; content is empty/partial with no signal.
- Breaks automation/exit-code semantics ("run completed" when it didn't).
Spec / Acceptance criteria
- Provider
complete() validates the response shape before returning Ok:
- Anthropic: require
content array with a text block; empty content → Err("empty completion").
- OpenAI-compatible: require
choices[0].message.content non-empty; also honor choices[0].finish_reason — map length/content_filter/missing to an error (or explicit flag).
- Session handler distinguishes "completed" vs "error": on
Err, respond status: "error" with the message (keep any partial content in the session as-is, but do not mark the turn completed).
- Test: stub a provider returning
{"choices":[{"message":{"content":""},"finish_reason":"stop"}]} → prompt fails with a clear error, not "completed".
Reference: upstream anomalyco/opencode#40061 (closes #39968); our code: crates/llm/src/lib.rs:209-215, 298-305, crates/server/src/handler/session.rs:269-291.
Problem
LLM responses are deserialized with
unwrap_or("")and never validated:crates/llm/src/lib.rs:210—body["content"][0]["text"].as_str().unwrap_or("")crates/llm/src/lib.rs:298—body["choices"][0]["message"]["content"].as_str().unwrap_or("")A truncated/empty/aborted response (connection died mid-turn, upstream stream ended without a finish frame, API returned an unexpected shape) is persisted as a successful assistant message and the prompt returns
status: "completed"(crates/server/src/handler/session.rs:271-279).This mirrors upstream anomalyco/opencode#40061 (closes #39968): when an SSE connection dies without a finish frame, the AI SDK synthesizes finishReason "other"/undefined which opencode mapped to benign "unknown", so truncated output was persisted as success and
opencode runexited 0. Upstream now maps "other"/undefined → "error" and fails the turn while keeping partial output.Impact
Spec / Acceptance criteria
complete()validates the response shape before returningOk:contentarray with atextblock; empty content →Err("empty completion").choices[0].message.contentnon-empty; also honorchoices[0].finish_reason— maplength/content_filter/missing to an error (or explicit flag).Err, respondstatus: "error"with the message (keep any partial content in the session as-is, but do not mark the turn completed).{"choices":[{"message":{"content":""},"finish_reason":"stop"}]}→ prompt fails with a clear error, not "completed".Reference: upstream anomalyco/opencode#40061 (closes #39968); our code:
crates/llm/src/lib.rs:209-215, 298-305,crates/server/src/handler/session.rs:269-291.