locode_provider/completion.rs
1//! The normalized model response — provider-neutral, not any wire's raw shape.
2
3use locode_protocol::{ContentBlock, Usage};
4
5/// One normalized completion: the parsed result of a single model call (ADR-0007).
6///
7/// This is **not** a serde mirror of any wire (Anthropic returns interleaved content
8/// blocks, OpenAI returns split fields); each wire parses its raw response *into*
9/// this shape. Following Grok Build's normalized response, the assistant turn is an
10/// **ordered list of [`ContentBlock`]s** (`Text` / `Thinking{signature}` / `ToolUse`,
11/// in order) rather than pre-split `text` + `tool_calls` — so nothing is lost and
12/// the engine can append `content` straight into an assistant
13/// [`Message`](locode_protocol::Message) (ADR-0013),
14/// preserving thinking blocks and their signatures for replay.
15#[derive(Debug, Clone, PartialEq)]
16pub struct Completion {
17 /// The assistant turn's content blocks, in order.
18 pub content: Vec<ContentBlock>,
19 /// Token accounting parsed from the terminal usage event.
20 pub usage: Usage,
21 /// Why the model stopped.
22 pub stop: StopReason,
23}
24
25impl Completion {
26 /// The `tool_use` blocks the model emitted, in order.
27 pub fn tool_uses(&self) -> impl Iterator<Item = &ContentBlock> {
28 self.content
29 .iter()
30 .filter(|block| matches!(block, ContentBlock::ToolUse { .. }))
31 }
32
33 /// Whether this completion asked to call any tools.
34 #[must_use]
35 pub fn has_tool_calls(&self) -> bool {
36 self.content
37 .iter()
38 .any(|block| matches!(block, ContentBlock::ToolUse { .. }))
39 }
40
41 /// The concatenated text of all `text` blocks, if any.
42 #[must_use]
43 pub fn text(&self) -> Option<String> {
44 let mut out = String::new();
45 for block in &self.content {
46 if let ContentBlock::Text { text } = block {
47 out.push_str(text);
48 }
49 }
50 (!out.is_empty()).then_some(out)
51 }
52}
53
54/// Why the model stopped generating (Anthropic-shaped, provider-neutral).
55///
56/// Mirrors an **open** wire enum (a provider can return a reason we don't model),
57/// so it is `#[non_exhaustive]` with an [`StopReason::Unknown`] catch-all carrying
58/// the raw string — the same approach codex-rs takes for forward-compatibility. Not
59/// serialized (it is mapped to [`locode_protocol::Status`] by the engine), so no
60/// serde derive is needed.
61#[derive(Debug, Clone, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum StopReason {
64 /// The model finished its turn with a natural stop.
65 EndTurn,
66 /// The output hit the `max_tokens` ceiling.
67 MaxTokens,
68 /// The model wants to call one or more tools.
69 ToolUse,
70 /// A configured stop sequence was produced.
71 StopSequence,
72 /// The model refused to continue.
73 Refusal,
74 /// The turn was paused (e.g. a server-tool round-trip).
75 PauseTurn,
76 /// A stop reason this client does not model, carried verbatim.
77 Unknown(String),
78}