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/// One incremental piece of a streaming completion (ADR-0021), normalized across
55/// wires — the display-oriented side channel of [`crate::Provider::stream`].
56///
57/// This is **display-only**: [`crate::Provider::stream`] still returns the same whole
58/// [`Completion`] (assembled via [`ToolCallAssembler`](crate::ToolCallAssembler)),
59/// so tool dispatch and history are unaffected. The parts mirror what the wires
60/// actually stream (text/reasoning on distinct channels; tool name/id early, args
61/// as a raw partial-JSON channel that is **never** parsed here — see ADR-0021's
62/// granularity table).
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum CompletionDelta {
65 /// A fragment of assistant text.
66 Text(String),
67 /// A fragment of reasoning/thinking text (its own channel).
68 Thinking(String),
69 /// A tool call has started — name + id are known early (before args stream).
70 ToolUseStart {
71 /// The `tool_use` id.
72 id: String,
73 /// The client-facing tool name.
74 name: String,
75 },
76 /// A raw partial-JSON fragment of the current tool call's arguments. **Not**
77 /// valid JSON in isolation — display-only; the call is assembled and parsed
78 /// once at its finalize boundary.
79 ToolArgs(String),
80}
81
82/// Why the model stopped generating (Anthropic-shaped, provider-neutral).
83///
84/// Mirrors an **open** wire enum (a provider can return a reason we don't model),
85/// so it is `#[non_exhaustive]` with an [`StopReason::Unknown`] catch-all carrying
86/// the raw string — the same approach codex-rs takes for forward-compatibility. Not
87/// serialized (it is mapped to [`locode_protocol::Status`] by the engine), so no
88/// serde derive is needed.
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum StopReason {
92 /// The model finished its turn with a natural stop.
93 EndTurn,
94 /// The output hit the `max_tokens` ceiling.
95 MaxTokens,
96 /// The model wants to call one or more tools.
97 ToolUse,
98 /// A configured stop sequence was produced.
99 StopSequence,
100 /// The model refused to continue.
101 Refusal,
102 /// The turn was paused (e.g. a server-tool round-trip).
103 PauseTurn,
104 /// A stop reason this client does not model, carried verbatim.
105 Unknown(String),
106}