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}