Skip to main content

llmleaf_client/
types.rs

1//! Public SDK types — serde structs that mirror `proto/llmleaf/v1/llmleaf.proto` and
2//! serialise to / parse from the exact OpenAI/OpenRouter-shaped JSON the gateway speaks
3//! (see SPEC.md).
4//!
5//! Why not the prost-generated types? Prost emits protobuf-binary-shaped types: oneofs
6//! become Rust enums with no JSON union behaviour, proto enums are `i32`, and there is
7//! no OpenAI casing. These structs are the wire model; [`crate::pb`] is the codegen
8//! proof / canonical-proto mirror.
9//!
10//! ## Enum ⇄ wire mapping
11//!
12//! SPEC.md asks for a single `enumToWire`/`enumFromWire` pair that lowercases the enum
13//! value name. In serde the mechanical equivalent is `#[serde(rename_all =
14//! "snake_case")]` on every closed-set enum: it turns `TOOL_CALLS` → `"tool_calls"`,
15//! `IN_PROGRESS` → `"in_progress"`, etc. The proto `*_UNSPECIFIED` zero value ⇔ field
16//! absent is modelled by making the field `Option<_>` and skipping it when `None`.
17//!
18//! ## Free-form JSON fields
19//!
20//! `ChatRequest.extra`, `FunctionDef.parameters`, `ResponseFormat.json_schema`,
21//! `EmbeddingRequest.extra`, `SpeechRequest.extra`, `ModelEntry.default_parameters`
22//! are carried as [`serde_json::Value`] (a raw JSON value), so they are spliced into the
23//! body verbatim on encode and captured back as a sub-object on decode — never
24//! double-encoded as a string. `ChatRequest.extra` is additionally flattened so its keys
25//! merge at the top level of the request object.
26
27use serde::{Deserialize, Serialize};
28
29// ---------------------------------------------------------------------------
30// Common
31// ---------------------------------------------------------------------------
32
33/// Token accounting echoed on every response. `cost_usd` is an llmleaf addition and is
34/// absent when the model has no known price.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Usage {
37    #[serde(default)]
38    pub prompt_tokens: u32,
39    #[serde(default)]
40    pub completion_tokens: u32,
41    #[serde(default)]
42    pub total_tokens: u32,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub cost_usd: Option<f64>,
45}
46
47// ---------------------------------------------------------------------------
48// Enums (closed-set; snake_case wire tokens; absent == unspecified)
49// ---------------------------------------------------------------------------
50
51/// Message author role.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum Role {
55    System,
56    User,
57    Assistant,
58    Tool,
59}
60
61/// Why the model stopped generating.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum FinishReason {
65    Stop,
66    Length,
67    ToolCalls,
68    ContentFilter,
69}
70
71/// Batch lifecycle status.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum BatchStatus {
75    Validating,
76    InProgress,
77    Finalizing,
78    Completed,
79    Failed,
80    Expired,
81    Canceling,
82    Canceled,
83}
84
85// ---------------------------------------------------------------------------
86// Chat — content parts (multimodal)
87// ---------------------------------------------------------------------------
88
89/// `{"url":"...","detail":"auto"}`.
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct ImageUrl {
92    pub url: String,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub detail: Option<String>,
95}
96
97/// A single content part of a multimodal message.
98///
99/// Serialises to the tagged OpenAI shapes:
100/// `{"type":"text","text":"..."}` and
101/// `{"type":"image_url","image_url":{...}}`.
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(tag = "type", rename_all = "snake_case")]
104pub enum ContentPart {
105    Text { text: String },
106    ImageUrl { image_url: ImageUrl },
107}
108
109impl ContentPart {
110    /// Convenience constructor for a text part.
111    pub fn text(text: impl Into<String>) -> Self {
112        ContentPart::Text { text: text.into() }
113    }
114
115    /// Convenience constructor for an image-url part.
116    pub fn image_url(url: impl Into<String>) -> Self {
117        ContentPart::ImageUrl {
118            image_url: ImageUrl {
119                url: url.into(),
120                detail: None,
121            },
122        }
123    }
124}
125
126/// Message `content`: a plain string when there is only text, else an array of parts.
127/// Untagged so it serialises as a bare string or a bare array, matching the wire.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(untagged)]
130pub enum Content {
131    Text(String),
132    Parts(Vec<ContentPart>),
133}
134
135impl From<String> for Content {
136    fn from(s: String) -> Self {
137        Content::Text(s)
138    }
139}
140
141impl From<&str> for Content {
142    fn from(s: &str) -> Self {
143        Content::Text(s.to_string())
144    }
145}
146
147impl From<Vec<ContentPart>> for Content {
148    fn from(p: Vec<ContentPart>) -> Self {
149        Content::Parts(p)
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Chat — tool calls
155// ---------------------------------------------------------------------------
156
157/// A function the model called. `arguments` is a JSON-encoded string (OpenAI shape).
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct FunctionCall {
160    pub name: String,
161    pub arguments: String,
162}
163
164/// A tool call emitted by the model.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct ToolCall {
167    pub id: String,
168    #[serde(rename = "type")]
169    pub kind: String,
170    pub function: FunctionCall,
171}
172
173/// Incremental tool-call fragment on a streaming delta.
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct FunctionCallDelta {
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub name: Option<String>,
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub arguments: Option<String>,
180}
181
182/// Streaming tool-call delta; fields arrive piecemeal.
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct ToolCallDelta {
185    pub index: u32,
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub id: Option<String>,
188    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
189    pub kind: Option<String>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub function: Option<FunctionCallDelta>,
192}
193
194// ---------------------------------------------------------------------------
195// Chat — messages
196// ---------------------------------------------------------------------------
197
198/// A chat message (request or response).
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct ChatMessage {
201    pub role: Role,
202    /// `content` is a plain string or an array of content parts. Optional because an
203    /// assistant message that only calls tools may omit it.
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub content: Option<Content>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub name: Option<String>,
208    #[serde(default, skip_serializing_if = "Vec::is_empty")]
209    pub tool_calls: Vec<ToolCall>,
210    /// Set when `role == Tool`.
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub tool_call_id: Option<String>,
213}
214
215impl ChatMessage {
216    /// A `system` message with text content.
217    pub fn system(text: impl Into<String>) -> Self {
218        Self::with_text(Role::System, text)
219    }
220
221    /// A `user` message with text content.
222    pub fn user(text: impl Into<String>) -> Self {
223        Self::with_text(Role::User, text)
224    }
225
226    /// An `assistant` message with text content.
227    pub fn assistant(text: impl Into<String>) -> Self {
228        Self::with_text(Role::Assistant, text)
229    }
230
231    fn with_text(role: Role, text: impl Into<String>) -> Self {
232        ChatMessage {
233            role,
234            content: Some(Content::Text(text.into())),
235            name: None,
236            tool_calls: Vec::new(),
237            tool_call_id: None,
238        }
239    }
240
241    /// Accumulated plain-text content, if the message carries a text body.
242    pub fn text(&self) -> Option<&str> {
243        match &self.content {
244            Some(Content::Text(s)) => Some(s.as_str()),
245            _ => None,
246        }
247    }
248}
249
250// ---------------------------------------------------------------------------
251// Chat — tool / response-format definitions
252// ---------------------------------------------------------------------------
253
254/// A function the model MAY call. `parameters` is a raw JSON Schema value.
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256pub struct FunctionDef {
257    pub name: String,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub description: Option<String>,
260    /// Raw JSON object spliced verbatim.
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub parameters: Option<serde_json::Value>,
263}
264
265/// A tool definition (`{"type":"function","function":{...}}`).
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct ToolDef {
268    #[serde(rename = "type")]
269    pub kind: String,
270    pub function: FunctionDef,
271}
272
273impl ToolDef {
274    /// A `function`-typed tool definition.
275    pub fn function(function: FunctionDef) -> Self {
276        ToolDef {
277            kind: "function".to_string(),
278            function,
279        }
280    }
281}
282
283/// Pin a specific function: `{"name":"..."}`.
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct FunctionName {
286    pub name: String,
287}
288
289/// Named tool choice: `{"type":"function","function":{"name":"..."}}`.
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
291pub struct NamedToolChoice {
292    #[serde(rename = "type")]
293    pub kind: String,
294    pub function: FunctionName,
295}
296
297/// `tool_choice`: a bare mode string (`"auto"`/`"none"`/`"required"`) or a named object.
298/// Untagged so it serialises as a bare string or the object, matching the wire.
299#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300#[serde(untagged)]
301pub enum ToolChoice {
302    Mode(String),
303    Named(NamedToolChoice),
304}
305
306impl ToolChoice {
307    /// `"auto"`, `"none"` or `"required"`.
308    pub fn mode(mode: impl Into<String>) -> Self {
309        ToolChoice::Mode(mode.into())
310    }
311
312    /// Pin a named function.
313    pub fn named(name: impl Into<String>) -> Self {
314        ToolChoice::Named(NamedToolChoice {
315            kind: "function".to_string(),
316            function: FunctionName { name: name.into() },
317        })
318    }
319}
320
321/// `response_format`: `{"type":"text"|"json_object"|"json_schema","json_schema":{...}}`.
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct ResponseFormat {
324    #[serde(rename = "type")]
325    pub kind: String,
326    /// Raw JSON object, present when `type == "json_schema"`.
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub json_schema: Option<serde_json::Value>,
329}
330
331// ---------------------------------------------------------------------------
332// Chat — request
333// ---------------------------------------------------------------------------
334
335/// `stop`: a bare string for one element, else an array (the wire accepts either; we
336/// emit a string for a single element and an array otherwise). Untagged.
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338#[serde(untagged)]
339pub enum Stop {
340    One(String),
341    Many(Vec<String>),
342}
343
344impl Stop {
345    /// Build the wire-appropriate `stop` from a list: a bare string for exactly one
346    /// element, an array otherwise. Returns `None` for an empty list.
347    pub fn from_vec(mut v: Vec<String>) -> Option<Self> {
348        match v.len() {
349            0 => None,
350            1 => Some(Stop::One(v.pop().unwrap())),
351            _ => Some(Stop::Many(v)),
352        }
353    }
354}
355
356/// `POST /v1/chat/completions` request body.
357#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
358pub struct ChatRequest {
359    pub model: String,
360    pub messages: Vec<ChatMessage>,
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub stream: Option<bool>,
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub temperature: Option<f32>,
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub top_p: Option<f32>,
367    /// Legacy token cap. Prefer `max_completion_tokens`; this is still sent when the
368    /// caller set only it.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub max_tokens: Option<u32>,
371    /// Modern token cap (takes precedence on the wire).
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub max_completion_tokens: Option<u32>,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub stop: Option<Stop>,
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub n: Option<u32>,
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub seed: Option<i64>,
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub frequency_penalty: Option<f32>,
382    #[serde(default, skip_serializing_if = "Option::is_none")]
383    pub presence_penalty: Option<f32>,
384    #[serde(default, skip_serializing_if = "Vec::is_empty")]
385    pub tools: Vec<ToolDef>,
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub tool_choice: Option<ToolChoice>,
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub response_format: Option<ResponseFormat>,
390    /// `"low" | "medium" | "high"`.
391    #[serde(default, skip_serializing_if = "Option::is_none")]
392    pub reasoning_effort: Option<String>,
393    /// Dialect-specific passthrough. Its keys are merged at the top level of the request
394    /// object (P7 transparent passthrough), so it is `#[serde(flatten)]`ed here.
395    #[serde(default, flatten, skip_serializing_if = "Option::is_none")]
396    pub extra: Option<serde_json::Map<String, serde_json::Value>>,
397}
398
399impl ChatRequest {
400    /// Start a chat request for `model` with `messages`.
401    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
402        ChatRequest {
403            model: model.into(),
404            messages,
405            ..Default::default()
406        }
407    }
408}
409
410// ---------------------------------------------------------------------------
411// Chat — non-streaming response
412// ---------------------------------------------------------------------------
413
414/// One completion choice.
415#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
416pub struct Choice {
417    #[serde(default)]
418    pub index: u32,
419    pub message: ChatMessage,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub finish_reason: Option<FinishReason>,
422}
423
424/// `POST /v1/chat/completions` non-streaming response (`object:"chat.completion"`).
425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
426pub struct ChatResponse {
427    pub id: String,
428    pub object: String,
429    pub created: i64,
430    pub model: String,
431    pub choices: Vec<Choice>,
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub usage: Option<Usage>,
434}
435
436impl ChatResponse {
437    /// Plain text of the first choice, if any.
438    pub fn first_text(&self) -> Option<&str> {
439        self.choices.first().and_then(|c| c.message.text())
440    }
441}
442
443// ---------------------------------------------------------------------------
444// Chat — streaming chunk
445// ---------------------------------------------------------------------------
446
447/// Incremental delta on a streaming choice.
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
449pub struct Delta {
450    /// First chunk only.
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub role: Option<Role>,
453    /// Incremental text.
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    pub content: Option<String>,
456    #[serde(default, skip_serializing_if = "Vec::is_empty")]
457    pub tool_calls: Vec<ToolCallDelta>,
458}
459
460/// One streaming choice.
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
462pub struct ChunkChoice {
463    #[serde(default)]
464    pub index: u32,
465    pub delta: Delta,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub finish_reason: Option<FinishReason>,
468}
469
470/// A streaming SSE frame (`object:"chat.completion.chunk"`).
471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
472pub struct ChatCompletionChunk {
473    pub id: String,
474    pub object: String,
475    pub created: i64,
476    pub model: String,
477    pub choices: Vec<ChunkChoice>,
478    /// Terminal chunk only.
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub usage: Option<Usage>,
481}
482
483impl ChatCompletionChunk {
484    /// The incremental text of the first choice's delta, if any.
485    pub fn first_delta_text(&self) -> Option<&str> {
486        self.choices
487            .first()
488            .and_then(|c| c.delta.content.as_deref())
489    }
490}
491
492// ---------------------------------------------------------------------------
493// Embeddings
494// ---------------------------------------------------------------------------
495
496/// `input`: the wire accepts a bare string or an array of strings. Untagged.
497#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
498#[serde(untagged)]
499pub enum EmbeddingInput {
500    One(String),
501    Many(Vec<String>),
502}
503
504impl From<String> for EmbeddingInput {
505    fn from(s: String) -> Self {
506        EmbeddingInput::One(s)
507    }
508}
509
510impl From<&str> for EmbeddingInput {
511    fn from(s: &str) -> Self {
512        EmbeddingInput::One(s.to_string())
513    }
514}
515
516impl From<Vec<String>> for EmbeddingInput {
517    fn from(v: Vec<String>) -> Self {
518        EmbeddingInput::Many(v)
519    }
520}
521
522/// `POST /v1/embeddings` request body.
523#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
524pub struct EmbeddingRequest {
525    pub model: String,
526    pub input: EmbeddingInput,
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub dimensions: Option<u32>,
529    /// `"float"` | `"base64"`. When `"base64"`, the transport decodes each embedding back
530    /// into a float vector before returning (see SPEC.md).
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub encoding_format: Option<String>,
533    /// Raw JSON object passthrough.
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub extra: Option<serde_json::Value>,
536}
537
538impl EmbeddingRequest {
539    /// Start an embedding request.
540    pub fn new(model: impl Into<String>, input: impl Into<EmbeddingInput>) -> Self {
541        EmbeddingRequest {
542            model: model.into(),
543            input: input.into(),
544            dimensions: None,
545            encoding_format: None,
546            extra: None,
547        }
548    }
549}
550
551/// A single embedding vector. The transport always presents `embedding` as floats, even
552/// when the wire carried base64.
553#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
554pub struct Embedding {
555    pub object: String,
556    #[serde(default)]
557    pub index: u32,
558    pub embedding: Vec<f32>,
559}
560
561/// `POST /v1/embeddings` response (`object:"list"`).
562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563pub struct EmbeddingResponse {
564    pub object: String,
565    pub data: Vec<Embedding>,
566    pub model: String,
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub usage: Option<Usage>,
569}
570
571// ---------------------------------------------------------------------------
572// Audio — speech / voices
573// ---------------------------------------------------------------------------
574
575/// `POST /v1/audio/speech` request body.
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577pub struct SpeechRequest {
578    pub model: String,
579    pub input: String,
580    pub voice: String,
581    /// `mp3|opus|aac|flac|wav|pcm`.
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub response_format: Option<String>,
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub speed: Option<f32>,
586    /// Raw JSON object passthrough.
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub extra: Option<serde_json::Value>,
589}
590
591impl SpeechRequest {
592    /// Start a speech request.
593    pub fn new(
594        model: impl Into<String>,
595        input: impl Into<String>,
596        voice: impl Into<String>,
597    ) -> Self {
598        SpeechRequest {
599            model: model.into(),
600            input: input.into(),
601            voice: voice.into(),
602            response_format: None,
603            speed: None,
604            extra: None,
605        }
606    }
607}
608
609/// A TTS voice.
610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611pub struct Voice {
612    pub id: String,
613    #[serde(default, skip_serializing_if = "Option::is_none")]
614    pub name: Option<String>,
615    #[serde(default, skip_serializing_if = "Vec::is_empty")]
616    pub languages: Vec<String>,
617}
618
619/// `GET /v1/audio/voices` response.
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub struct VoicesResponse {
622    pub model: String,
623    pub voices: Vec<Voice>,
624}
625
626// ---------------------------------------------------------------------------
627// Audio — transcriptions (STT)
628// ---------------------------------------------------------------------------
629
630/// The accompanying form fields for `POST /v1/audio/transcriptions`. The audio bytes are
631/// the multipart `file` part and are passed separately to the SDK call.
632#[derive(Debug, Clone, PartialEq, Default)]
633pub struct TranscriptionRequest {
634    pub model: String,
635    /// ISO-639-1 hint.
636    pub language: Option<String>,
637    /// Decoding bias.
638    pub prompt: Option<String>,
639    /// `json|text|verbose_json|srt|vtt`.
640    pub response_format: Option<String>,
641    pub temperature: Option<f32>,
642}
643
644impl TranscriptionRequest {
645    /// Start a transcription request for `model`.
646    pub fn new(model: impl Into<String>) -> Self {
647        TranscriptionRequest {
648            model: model.into(),
649            ..Default::default()
650        }
651    }
652}
653
654/// Structured transcription result (for `response_format` json / verbose_json).
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
656pub struct TranscriptionResponse {
657    pub text: String,
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub task: Option<String>,
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub language: Option<String>,
662    #[serde(default, skip_serializing_if = "Option::is_none")]
663    pub duration: Option<f32>,
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub usage: Option<Usage>,
666}
667
668/// Result of a transcription: a structured object (json/verbose_json) or a plain-text
669/// body (text/srt/vtt) — SPEC.md returns text directly for the latter.
670#[derive(Debug, Clone, PartialEq)]
671pub enum Transcription {
672    Json(TranscriptionResponse),
673    Text(String),
674}
675
676// ---------------------------------------------------------------------------
677// Model catalog
678// ---------------------------------------------------------------------------
679
680/// Model architecture metadata.
681#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682pub struct Architecture {
683    #[serde(default, skip_serializing_if = "Vec::is_empty")]
684    pub input_modalities: Vec<String>,
685    #[serde(default, skip_serializing_if = "Vec::is_empty")]
686    pub output_modalities: Vec<String>,
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub modality: Option<String>,
689    #[serde(default)]
690    pub tokenizer: String,
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub instruct_type: Option<String>,
693}
694
695/// Per-token pricing (decimal strings, USD).
696#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
697pub struct Pricing {
698    pub prompt: String,
699    pub completion: String,
700}
701
702/// Top-provider capabilities for a model.
703#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
704pub struct TopProvider {
705    #[serde(default, skip_serializing_if = "Option::is_none")]
706    pub context_length: Option<u32>,
707    #[serde(default, skip_serializing_if = "Option::is_none")]
708    pub max_completion_tokens: Option<u32>,
709    #[serde(default)]
710    pub is_moderated: bool,
711    #[serde(default, skip_serializing_if = "Option::is_none")]
712    pub max_thinking_tokens: Option<u32>,
713}
714
715/// Admin-only fallback-chain entry (present only with a valid `x-admin-token`).
716#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
717pub struct ModelEndpoint {
718    pub provider: String,
719    pub model: String,
720    #[serde(default)]
721    pub down: bool,
722    /// `"route" | "prefix"`.
723    pub source: String,
724}
725
726/// One model in the catalog.
727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
728pub struct ModelEntry {
729    pub id: String,
730    #[serde(default)]
731    pub canonical_slug: String,
732    #[serde(default)]
733    pub name: String,
734    #[serde(default)]
735    pub created: i64,
736    #[serde(default)]
737    pub description: String,
738    #[serde(default, skip_serializing_if = "Option::is_none")]
739    pub context_length: Option<u32>,
740    #[serde(default, skip_serializing_if = "Option::is_none")]
741    pub architecture: Option<Architecture>,
742    #[serde(default, skip_serializing_if = "Option::is_none")]
743    pub pricing: Option<Pricing>,
744    #[serde(default, skip_serializing_if = "Option::is_none")]
745    pub top_provider: Option<TopProvider>,
746    #[serde(default, skip_serializing_if = "Vec::is_empty")]
747    pub supported_parameters: Vec<String>,
748    #[serde(default, skip_serializing_if = "Vec::is_empty")]
749    pub unsupported_parameters: Vec<String>,
750    /// Raw JSON object.
751    #[serde(default, skip_serializing_if = "Option::is_none")]
752    pub default_parameters: Option<serde_json::Value>,
753    /// Admin-only.
754    #[serde(default, skip_serializing_if = "Vec::is_empty")]
755    pub endpoints: Vec<ModelEndpoint>,
756}
757
758/// `GET /v1/models` response.
759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
760pub struct ListModelsResponse {
761    #[serde(default)]
762    pub data: Vec<ModelEntry>,
763}
764
765/// `type` query filter for `GET /v1/models`.
766#[derive(Debug, Clone, Copy, PartialEq, Eq)]
767pub enum ModelType {
768    All,
769    Llm,
770    Tts,
771    Stt,
772    Embedding,
773}
774
775impl ModelType {
776    pub(crate) fn as_str(self) -> &'static str {
777        match self {
778            ModelType::All => "all",
779            ModelType::Llm => "llm",
780            ModelType::Tts => "tts",
781            ModelType::Stt => "stt",
782            ModelType::Embedding => "embedding",
783        }
784    }
785}
786
787// ---------------------------------------------------------------------------
788// Batches
789// ---------------------------------------------------------------------------
790
791/// One item in a batch create request: a `custom_id` plus a `ChatRequest` body.
792#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
793pub struct BatchRequestItem {
794    pub custom_id: String,
795    pub body: ChatRequest,
796}
797
798/// `POST /v1/batches` request body.
799#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
800pub struct BatchCreateRequest {
801    pub requests: Vec<BatchRequestItem>,
802}
803
804/// Aggregate counts on a batch handle.
805#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
806pub struct BatchCounts {
807    #[serde(default)]
808    pub total: u64,
809    #[serde(default)]
810    pub processing: u64,
811    #[serde(default)]
812    pub succeeded: u64,
813    #[serde(default)]
814    pub errored: u64,
815    #[serde(default)]
816    pub canceled: u64,
817    #[serde(default)]
818    pub expired: u64,
819}
820
821/// A batch handle returned by create / retrieve / cancel.
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823pub struct BatchHandle {
824    pub id: String,
825    pub status: BatchStatus,
826    #[serde(default)]
827    pub counts: BatchCounts,
828    #[serde(default, skip_serializing_if = "Option::is_none")]
829    pub created_at: Option<i64>,
830    #[serde(default, skip_serializing_if = "Option::is_none")]
831    pub expires_at: Option<i64>,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub ended_at: Option<i64>,
834    #[serde(default, skip_serializing_if = "Option::is_none")]
835    pub endpoint: Option<String>,
836}
837
838/// A successful per-item batch response.
839#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
840pub struct BatchResponse {
841    pub status_code: u32,
842    pub body: ChatResponse,
843}
844
845/// A failed per-item batch response.
846#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
847pub struct BatchError {
848    pub code: String,
849    pub message: String,
850}
851
852/// One line of the JSONL results stream.
853#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
854pub struct BatchResultLine {
855    pub custom_id: String,
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub response: Option<BatchResponse>,
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub error: Option<BatchError>,
860}