Skip to main content

rig_candle/
types.rs

1//! Public errors and response metadata.
2
3use rig_core::completion::{CompletionError, Usage};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::profile::ModelFamily;
8
9/// Why a local Candle completion failed.
10#[derive(Debug, Error, Clone)]
11pub enum CandleError {
12    /// A required artifact buffer was empty.
13    #[error("the {artifact} buffer is empty")]
14    EmptyBuffer { artifact: &'static str },
15    /// The Hugging Face configuration could not be parsed or is internally invalid.
16    #[error("invalid model configuration: {0}")]
17    Configuration(String),
18    /// A parsed configuration field is incompatible with Candle's Llama implementation.
19    #[error("invalid model configuration field `{field}`: {reason}")]
20    InvalidConfigurationValue {
21        /// Configuration field name.
22        field: &'static str,
23        /// Explanation of the invalid value or relationship.
24        reason: String,
25    },
26    /// The tokenizer bytes could not be loaded.
27    #[error("invalid tokenizer: {0}")]
28    TokenizerLoading(String),
29    /// The tokenizer metadata does not identify a supported prompt family.
30    #[error("unsupported model family: {0}")]
31    UnsupportedModelFamily(String),
32    /// An explicitly selected model family disagrees with validated artifacts.
33    #[error("selected model family {selected:?} does not match detected family {detected:?}")]
34    ModelFamilyMismatch {
35        /// Family requested by the caller.
36        selected: ModelFamily,
37        /// Family detected from the tokenizer.
38        detected: ModelFamily,
39    },
40    /// Independently supplied model artifacts disagree with one another.
41    #[error("{artifact} does not match the selected model artifacts: {reason}")]
42    ArtifactMismatch {
43        /// Artifact or metadata field that disagreed.
44        artifact: &'static str,
45        /// Human-readable mismatch details.
46        reason: String,
47    },
48    /// The tokenizer vocabulary does not agree with the model configuration.
49    #[error("tokenizer vocabulary size {actual} does not match config.vocab_size {expected}")]
50    TokenizerVocabularyMismatch {
51        /// Vocabulary size required by the model configuration.
52        expected: usize,
53        /// Vocabulary size reported by the tokenizer, including added tokens.
54        actual: usize,
55    },
56    /// A selected prompt-format token is absent from the tokenizer.
57    #[error("tokenizer is missing required prompt-format special token `{token}`")]
58    MissingSpecialToken {
59        /// Required special-token string.
60        token: &'static str,
61    },
62    /// A prompt-format token exists but is not registered as special.
63    #[error("tokenizer token `{token}` must be registered as a special token")]
64    SpecialTokenNotMarked {
65        /// Formatting token that must be treated atomically by the tokenizer.
66        token: &'static str,
67    },
68    /// A configured or tokenizer-provided token ID lies outside the model vocabulary.
69    #[error("token `{token}` has ID {id}, outside vocabulary size {vocab_size}")]
70    TokenIdOutOfRange {
71        /// Configuration field or special-token name.
72        token: String,
73        /// Invalid token ID.
74        id: u32,
75        /// Model vocabulary size.
76        vocab_size: usize,
77    },
78    /// The tokenizer failed to encode the rendered prompt.
79    #[error("tokenizer encoding failed: {0}")]
80    TokenizerEncoding(String),
81    /// The tokenizer failed to decode generated token IDs.
82    #[error("tokenizer decoding failed: {0}")]
83    TokenizerDecoding(String),
84    /// The safetensors checkpoint was malformed or incompatible with the configuration.
85    #[error("invalid or incompatible safetensors checkpoint: {0}")]
86    InvalidCheckpoint(String),
87    /// A GGUF checkpoint was malformed or inconsistent with its configuration.
88    #[error("invalid or incompatible GGUF checkpoint: {0}")]
89    InvalidQuantizedCheckpoint(String),
90    /// The GGUF checkpoint does not use a supported production quantization.
91    #[error("unsupported GGUF quantization: {0}")]
92    UnsupportedQuantization(String),
93    /// A message contains content that the selected text-only prompt renderer cannot represent.
94    #[error("unsupported prompt content: {0}")]
95    UnsupportedPromptContent(&'static str),
96    /// Caller-controlled content contains a delimiter reserved by the selected chat template.
97    #[error("{field} contains reserved protocol marker `{marker}`")]
98    ReservedProtocolMarker {
99        /// Kind of prompt content containing the marker.
100        field: &'static str,
101        /// Structural delimiter that was rejected.
102        marker: &'static str,
103    },
104    /// A tensor required by the configured architecture was absent.
105    #[error("checkpoint is missing expected tensor `{0}`")]
106    MissingTensor(String),
107    /// A checkpoint tensor has an incompatible shape.
108    #[error("tensor `{tensor}` has shape {actual:?}, expected {expected:?}")]
109    TensorShapeMismatch {
110        /// Tensor name in the checkpoint.
111        tensor: String,
112        /// Shape required by Candle's implementation.
113        expected: Vec<usize>,
114        /// Shape stored in the checkpoint.
115        actual: Vec<usize>,
116    },
117    /// A checkpoint tensor uses a dtype outside the portable CPU scope.
118    #[error("tensor `{tensor}` uses safetensors dtype `{dtype}`; expected F32, F16, or BF16")]
119    UnsupportedTensorDtype {
120        /// Tensor name in the checkpoint.
121        tensor: String,
122        /// Unsupported safetensors dtype.
123        dtype: String,
124    },
125    /// Neither the configuration nor tokenizer supplied a usable stop token.
126    #[error("unable to determine a valid EOS or model end-of-turn token")]
127    MissingStopToken,
128    /// Candle could not load the model tensors.
129    #[error("Candle model loading failed: {0}")]
130    ModelLoading(String),
131    /// Candle inference failed.
132    #[error("Candle inference failed: {0}")]
133    Inference(String),
134    /// A generation setting was invalid.
135    #[error("invalid generation setting: {0}")]
136    InvalidGeneration(String),
137    /// The encoded prompt itself exceeds the model context window.
138    #[error("prompt length {prompt_tokens} exceeds the model context limit {context_limit}")]
139    PromptTooLong {
140        /// Number of prompt tokens.
141        prompt_tokens: usize,
142        /// Configured model context limit.
143        context_limit: usize,
144    },
145    /// The prompt fills the context window, leaving no capacity for generation.
146    #[error(
147        "prompt length {prompt_tokens} leaves no generation capacity in context limit {context_limit}"
148    )]
149    NoGenerationCapacity {
150        /// Number of prompt tokens.
151        prompt_tokens: usize,
152        /// Configured model context limit.
153        context_limit: usize,
154    },
155    /// A numeric generation value cannot be represented on the current platform.
156    #[error("generation value `{field}`={value} cannot be represented on this platform")]
157    NumericConversion {
158        /// Name of the value being converted.
159        field: &'static str,
160        /// Original portable value.
161        value: u64,
162    },
163    /// The native inference concurrency limit is invalid.
164    #[error("max_concurrent_requests must be greater than zero")]
165    InvalidConcurrencyLimit,
166    /// The native inference admission controller was closed unexpectedly.
167    #[cfg(not(target_family = "wasm"))]
168    #[error("Candle inference concurrency controller is closed")]
169    ConcurrencyControllerClosed,
170    /// Local inference was cooperatively cancelled.
171    #[error("Candle inference was cancelled")]
172    Cancelled,
173    /// The request uses a feature outside the selected local-model protocol.
174    #[error("unsupported Candle request feature: {0}")]
175    UnsupportedFeature(String),
176    /// A tool definition cannot be represented safely by the selected protocol.
177    #[error("invalid tool definition `{tool}`: {reason}")]
178    InvalidToolDefinition {
179        /// Tool name, or a placeholder when the name itself is invalid.
180        tool: String,
181        /// Validation failure.
182        reason: String,
183    },
184    /// A model-generated tool call was malformed or incomplete.
185    #[error("malformed Qwen3 tool call: {0}")]
186    MalformedToolCall(String),
187    /// The generated response violated the requested tool-choice policy.
188    #[error("Qwen3 tool-choice violation: {0}")]
189    ToolChoiceViolation(String),
190    /// A tool result could not be correlated with a preceding assistant call.
191    #[error("tool result `{result_id}` does not match a preceding unresolved tool call")]
192    UnmatchedToolResult {
193        /// Result/call identifier supplied in history.
194        result_id: String,
195    },
196    /// `CompletionModel::make` cannot load a byte-backed model.
197    #[error(
198        "`CompletionModel::make` is unsupported for rig-candle; use a byte-backed `CandleModel` constructor or builder"
199    )]
200    UnsupportedMake,
201    /// A native blocking inference task could not be joined.
202    #[cfg(not(target_family = "wasm"))]
203    #[error("Candle blocking task failed: {0}")]
204    BlockingTaskJoin(String),
205    /// The consumer of a native streaming response closed its bounded channel.
206    #[cfg(not(target_family = "wasm"))]
207    #[error("Candle streaming response channel was closed")]
208    StreamingChannelClosed,
209}
210
211impl From<CandleError> for CompletionError {
212    fn from(error: CandleError) -> Self {
213        CompletionError::ProviderError(error.to_string())
214    }
215}
216
217/// The reason local generation ended.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum FinishReason {
221    /// A configured EOS or family-specific end-of-turn token was sampled.
222    Eos,
223    /// The configured maximum output length was reached.
224    MaxTokens,
225}
226
227/// Serializable details returned alongside a Rig completion response.
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct CandleCompletionResponse {
230    /// Decoded generated text, excluding the prompt and stop token.
231    pub text: String,
232    /// Number of encoded prompt tokens.
233    pub prompt_tokens: u64,
234    /// Number of sampled output tokens, including an EOS token when sampled.
235    pub generated_tokens: u64,
236    /// Maximum output tokens selected by request/default precedence before context clamping.
237    pub requested_max_tokens: u64,
238    /// Maximum output tokens available after applying the model context limit.
239    pub effective_max_tokens: u64,
240    /// Why generation ended.
241    pub finish_reason: FinishReason,
242    /// Time spent preparing the prompt tensor and running its initial forward pass.
243    pub prefill_duration_ms: u64,
244    /// Time from generation start until the first sampled token, in milliseconds.
245    pub time_to_first_token_ms: Option<u64>,
246    /// Total time spent in prefill and token generation, in milliseconds.
247    pub generation_duration_ms: u64,
248    /// Generated tokens per second when the measured duration is nonzero.
249    pub tokens_per_second: Option<f64>,
250}
251
252/// Stable descriptor name reported on normalized responses from this crate.
253pub const PROVIDER_NAME: &str = "candle";
254
255impl From<&CandleCompletionResponse> for Usage {
256    fn from(response: &CandleCompletionResponse) -> Self {
257        Usage {
258            input_tokens: response.prompt_tokens,
259            output_tokens: response.generated_tokens,
260            total_tokens: response
261                .prompt_tokens
262                .saturating_add(response.generated_tokens),
263            ..Usage::new()
264        }
265    }
266}
267
268impl From<FinishReason> for rig_core::completion::FinishReason {
269    fn from(reason: FinishReason) -> Self {
270        match reason {
271            // A sampled EOS token is the local equivalent of a natural stop;
272            // `reconcile_with_output` promotes it to `ToolCalls` when the
273            // protocol parsed tool calls out of the generated text.
274            FinishReason::Eos => rig_core::completion::FinishReason::Stop,
275            FinishReason::MaxTokens => rig_core::completion::FinishReason::Length,
276        }
277    }
278}