* fix(stream): synthesize terminal finish_reason chunk when upstream omits it (#7800)
Some providers close the SSE stream without emitting a final chunk
carrying a non-null finish_reason. The OpenAI spec requires the terminal
chunk to include finish_reason (e.g. "stop"); strict clients (pi CLI)
reject the stream with "Stream ended without finish_reason".
In passthrough mode (OpenAI Chat Completions shape), track whether a
chunk with non-null finish_reason was seen. If not, synthesize a
terminal chunk with finish_reason "stop" (or "tool_calls" when tool
calls were present) before emitting [DONE].
Translate mode is unaffected — translators (claude-to-openai, gemini-
to-openai) already guarantee a terminal finish_reason chunk via
finishReasonSent / fallback defaults.
Tests: 3 new regression tests covering omission, no-op when upstream
sends finish_reason, and tool_calls variant.
* fix(stream): track finish_reason in flush path to avoid false synthesis (#7800)
The synthetic finish_reason chunk fired even when the upstream DID emit
a finish_reason chunk — but as the final buffered line without a trailing
newline. The flush path (which handles the leftover buffer) normalized
IDs but never set passthroughSawFinishReason, so the synthesis guard
!passthroughSawFinishReason stayed true and emitted a spurious synthetic
chunk with a chatcmpl-* id, breaking the numeric-id normalization test.