In this article
Choice completion, final usage and HTTP closure are separate events. A state machine must relate them without treating accounting repetition as another answer.
Treating finish_reason as a label on the last chunk is insufficient for provider testing. One generation passes several states: accepted request, opened stream, received data, completed choice, final accounting and closed transport. Mixing these events can classify truncation as success or count completion twice. An explicit state model makes such mistakes observable.
Define the state unit
Request state and individual-choice state do not always coincide. A request can contain several independent results where the interface supports that capability. Scope each record by request ID and choice index. Do not infer completion of the entire operation from the first non-null finish_reason.
OpenRouter documents normalized reasons, including stop, length, tool_calls, content_filter and error, alongside native_finish_reason. The normalized value supports client logic while the native value preserves implementation evidence. Do not substitute one for the other arbitrarily when investigating differences.
Define allowed transitions
A minimal state machine distinguishes unfinished work, successful terminal outcomes and failures. Receiving a data event does not establish success. Store generation termination reason separately from completeness of envelope delivery. A length outcome can then represent a legitimate output limit while a truncated delivery after content remains incomplete.
A transition table contains incoming event, previous state, next state and observer action. Preserve unknown reasons for investigation. Silently converting them to stop hides contract changes and makes regression tests too permissive.
Account for the accounting frame
The current OpenRouter streaming documentation describes a final usage chunk before DONE. On Chat Completions it contains a nonempty choices array and repeats the terminal reason to accommodate clients expecting an array element. This is a platform-facing interface detail rather than a universal requirement for upstream servers.
A rule declaring every repeated terminal reason erroneous is therefore too broad. Recognize the accounting frame without creating another completed answer. New content after final accounting, however, needs explicit examination rather than automatic appending to a closed result.
Construct minimal traces
Prepare short synthetic sequences for ordinary stop, length with no visible text, tool_calls and an incomplete stream without its terminal marker. Save expected classification and completed-result count for each. Keep fixtures small enough to locate an incorrect transition at one event.
Rejecting an unsupported capability before streaming is a different branch. Do not merge it with a reason emitted inside generation. A complete report exposes HTTP outcome, application outcome and final-usage presence independently.
Limit conclusions to the tested contract
The machine validates event order for one interface version, not prose quality or future availability. Revisit transitions when changing APIs: another endpoint can have different terminal events. A successful Chat Completions test must not silently apply to Responses.
Related reading covers errors inside a stream and usage reconciliation. Keep them distinct: one explains failures, another counters, while the state machine connects both observations without inventing a universal protocol.