Skip to main content

ic_rig/
completion.rs

1//! Completion model trait and associated request/response types.
2//!
3//! [`CompletionModel`] is the single interface that provider implementations
4//! must satisfy. The [`Agent`](crate::agent::Agent) and any other high-level
5//! construct in this crate are generic over it.
6//!
7//! # Implementing a provider
8//!
9//! ```rust,ignore
10//! use irig::completion::{
11//!     CompletionModel, CompletionRequest, CompletionResponse, CompletionError,
12//! };
13//!
14//! pub struct MyModel<H> {
15//!     client: H,
16//!     model: String,
17//!     api_key: String,
18//! }
19//!
20//! impl<H: HttpClient> CompletionModel for MyModel<H> {
21//!     type Error = CompletionError;
22//!
23//!     async fn complete(
24//!         &self,
25//!         request: CompletionRequest,
26//!     ) -> Result<CompletionResponse, Self::Error> {
27//!         // 1. Serialise `request` into the provider's API format.
28//!         // 2. Call `self.client.post(...)`.
29//!         // 3. Deserialise the response into `CompletionResponse`.
30//!         todo!()
31//!     }
32//! }
33//! ```
34
35use crate::message::{Message, ToolCall};
36use crate::tool::ToolDefinition;
37use thiserror::Error;
38
39// ── Request ───────────────────────────────────────────────────────────────────
40
41/// Everything a completion model needs to generate a response.
42#[derive(Debug, Clone)]
43pub struct CompletionRequest {
44    /// Conversation history, including the latest user turn.
45    pub messages: Vec<Message>,
46    /// Tools the model is allowed to call.
47    pub tools: Vec<ToolDefinition>,
48    /// Optional sampling temperature (provider-specific range).
49    pub temperature: Option<f64>,
50    /// Optional maximum number of tokens to generate.
51    pub max_tokens: Option<u32>,
52    /// Explicitly turn the model's extended-thinking / reasoning mode on or
53    /// off for this request. `None` leaves the provider's own default
54    /// behavior untouched.
55    ///
56    /// Each provider translates this into its own wire format — there's no
57    /// universal "thinking" knob across vendors, so treat this as
58    /// best-effort:
59    /// - **DeepSeek**: `thinking: {"type": "enabled" | "disabled"}`.
60    /// - **Anthropic**: `thinking: {"type": "adaptive"}` when `true`,
61    ///   `{"type": "disabled"}` when `false`. Matches Claude 4.6 and newer;
62    ///   older dated snapshots (pre-4.6) don't support this and may reject
63    ///   the request — see [`anthropic`](crate::providers::anthropic).
64    /// - **Gemini**: `generationConfig.thinkingConfig` (`thinkingBudget: -1`
65    ///   / `0`, `includeThoughts`). Some Gemini 3 models (e.g.
66    ///   `gemini-3.1-pro-preview`) can't fully disable thinking — Google's
67    ///   own docs note this — so `false` may not be honored there.
68    /// - **OpenAI**: `reasoning_effort: "high" | "minimal"`. Only
69    ///   o-series/GPT-5-family models support this; sending it to a
70    ///   non-reasoning model (e.g. `gpt-4o`) will likely error.
71    ///
72    /// Regardless of this setting, a provider's raw reasoning trace (when it
73    /// returns one) never ends up in [`CompletionResponse::choice`] — see
74    /// [`CompletionResponse::reasoning`].
75    pub thinking: Option<bool>,
76    /// Provider-specific extra parameters (passed through as-is).
77    pub extra: Option<serde_json::Value>,
78}
79
80impl CompletionRequest {
81    pub fn new(messages: Vec<Message>) -> Self {
82        Self {
83            messages,
84            tools: Vec::new(),
85            temperature: None,
86            max_tokens: None,
87            thinking: None,
88            extra: None,
89        }
90    }
91}
92
93// ── Response ──────────────────────────────────────────────────────────────────
94
95/// The model's choice of response for a single completion turn.
96#[derive(Debug, Clone)]
97pub enum ModelChoice {
98    /// The model produced a text reply.
99    Message(String),
100    /// The model wants to call one or more tools.
101    ToolCall(Vec<ToolCall>),
102}
103
104/// Token usage reported by the provider (optional — not all providers expose this).
105#[derive(Debug, Clone, Default)]
106pub struct Usage {
107    pub prompt_tokens: u32,
108    pub completion_tokens: u32,
109}
110
111/// The full response returned by a [`CompletionModel`].
112#[derive(Debug, Clone)]
113pub struct CompletionResponse {
114    /// What the model decided to do this turn.
115    ///
116    /// This is always the model's final answer — a chain-of-thought /
117    /// "thinking" trace, if the provider returns one, never ends up here.
118    /// See [`reasoning`](Self::reasoning) instead.
119    pub choice: ModelChoice,
120    /// The model's raw reasoning / chain-of-thought trace, if the provider
121    /// exposes one for this request (e.g. Anthropic extended thinking,
122    /// Gemini "thinking" parts, DeepSeek's `reasoning_content`) and the
123    /// model actually produced one this turn.
124    ///
125    /// Most callers can ignore this field entirely — `choice` is always the
126    /// straight answer. It's here for callers who specifically want to show
127    /// or log the reasoning trace alongside the answer.
128    pub reasoning: Option<String>,
129    /// Token usage, if the provider reported it.
130    pub usage: Option<Usage>,
131}
132
133// ── Error ─────────────────────────────────────────────────────────────────────
134
135/// Errors that can occur during a completion request.
136#[derive(Debug, Error)]
137pub enum CompletionError {
138    /// The HTTP transport returned an error.
139    #[error("HTTP error: {0}")]
140    Http(String),
141
142    /// The response body could not be deserialised.
143    #[error("JSON error: {0}")]
144    Json(#[from] serde_json::Error),
145
146    /// The provider returned a non-2xx status with a message.
147    #[error("Provider error ({status}): {message}")]
148    Provider { status: u16, message: String },
149
150    /// The response structure was unexpected or missing required fields.
151    #[error("Response error: {0}")]
152    Response(String),
153}
154
155// ── Trait ─────────────────────────────────────────────────────────────────────
156
157/// The core abstraction for any text-generation model.
158///
159/// Implement this for each provider (OpenAI, Anthropic, …) or for your own
160/// local model backend.
161pub trait CompletionModel {
162    type Error: std::error::Error + 'static;
163
164    /// Send a completion request and return the model's response.
165    fn complete(
166        &self,
167        request: CompletionRequest,
168    ) -> impl std::future::Future<Output = Result<CompletionResponse, Self::Error>>;
169}