modelplease/response.rs
1// Copyright 2026 Thomas Santerre and Moderately AI Inc.
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Language model response types.
6
7/// Normalized reason the model stopped generating.
8///
9/// Maps the per-provider stop-reason strings into one shared enum so
10/// downstream consumers can distinguish
11/// "ran out of room" (`MaxTokens`) from "finished naturally" (`EndTurn`)
12/// without knowing which provider produced the response. Without this,
13/// a truncated completion might otherwise surface as a parse error with no
14/// hint that the model ran out of `max_tokens`.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum StopReason {
17 /// Model finished generating naturally. Anthropic `end_turn`,
18 /// OpenAI `stop`, Bedrock `end_turn`.
19 EndTurn,
20 /// Generation hit the `max_tokens` budget and was truncated.
21 /// Anthropic `max_tokens`, OpenAI `length`, Bedrock `max_tokens`.
22 MaxTokens,
23 /// A configured stop sequence appeared in the output. Anthropic
24 /// `stop_sequence`, OpenAI `stop` with a sequence match, Bedrock
25 /// `stop_sequence`.
26 StopSequence,
27 /// Model emitted a tool-use block as its terminal action. Anthropic
28 /// `tool_use`, Bedrock `tool_use`. OpenAI uses `tool_calls`.
29 ToolUse,
30 /// Provider-side safety / content filter aborted generation.
31 /// OpenAI `content_filter`, Bedrock `content_filtered` or
32 /// `guardrail_intervened`.
33 ContentFilter,
34 /// An upstream-specific stop-reason value we don't normalize.
35 /// Carries the raw string so logs and downstream parsing can still
36 /// see what the provider sent.
37 Other(String),
38}
39
40/// A response from a language model generation call.
41#[derive(Debug, Clone, Default)]
42pub struct LanguageModelResponse {
43 /// The generated text content.
44 pub content: String,
45 /// Extended-thinking reasoning blocks, concatenated in order.
46 ///
47 /// Populated for Anthropic Claude 4.x models that emit "thinking"
48 /// content blocks ahead of the final text. Empty when the provider
49 /// doesn't produce thinking traces. Always billable even when empty
50 /// — the token count for thinking lives in `usage.output_tokens`.
51 pub thinking: Option<String>,
52 /// Token usage statistics, if available.
53 pub usage: Option<Usage>,
54 /// The model that generated this response, if reported.
55 pub model: Option<String>,
56 /// Normalized reason generation stopped. `None` only when the
57 /// provider didn't include one on the response (older OpenAI-compat
58 /// servers, in-progress stream snapshots). Production providers
59 /// always populate this; downstream code that needs to distinguish
60 /// truncation from natural completion treats `None` as "unknown,
61 /// fall through to existing parse logic".
62 pub stop_reason: Option<StopReason>,
63}
64
65/// An incremental chunk from a streaming language model response.
66#[derive(Debug, Clone)]
67pub struct StreamDelta {
68 /// Incremental text content (may be empty on non-content events).
69 pub content: String,
70 /// Extended-thinking / reasoning text emitted on this chunk. Streamed
71 /// alongside (or before) `content` on providers that expose
72 /// chain-of-thought as a distinct content block:
73 ///
74 /// - **Bedrock Converse:** populated from `ContentBlockDelta::Reasoning` events (gpt-oss,
75 /// DeepSeek R1, Magistral, MiniMax M2, Nemotron-super on the Runtime endpoint).
76 ///
77 /// `None` on the OpenAI Chat Completions surface (reasoning isn't
78 /// streamed per OpenAI spec — it shows up only in
79 /// `completion_tokens_details.reasoning_tokens` accounting), and on
80 /// adapters that haven't been extended to surface reasoning yet
81 /// (Anthropic thinking blocks, Mantle Responses reasoning events).
82 pub thinking: Option<String>,
83 /// Token usage statistics. Presence and timing depend on the provider:
84 ///
85 /// - **Anthropic:** populated on `message_delta` events (mid-stream, sometimes on multiple
86 /// events as token counts evolve). The final `message_stop` event carries `None`.
87 /// - **OpenAI / Ollama:** populated only on the final pre-`[DONE]` chunk, because we set
88 /// `stream_options.include_usage = true` on the request. Without that flag the field would
89 /// be `None` for every chunk.
90 /// - **Default `generate_stream` impl** (non-streaming providers wrapped into a one-item
91 /// stream): populated on the single delta which is also `is_final`.
92 ///
93 /// Consumers that need cost tracking should accumulate or take the
94 /// last non-`None` value rather than expect `usage` on every chunk.
95 pub usage: Option<Usage>,
96 /// The model name — typically only present on the first chunk.
97 pub model: Option<String>,
98 /// Normalized reason generation stopped — populated on whichever
99 /// chunk carries the wire-level stop signal:
100 ///
101 /// - **OpenAI Chat Completions:** the final content chunk with a non-null `finish_reason`.
102 /// - **Anthropic Messages:** the `message_delta` event (which also carries final usage).
103 /// - **Bedrock Converse:** the `MessageStop` event (precedes the terminating `Metadata` chunk
104 /// that carries usage).
105 /// - **Mantle Responses:** the `response.completed` event (also carries usage).
106 /// `response.incomplete` events with recoverable reasons (`max_output_tokens`,
107 /// `content_filter`) surface their stop reason here too instead of being raised as errors.
108 ///
109 /// `None` on intermediate content deltas and on providers that
110 /// don't surface this on streams. Downstream consumers comparing
111 /// against `max_tokens` should prefer this field when present rather
112 /// than inferring truncation from output-token counts.
113 pub stop_reason: Option<StopReason>,
114 /// Whether this is the final chunk in the stream.
115 pub is_final: bool,
116}
117
118/// Token usage statistics for a generation call.
119///
120/// Under prompt caching the provider splits the prompt: `input_tokens`
121/// counts only the *uncached* remainder, while the cached prefix is
122/// reported separately in `cache_creation_input_tokens` (a cache write
123/// on a miss) and `cache_read_input_tokens` (a cache hit). The full
124/// prompt size is therefore `input_tokens + cache_creation_input_tokens
125/// + cache_read_input_tokens` — summing only `input_tokens` undercounts
126/// once caching engages. Providers that don't report caching leave both
127/// cache fields `0`.
128#[derive(Debug, Clone, Copy, Default)]
129pub struct Usage {
130 /// Number of uncached tokens in the input/prompt.
131 pub input_tokens: u64,
132 /// Number of tokens in the generated output.
133 pub output_tokens: u64,
134 /// Tokens written to the prompt cache on a miss (Anthropic
135 /// `cache_creation_input_tokens` / Bedrock `cacheWriteInputTokens`).
136 /// `0` when caching was not engaged or not supported.
137 pub cache_creation_input_tokens: u64,
138 /// Tokens served from the prompt cache on a hit (Anthropic
139 /// `cache_read_input_tokens` / Bedrock `cacheReadInputTokens`, OpenAI
140 /// `prompt_tokens_details.cached_tokens`). `0` on a cold call.
141 pub cache_read_input_tokens: u64,
142}