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}