Skip to main content

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}