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::de::{self, Deserializer};
28use serde::ser::{SerializeSeq, Serializer};
29use serde::{Deserialize, Serialize};
30
31// ---------------------------------------------------------------------------
32// Common
33// ---------------------------------------------------------------------------
34
35/// Token accounting echoed on every response. `cost_usd` is an llmleaf addition and is
36/// absent when the model has no known price.
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct Usage {
39    #[serde(default)]
40    pub prompt_tokens: u32,
41    #[serde(default)]
42    pub completion_tokens: u32,
43    #[serde(default)]
44    pub total_tokens: u32,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub cost_usd: Option<f64>,
47    /// Prompt-cache hit accounting (OpenAI `usage.prompt_tokens_details`). Absent when the upstream
48    /// reported no caching; [`Usage::cached_tokens`] flattens it to a plain count.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub prompt_tokens_details: Option<PromptTokensDetails>,
51    /// Input tokens written to the provider's prompt cache this request — a cache *write* (creation).
52    /// An llmleaf extension (Anthropic reports it; OpenAI/OpenRouter do not); absent when there were none.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub cache_creation_tokens: Option<u32>,
55}
56
57impl Usage {
58    /// Prompt tokens served from the provider's cache this request — a cache *read* (hit). `0` when
59    /// the upstream reported no caching.
60    pub fn cached_tokens(&self) -> u32 {
61        self.prompt_tokens_details
62            .as_ref()
63            .and_then(|d| d.cached_tokens)
64            .unwrap_or(0)
65    }
66
67    /// Input tokens written to the provider's cache this request — a cache *write* (creation). `0`
68    /// when there were none (or the provider does not report writes).
69    pub fn cache_writes(&self) -> u32 {
70        self.cache_creation_tokens.unwrap_or(0)
71    }
72}
73
74/// Breakdown of [`Usage::prompt_tokens`]. Today only the cache-read (hit) share is surfaced — the
75/// count of prompt tokens served from the provider's cache rather than processed fresh.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct PromptTokensDetails {
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub cached_tokens: Option<u32>,
80}
81
82// ---------------------------------------------------------------------------
83// Enums (closed-set; snake_case wire tokens; absent == unspecified)
84// ---------------------------------------------------------------------------
85
86/// Message author role.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum Role {
90    System,
91    User,
92    Assistant,
93    Tool,
94}
95
96/// Why the model stopped generating.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum FinishReason {
100    Stop,
101    Length,
102    ToolCalls,
103    ContentFilter,
104}
105
106/// Batch lifecycle status.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum BatchStatus {
110    Validating,
111    InProgress,
112    Finalizing,
113    Completed,
114    Failed,
115    Expired,
116    Canceling,
117    Canceled,
118}
119
120// ---------------------------------------------------------------------------
121// Chat — content parts (multimodal)
122// ---------------------------------------------------------------------------
123
124/// `{"url":"...","detail":"auto"}`.
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct ImageUrl {
127    pub url: String,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub detail: Option<String>,
130}
131
132/// A single content part of a multimodal message.
133///
134/// Serialises to the tagged OpenAI shapes:
135/// `{"type":"text","text":"..."}` and
136/// `{"type":"image_url","image_url":{...}}`.
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[serde(tag = "type", rename_all = "snake_case")]
139pub enum ContentPart {
140    Text { text: String },
141    ImageUrl { image_url: ImageUrl },
142}
143
144impl ContentPart {
145    /// Convenience constructor for a text part.
146    pub fn text(text: impl Into<String>) -> Self {
147        ContentPart::Text { text: text.into() }
148    }
149
150    /// Convenience constructor for an image-url part.
151    pub fn image_url(url: impl Into<String>) -> Self {
152        ContentPart::ImageUrl {
153            image_url: ImageUrl {
154                url: url.into(),
155                detail: None,
156            },
157        }
158    }
159}
160
161/// Message `content`: a plain string when there is only text, else an array of parts.
162/// Untagged so it serialises as a bare string or a bare array, matching the wire.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(untagged)]
165pub enum Content {
166    Text(String),
167    Parts(Vec<ContentPart>),
168}
169
170impl From<String> for Content {
171    fn from(s: String) -> Self {
172        Content::Text(s)
173    }
174}
175
176impl From<&str> for Content {
177    fn from(s: &str) -> Self {
178        Content::Text(s.to_string())
179    }
180}
181
182impl From<Vec<ContentPart>> for Content {
183    fn from(p: Vec<ContentPart>) -> Self {
184        Content::Parts(p)
185    }
186}
187
188// ---------------------------------------------------------------------------
189// Chat — tool calls
190// ---------------------------------------------------------------------------
191
192/// A function the model called. `arguments` is a JSON-encoded string (OpenAI shape).
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct FunctionCall {
195    pub name: String,
196    pub arguments: String,
197}
198
199/// A tool call emitted by the model.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct ToolCall {
202    pub id: String,
203    #[serde(rename = "type")]
204    pub kind: String,
205    pub function: FunctionCall,
206}
207
208/// Incremental tool-call fragment on a streaming delta.
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210pub struct FunctionCallDelta {
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub name: Option<String>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub arguments: Option<String>,
215}
216
217/// Streaming tool-call delta; fields arrive piecemeal.
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct ToolCallDelta {
220    pub index: u32,
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub id: Option<String>,
223    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
224    pub kind: Option<String>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub function: Option<FunctionCallDelta>,
227}
228
229// ---------------------------------------------------------------------------
230// Chat — reasoning ("thinking") blocks
231// ---------------------------------------------------------------------------
232
233/// One structured reasoning ("thinking") block (OpenRouter `reasoning_details[]`). It expresses both
234/// *open* reasoning — visible text, optionally signed — and *hidden* reasoning — an encrypted/redacted
235/// blob the provider returns in place of the text. `kind` (wire `type`) is the discriminator:
236///
237/// - `"reasoning.text"` → [`text`](Self::text) (+ optional [`signature`](Self::signature)) — **open**
238/// - `"reasoning.summary"` → [`summary`](Self::summary) — **open** (a summarised view)
239/// - `"reasoning.encrypted"` → [`data`](Self::data) — **hidden** (redacted / opaque)
240///
241/// `signature` and `data` are opaque and MUST be sent back verbatim in the next request's
242/// `reasoning_details` to continue a signed/encrypted reasoning turn (the upstream rejects an altered
243/// or dropped block — e.g. before a tool call). Use [`is_hidden`](Self::is_hidden) /
244/// [`open_text`](Self::open_text) to branch without matching on the raw `kind` string.
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
246pub struct ReasoningDetail {
247    #[serde(rename = "type")]
248    pub kind: String,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub text: Option<String>,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub summary: Option<String>,
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub data: Option<String>,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub signature: Option<String>,
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub id: Option<String>,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub format: Option<String>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub index: Option<u32>,
263}
264
265impl ReasoningDetail {
266    /// Whether this block is hidden (redacted / encrypted) rather than open visible reasoning.
267    pub fn is_hidden(&self) -> bool {
268        self.kind == "reasoning.encrypted" || (self.data.is_some() && self.text.is_none())
269    }
270
271    /// The visible reasoning text of an open block — its `text`, falling back to its `summary`.
272    /// `None` for a hidden block.
273    pub fn open_text(&self) -> Option<&str> {
274        self.text.as_deref().or(self.summary.as_deref())
275    }
276}
277
278// ---------------------------------------------------------------------------
279// Chat — messages
280// ---------------------------------------------------------------------------
281
282/// A chat message (request or response).
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284pub struct ChatMessage {
285    pub role: Role,
286    /// `content` is a plain string or an array of content parts. Optional because an
287    /// assistant message that only calls tools may omit it.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub content: Option<Content>,
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub name: Option<String>,
292    #[serde(default, skip_serializing_if = "Vec::is_empty")]
293    pub tool_calls: Vec<ToolCall>,
294    /// Set when `role == Tool`.
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub tool_call_id: Option<String>,
297    /// Open reasoning text the assistant emitted (OpenRouter `reasoning`), if any. The flat,
298    /// human-readable form; the structured [`reasoning_details`](Self::reasoning_details) is the
299    /// replay-safe one.
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub reasoning: Option<String>,
302    /// Structured reasoning blocks (open and hidden, with signatures — see [`ReasoningDetail`]). Echo
303    /// these back verbatim on the next request to preserve signed reasoning across a turn.
304    #[serde(default, skip_serializing_if = "Vec::is_empty")]
305    pub reasoning_details: Vec<ReasoningDetail>,
306}
307
308impl ChatMessage {
309    /// A `system` message with text content.
310    pub fn system(text: impl Into<String>) -> Self {
311        Self::with_text(Role::System, text)
312    }
313
314    /// A `user` message with text content.
315    pub fn user(text: impl Into<String>) -> Self {
316        Self::with_text(Role::User, text)
317    }
318
319    /// An `assistant` message with text content.
320    pub fn assistant(text: impl Into<String>) -> Self {
321        Self::with_text(Role::Assistant, text)
322    }
323
324    fn with_text(role: Role, text: impl Into<String>) -> Self {
325        ChatMessage {
326            role,
327            content: Some(Content::Text(text.into())),
328            name: None,
329            tool_calls: Vec::new(),
330            tool_call_id: None,
331            reasoning: None,
332            reasoning_details: Vec::new(),
333        }
334    }
335
336    /// Accumulated plain-text content, if the message carries a text body.
337    pub fn text(&self) -> Option<&str> {
338        match &self.content {
339            Some(Content::Text(s)) => Some(s.as_str()),
340            _ => None,
341        }
342    }
343
344    /// The visible (open) reasoning for this message: the flat `reasoning` text if present, else the
345    /// concatenation of the open `reasoning_details` blocks. `None` when the turn carried no visible
346    /// reasoning (it may still carry hidden blocks — see [`reasoning_details`](Self::reasoning_details)).
347    pub fn reasoning_text(&self) -> Option<String> {
348        if let Some(r) = &self.reasoning {
349            return Some(r.clone());
350        }
351        let joined: String = self
352            .reasoning_details
353            .iter()
354            .filter_map(ReasoningDetail::open_text)
355            .collect();
356        (!joined.is_empty()).then_some(joined)
357    }
358}
359
360// ---------------------------------------------------------------------------
361// Chat — tool / response-format definitions
362// ---------------------------------------------------------------------------
363
364/// A function the model MAY call. `parameters` is a raw JSON Schema value.
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
366pub struct FunctionDef {
367    pub name: String,
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub description: Option<String>,
370    /// Raw JSON object spliced verbatim.
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub parameters: Option<serde_json::Value>,
373}
374
375/// A tool definition (`{"type":"function","function":{...}}`).
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct ToolDef {
378    #[serde(rename = "type")]
379    pub kind: String,
380    pub function: FunctionDef,
381}
382
383impl ToolDef {
384    /// A `function`-typed tool definition.
385    pub fn function(function: FunctionDef) -> Self {
386        ToolDef {
387            kind: "function".to_string(),
388            function,
389        }
390    }
391}
392
393/// Pin a specific function: `{"name":"..."}`.
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
395pub struct FunctionName {
396    pub name: String,
397}
398
399/// Named tool choice: `{"type":"function","function":{"name":"..."}}`.
400#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
401pub struct NamedToolChoice {
402    #[serde(rename = "type")]
403    pub kind: String,
404    pub function: FunctionName,
405}
406
407/// `tool_choice`: a bare mode string (`"auto"`/`"none"`/`"required"`) or a named object.
408/// Untagged so it serialises as a bare string or the object, matching the wire.
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
410#[serde(untagged)]
411pub enum ToolChoice {
412    Mode(String),
413    Named(NamedToolChoice),
414}
415
416impl ToolChoice {
417    /// `"auto"`, `"none"` or `"required"`.
418    pub fn mode(mode: impl Into<String>) -> Self {
419        ToolChoice::Mode(mode.into())
420    }
421
422    /// Pin a named function.
423    pub fn named(name: impl Into<String>) -> Self {
424        ToolChoice::Named(NamedToolChoice {
425            kind: "function".to_string(),
426            function: FunctionName { name: name.into() },
427        })
428    }
429}
430
431/// `response_format`: `{"type":"text"|"json_object"|"json_schema","json_schema":{...}}`.
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
433pub struct ResponseFormat {
434    #[serde(rename = "type")]
435    pub kind: String,
436    /// Raw JSON object, present when `type == "json_schema"`.
437    #[serde(default, skip_serializing_if = "Option::is_none")]
438    pub json_schema: Option<serde_json::Value>,
439}
440
441// ---------------------------------------------------------------------------
442// Chat — request
443// ---------------------------------------------------------------------------
444
445/// `stop`: a bare string for one element, else an array (the wire accepts either; we
446/// emit a string for a single element and an array otherwise). Untagged.
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448#[serde(untagged)]
449pub enum Stop {
450    One(String),
451    Many(Vec<String>),
452}
453
454impl Stop {
455    /// Build the wire-appropriate `stop` from a list: a bare string for exactly one
456    /// element, an array otherwise. Returns `None` for an empty list.
457    pub fn from_vec(mut v: Vec<String>) -> Option<Self> {
458        match v.len() {
459            0 => None,
460            1 => Some(Stop::One(v.pop().unwrap())),
461            _ => Some(Stop::Many(v)),
462        }
463    }
464}
465
466/// `POST /v1/chat/completions` request body.
467#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
468pub struct ChatRequest {
469    pub model: String,
470    pub messages: Vec<ChatMessage>,
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub stream: Option<bool>,
473    #[serde(default, skip_serializing_if = "Option::is_none")]
474    pub temperature: Option<f32>,
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub top_p: Option<f32>,
477    /// Legacy token cap. Prefer `max_completion_tokens`; this is still sent when the
478    /// caller set only it.
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub max_tokens: Option<u32>,
481    /// Modern token cap (takes precedence on the wire).
482    #[serde(default, skip_serializing_if = "Option::is_none")]
483    pub max_completion_tokens: Option<u32>,
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub stop: Option<Stop>,
486    #[serde(default, skip_serializing_if = "Option::is_none")]
487    pub n: Option<u32>,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub seed: Option<i64>,
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    pub frequency_penalty: Option<f32>,
492    #[serde(default, skip_serializing_if = "Option::is_none")]
493    pub presence_penalty: Option<f32>,
494    #[serde(default, skip_serializing_if = "Vec::is_empty")]
495    pub tools: Vec<ToolDef>,
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub tool_choice: Option<ToolChoice>,
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub response_format: Option<ResponseFormat>,
500    /// `"low" | "medium" | "high"`.
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub reasoning_effort: Option<String>,
503    /// Dialect-specific passthrough. Its keys are merged at the top level of the request
504    /// object (P7 transparent passthrough), so it is `#[serde(flatten)]`ed here.
505    #[serde(default, flatten, skip_serializing_if = "Option::is_none")]
506    pub extra: Option<serde_json::Map<String, serde_json::Value>>,
507}
508
509impl ChatRequest {
510    /// Start a chat request for `model` with `messages`.
511    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
512        ChatRequest {
513            model: model.into(),
514            messages,
515            ..Default::default()
516        }
517    }
518}
519
520// ---------------------------------------------------------------------------
521// Chat — non-streaming response
522// ---------------------------------------------------------------------------
523
524/// One completion choice.
525#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
526pub struct Choice {
527    #[serde(default)]
528    pub index: u32,
529    pub message: ChatMessage,
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub finish_reason: Option<FinishReason>,
532}
533
534/// `POST /v1/chat/completions` non-streaming response (`object:"chat.completion"`).
535#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
536pub struct ChatResponse {
537    pub id: String,
538    pub object: String,
539    pub created: i64,
540    pub model: String,
541    pub choices: Vec<Choice>,
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub usage: Option<Usage>,
544}
545
546impl ChatResponse {
547    /// Plain text of the first choice, if any.
548    pub fn first_text(&self) -> Option<&str> {
549        self.choices.first().and_then(|c| c.message.text())
550    }
551}
552
553// ---------------------------------------------------------------------------
554// Chat — streaming chunk
555// ---------------------------------------------------------------------------
556
557/// Incremental delta on a streaming choice.
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
559pub struct Delta {
560    /// First chunk only.
561    #[serde(default, skip_serializing_if = "Option::is_none")]
562    pub role: Option<Role>,
563    /// Incremental text.
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub content: Option<String>,
566    #[serde(default, skip_serializing_if = "Vec::is_empty")]
567    pub tool_calls: Vec<ToolCallDelta>,
568    /// Incremental open reasoning text, if any.
569    #[serde(default, skip_serializing_if = "Option::is_none")]
570    pub reasoning: Option<String>,
571    /// Incremental structured reasoning blocks (open / hidden — see [`ReasoningDetail`]).
572    #[serde(default, skip_serializing_if = "Vec::is_empty")]
573    pub reasoning_details: Vec<ReasoningDetail>,
574}
575
576/// One streaming choice.
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
578pub struct ChunkChoice {
579    #[serde(default)]
580    pub index: u32,
581    pub delta: Delta,
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub finish_reason: Option<FinishReason>,
584}
585
586/// A streaming SSE frame (`object:"chat.completion.chunk"`).
587#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
588pub struct ChatCompletionChunk {
589    pub id: String,
590    pub object: String,
591    pub created: i64,
592    pub model: String,
593    pub choices: Vec<ChunkChoice>,
594    /// Terminal chunk only.
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub usage: Option<Usage>,
597}
598
599impl ChatCompletionChunk {
600    /// The incremental text of the first choice's delta, if any.
601    pub fn first_delta_text(&self) -> Option<&str> {
602        self.choices
603            .first()
604            .and_then(|c| c.delta.content.as_deref())
605    }
606
607    /// The incremental open reasoning text of the first choice's delta, if any.
608    pub fn first_delta_reasoning(&self) -> Option<&str> {
609        self.choices
610            .first()
611            .and_then(|c| c.delta.reasoning.as_deref())
612    }
613}
614
615// ---------------------------------------------------------------------------
616// Responses (POST /v1/responses) — the OpenAI Responses dialect
617// ---------------------------------------------------------------------------
618//
619// Same canonical core as chat, a different edge dialect. The wire quirks (SPEC.md):
620// `input` is a bare string or an item array; message items are role-keyed objects with
621// NO `"type"`, every other item carries its `"type"`; tools and the named `tool_choice`
622// are FLAT (no nested `function` object); an `input_image.image_url` is a plain string;
623// reasoning `summary[]`/`content[]` entries take their `"summary_text"`/`"reasoning_text"`
624// token from the list they live in; output-text parts always carry an `annotations` array.
625
626/// The caller's answer to a function call, replayed on the next turn
627/// (`{"type":"function_call_output","call_id":"...","output":"..."}`).
628#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
629pub struct ResponseFunctionCallOutputItem {
630    #[serde(default, skip_serializing_if = "Option::is_none")]
631    pub id: Option<String>,
632    pub call_id: String,
633    pub output: String,
634}
635
636/// A function call the model made (`{"type":"function_call","call_id","name","arguments"}`).
637/// `call_id` pairs it with its [`ResponseFunctionCallOutputItem`]; `arguments` is the raw
638/// JSON string exactly as emitted.
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
640pub struct ResponseFunctionCallItem {
641    #[serde(default, skip_serializing_if = "Option::is_none")]
642    pub id: Option<String>,
643    pub call_id: String,
644    pub name: String,
645    pub arguments: String,
646    #[serde(default, skip_serializing_if = "Option::is_none")]
647    pub status: Option<String>,
648}
649
650/// One reasoning text entry. Its wire `"type"` token is decided by the list it sits in on a
651/// [`ResponseReasoningItem`]: `summary[]` → `"summary_text"`, `content[]` → `"reasoning_text"`.
652#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
653pub struct ResponseReasoningText {
654    pub text: String,
655}
656
657impl ResponseReasoningText {
658    /// Wrap a piece of reasoning text.
659    pub fn new(text: impl Into<String>) -> Self {
660        ResponseReasoningText { text: text.into() }
661    }
662}
663
664/// A reasoning ("thinking") item. `encrypted_content` is opaque and MUST be echoed back
665/// verbatim in the next request's input to continue an encrypted reasoning turn (SPEC.md).
666#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
667pub struct ResponseReasoningItem {
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub id: Option<String>,
670    /// Summarised reasoning; each entry serialises as `{"type":"summary_text","text"}`.
671    #[serde(
672        default,
673        skip_serializing_if = "Vec::is_empty",
674        serialize_with = "serialize_summary_texts"
675    )]
676    pub summary: Vec<ResponseReasoningText>,
677    /// Full reasoning; each entry serialises as `{"type":"reasoning_text","text"}`.
678    #[serde(
679        default,
680        skip_serializing_if = "Vec::is_empty",
681        serialize_with = "serialize_content_texts"
682    )]
683    pub content: Vec<ResponseReasoningText>,
684    #[serde(default, skip_serializing_if = "Option::is_none")]
685    pub encrypted_content: Option<String>,
686}
687
688/// A single content part of a Responses message. The wire `"type"` token matches the set
689/// variant; note `input_image.image_url` is a plain **string** (not the chat dialect's
690/// `{url}` object) and `output_text` always carries an `annotations` array.
691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
692#[serde(tag = "type", rename_all = "snake_case")]
693pub enum ResponseContentPart {
694    InputText {
695        text: String,
696    },
697    InputImage {
698        image_url: String,
699        #[serde(default, skip_serializing_if = "Option::is_none")]
700        detail: Option<String>,
701    },
702    OutputText {
703        text: String,
704        /// Always emitted (as `[]` when empty), matching the wire; contents are opaque.
705        #[serde(default)]
706        annotations: Vec<serde_json::Value>,
707    },
708}
709
710impl ResponseContentPart {
711    /// An `input_text` part.
712    pub fn input_text(text: impl Into<String>) -> Self {
713        ResponseContentPart::InputText { text: text.into() }
714    }
715
716    /// An `input_image` part; `image_url` is a plain string here.
717    pub fn input_image(image_url: impl Into<String>) -> Self {
718        ResponseContentPart::InputImage {
719            image_url: image_url.into(),
720            detail: None,
721        }
722    }
723
724    /// An `output_text` part; `annotations` starts empty (serialises as `[]`).
725    pub fn output_text(text: impl Into<String>) -> Self {
726        ResponseContentPart::OutputText {
727            text: text.into(),
728            annotations: Vec::new(),
729        }
730    }
731
732    /// The text of a text part (`input_text` or `output_text`), if this is one.
733    pub fn text(&self) -> Option<&str> {
734        match self {
735            ResponseContentPart::InputText { text }
736            | ResponseContentPart::OutputText { text, .. } => Some(text.as_str()),
737            ResponseContentPart::InputImage { .. } => None,
738        }
739    }
740}
741
742/// A Responses message `content`: a bare string, or an array of parts. Untagged.
743#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
744#[serde(untagged)]
745pub enum ResponseContent {
746    Text(String),
747    Parts(Vec<ResponseContentPart>),
748}
749
750impl From<String> for ResponseContent {
751    fn from(s: String) -> Self {
752        ResponseContent::Text(s)
753    }
754}
755
756impl From<&str> for ResponseContent {
757    fn from(s: &str) -> Self {
758        ResponseContent::Text(s.to_string())
759    }
760}
761
762impl From<Vec<ResponseContentPart>> for ResponseContent {
763    fn from(p: Vec<ResponseContentPart>) -> Self {
764        ResponseContent::Parts(p)
765    }
766}
767
768/// A conversation message item. On the wire it is a role-keyed object with NO `"type"`
769/// field: `{"role":"user","content":"..."}`. `id`/`status` are output-only.
770#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
771pub struct ResponseMessageItem {
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub id: Option<String>,
774    /// `"user" | "system" | "developer" | "assistant"` — a plain wire string (the dialect's
775    /// `"developer"` role collides with no chat enum, so it stays free-form, per SPEC.md).
776    pub role: String,
777    #[serde(default, skip_serializing_if = "Option::is_none")]
778    pub content: Option<ResponseContent>,
779    /// Output only: `"in_progress" | "completed"`.
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub status: Option<String>,
782}
783
784impl ResponseMessageItem {
785    fn with_role(role: &str, content: impl Into<ResponseContent>) -> Self {
786        ResponseMessageItem {
787            id: None,
788            role: role.to_string(),
789            content: Some(content.into()),
790            status: None,
791        }
792    }
793
794    /// A `user` message.
795    pub fn user(content: impl Into<ResponseContent>) -> Self {
796        Self::with_role("user", content)
797    }
798
799    /// A `system` message.
800    pub fn system(content: impl Into<ResponseContent>) -> Self {
801        Self::with_role("system", content)
802    }
803
804    /// A `developer` message (Responses-only role).
805    pub fn developer(content: impl Into<ResponseContent>) -> Self {
806        Self::with_role("developer", content)
807    }
808
809    /// An `assistant` message.
810    pub fn assistant(content: impl Into<ResponseContent>) -> Self {
811        Self::with_role("assistant", content)
812    }
813}
814
815/// One item of the request `input` array or the response `output` array. The wire
816/// discriminator is `"type"`; a message is the role-keyed object emitted with no `"type"`
817/// (and decoded from an absent or `"message"` type). [`ResponseItem::Other`] preserves any
818/// item type this SDK version does not model, verbatim.
819#[derive(Debug, Clone, PartialEq)]
820pub enum ResponseItem {
821    Message(ResponseMessageItem),
822    FunctionCall(ResponseFunctionCallItem),
823    FunctionCallOutput(ResponseFunctionCallOutputItem),
824    Reasoning(ResponseReasoningItem),
825    /// An item type not modelled by this SDK version, kept verbatim (forward compatibility).
826    Other(serde_json::Value),
827}
828
829impl ResponseItem {
830    /// A `user` message item.
831    pub fn user(content: impl Into<ResponseContent>) -> Self {
832        ResponseItem::Message(ResponseMessageItem::user(content))
833    }
834
835    /// A `system` message item.
836    pub fn system(content: impl Into<ResponseContent>) -> Self {
837        ResponseItem::Message(ResponseMessageItem::system(content))
838    }
839
840    /// A `developer` message item.
841    pub fn developer(content: impl Into<ResponseContent>) -> Self {
842        ResponseItem::Message(ResponseMessageItem::developer(content))
843    }
844
845    /// An `assistant` message item.
846    pub fn assistant(content: impl Into<ResponseContent>) -> Self {
847        ResponseItem::Message(ResponseMessageItem::assistant(content))
848    }
849
850    /// A `function_call` item to replay a tool call the model made.
851    pub fn function_call(
852        call_id: impl Into<String>,
853        name: impl Into<String>,
854        arguments: impl Into<String>,
855    ) -> Self {
856        ResponseItem::FunctionCall(ResponseFunctionCallItem {
857            id: None,
858            call_id: call_id.into(),
859            name: name.into(),
860            arguments: arguments.into(),
861            status: None,
862        })
863    }
864
865    /// A `function_call_output` item — the caller's answer to a function call.
866    pub fn function_call_output(call_id: impl Into<String>, output: impl Into<String>) -> Self {
867        ResponseItem::FunctionCallOutput(ResponseFunctionCallOutputItem {
868            id: None,
869            call_id: call_id.into(),
870            output: output.into(),
871        })
872    }
873}
874
875/// Internally-tagged, borrowing view used to serialise the typed (non-message) items:
876/// the variant name becomes the `"type"` token and the struct's fields are inlined.
877#[derive(Serialize)]
878#[serde(tag = "type", rename_all = "snake_case")]
879enum TypedItemRef<'a> {
880    FunctionCall(&'a ResponseFunctionCallItem),
881    FunctionCallOutput(&'a ResponseFunctionCallOutputItem),
882    Reasoning(&'a ResponseReasoningItem),
883}
884
885impl Serialize for ResponseItem {
886    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
887        match self {
888            // A message is a bare role-keyed object with no `"type"` (SPEC.md).
889            ResponseItem::Message(m) => m.serialize(serializer),
890            ResponseItem::FunctionCall(f) => TypedItemRef::FunctionCall(f).serialize(serializer),
891            ResponseItem::FunctionCallOutput(f) => {
892                TypedItemRef::FunctionCallOutput(f).serialize(serializer)
893            }
894            ResponseItem::Reasoning(r) => TypedItemRef::Reasoning(r).serialize(serializer),
895            ResponseItem::Other(v) => v.serialize(serializer),
896        }
897    }
898}
899
900impl<'de> Deserialize<'de> for ResponseItem {
901    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
902    where
903        D: Deserializer<'de>,
904    {
905        let value = serde_json::Value::deserialize(deserializer)?;
906        // The `"type"` token selects the variant; absent (or "message") means a message.
907        let ty = value
908            .get("type")
909            .and_then(serde_json::Value::as_str)
910            .map(str::to_owned);
911        let item = match ty.as_deref() {
912            None | Some("message") => {
913                ResponseItem::Message(serde_json::from_value(value).map_err(de::Error::custom)?)
914            }
915            Some("function_call") => {
916                ResponseItem::FunctionCall(serde_json::from_value(value).map_err(de::Error::custom)?)
917            }
918            Some("function_call_output") => ResponseItem::FunctionCallOutput(
919                serde_json::from_value(value).map_err(de::Error::custom)?,
920            ),
921            Some("reasoning") => {
922                ResponseItem::Reasoning(serde_json::from_value(value).map_err(de::Error::custom)?)
923            }
924            // An unmodelled item type is kept verbatim rather than dropped or erroring.
925            Some(_) => ResponseItem::Other(value),
926        };
927        Ok(item)
928    }
929}
930
931/// Serialise a reasoning entry with its list-derived `"type"` token.
932#[derive(Serialize)]
933struct TaggedReasoningText<'a> {
934    #[serde(rename = "type")]
935    kind: &'static str,
936    text: &'a str,
937}
938
939fn serialize_reasoning_texts<S: Serializer>(
940    texts: &[ResponseReasoningText],
941    kind: &'static str,
942    serializer: S,
943) -> Result<S::Ok, S::Error> {
944    let mut seq = serializer.serialize_seq(Some(texts.len()))?;
945    for t in texts {
946        seq.serialize_element(&TaggedReasoningText {
947            kind,
948            text: &t.text,
949        })?;
950    }
951    seq.end()
952}
953
954fn serialize_summary_texts<S: Serializer>(
955    texts: &[ResponseReasoningText],
956    serializer: S,
957) -> Result<S::Ok, S::Error> {
958    serialize_reasoning_texts(texts, "summary_text", serializer)
959}
960
961fn serialize_content_texts<S: Serializer>(
962    texts: &[ResponseReasoningText],
963    serializer: S,
964) -> Result<S::Ok, S::Error> {
965    serialize_reasoning_texts(texts, "reasoning_text", serializer)
966}
967
968/// A tool the model MAY call — FLAT in this dialect (`type`/`name`/`parameters` at the top
969/// level, no nested `function` object). `parameters` is a raw JSON Schema value.
970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
971pub struct ResponsesToolDef {
972    #[serde(rename = "type")]
973    pub kind: String,
974    pub name: String,
975    #[serde(default, skip_serializing_if = "Option::is_none")]
976    pub description: Option<String>,
977    /// Raw JSON object spliced verbatim.
978    #[serde(default, skip_serializing_if = "Option::is_none")]
979    pub parameters: Option<serde_json::Value>,
980    #[serde(default, skip_serializing_if = "Option::is_none")]
981    pub strict: Option<bool>,
982}
983
984impl ResponsesToolDef {
985    /// A `function`-typed tool definition.
986    pub fn function(name: impl Into<String>) -> Self {
987        ResponsesToolDef {
988            kind: "function".to_string(),
989            name: name.into(),
990            description: None,
991            parameters: None,
992            strict: None,
993        }
994    }
995}
996
997/// The FLAT named tool choice: `{"type":"function","name":"..."}` (no nested `function`).
998#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
999pub struct ResponsesNamedToolChoice {
1000    #[serde(rename = "type")]
1001    pub kind: String,
1002    pub name: String,
1003}
1004
1005/// `tool_choice`: a bare mode string (`"auto"`/`"none"`/`"required"`) or the flat named
1006/// object. Untagged so it serialises as a bare string or the object, matching the wire.
1007#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1008#[serde(untagged)]
1009pub enum ResponsesToolChoice {
1010    Mode(String),
1011    Named(ResponsesNamedToolChoice),
1012}
1013
1014impl ResponsesToolChoice {
1015    /// `"auto"`, `"none"` or `"required"`.
1016    pub fn mode(mode: impl Into<String>) -> Self {
1017        ResponsesToolChoice::Mode(mode.into())
1018    }
1019
1020    /// Pin a named function (flat form).
1021    pub fn named(name: impl Into<String>) -> Self {
1022        ResponsesToolChoice::Named(ResponsesNamedToolChoice {
1023            kind: "function".to_string(),
1024            name: name.into(),
1025        })
1026    }
1027}
1028
1029/// `reasoning`: `{"effort":"minimal"|"low"|"medium"|"high"|..., "summary":...}`.
1030#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1031pub struct ResponsesReasoning {
1032    #[serde(default, skip_serializing_if = "Option::is_none")]
1033    pub effort: Option<String>,
1034    #[serde(default, skip_serializing_if = "Option::is_none")]
1035    pub summary: Option<String>,
1036}
1037
1038/// `input`: a bare string (one user message) or an array of items. Untagged so it
1039/// serialises as a bare string or a bare array, matching the wire.
1040#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1041#[serde(untagged)]
1042pub enum ResponsesInput {
1043    Text(String),
1044    Items(Vec<ResponseItem>),
1045}
1046
1047impl From<String> for ResponsesInput {
1048    fn from(s: String) -> Self {
1049        ResponsesInput::Text(s)
1050    }
1051}
1052
1053impl From<&str> for ResponsesInput {
1054    fn from(s: &str) -> Self {
1055        ResponsesInput::Text(s.to_string())
1056    }
1057}
1058
1059impl From<Vec<ResponseItem>> for ResponsesInput {
1060    fn from(items: Vec<ResponseItem>) -> Self {
1061        ResponsesInput::Items(items)
1062    }
1063}
1064
1065/// `POST /v1/responses` request body.
1066#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1067pub struct ResponsesRequest {
1068    pub model: String,
1069    pub input: ResponsesInput,
1070    /// Becomes a leading system message.
1071    #[serde(default, skip_serializing_if = "Option::is_none")]
1072    pub instructions: Option<String>,
1073    #[serde(default, skip_serializing_if = "Option::is_none")]
1074    pub stream: Option<bool>,
1075    #[serde(default, skip_serializing_if = "Option::is_none")]
1076    pub temperature: Option<f32>,
1077    #[serde(default, skip_serializing_if = "Option::is_none")]
1078    pub top_p: Option<f32>,
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub max_output_tokens: Option<u32>,
1081    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1082    pub tools: Vec<ResponsesToolDef>,
1083    #[serde(default, skip_serializing_if = "Option::is_none")]
1084    pub tool_choice: Option<ResponsesToolChoice>,
1085    #[serde(default, skip_serializing_if = "Option::is_none")]
1086    pub reasoning: Option<ResponsesReasoning>,
1087    /// Accepted but always answered `false` — llmleaf stores nothing (SPEC.md).
1088    #[serde(default, skip_serializing_if = "Option::is_none")]
1089    pub store: Option<bool>,
1090    /// Dialect-specific passthrough; its keys are merged at the top level of the request
1091    /// object (P7 transparent passthrough), so it is `#[serde(flatten)]`ed here.
1092    #[serde(default, flatten, skip_serializing_if = "Option::is_none")]
1093    pub extra: Option<serde_json::Map<String, serde_json::Value>>,
1094}
1095
1096impl ResponsesRequest {
1097    /// Start a responses request for `model` with `input` (a bare string or item array).
1098    pub fn new(model: impl Into<String>, input: impl Into<ResponsesInput>) -> Self {
1099        ResponsesRequest {
1100            model: model.into(),
1101            input: input.into(),
1102            instructions: None,
1103            stream: None,
1104            temperature: None,
1105            top_p: None,
1106            max_output_tokens: None,
1107            tools: Vec::new(),
1108            tool_choice: None,
1109            reasoning: None,
1110            store: None,
1111            extra: None,
1112        }
1113    }
1114}
1115
1116/// Prompt-cache hit accounting inside [`ResponsesUsage`].
1117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1118pub struct ResponsesInputTokensDetails {
1119    #[serde(default, skip_serializing_if = "Option::is_none")]
1120    pub cached_tokens: Option<u32>,
1121}
1122
1123/// Reasoning-token accounting inside [`ResponsesUsage`].
1124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1125pub struct ResponsesOutputTokensDetails {
1126    #[serde(default, skip_serializing_if = "Option::is_none")]
1127    pub reasoning_tokens: Option<u32>,
1128}
1129
1130/// Token accounting in the Responses dialect's own names (`input_tokens`/`output_tokens`,
1131/// not the chat dialect's `prompt_tokens`/`completion_tokens`).
1132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1133pub struct ResponsesUsage {
1134    #[serde(default)]
1135    pub input_tokens: u32,
1136    #[serde(default, skip_serializing_if = "Option::is_none")]
1137    pub input_tokens_details: Option<ResponsesInputTokensDetails>,
1138    #[serde(default)]
1139    pub output_tokens: u32,
1140    #[serde(default, skip_serializing_if = "Option::is_none")]
1141    pub output_tokens_details: Option<ResponsesOutputTokensDetails>,
1142    #[serde(default)]
1143    pub total_tokens: u32,
1144}
1145
1146impl ResponsesUsage {
1147    /// Input tokens served from the provider's cache this request — a cache *read* (hit).
1148    /// `0` when the upstream reported no caching.
1149    pub fn cached_tokens(&self) -> u32 {
1150        self.input_tokens_details
1151            .as_ref()
1152            .and_then(|d| d.cached_tokens)
1153            .unwrap_or(0)
1154    }
1155
1156    /// Output tokens spent on reasoning this request. `0` when none were reported.
1157    pub fn reasoning_tokens(&self) -> u32 {
1158        self.output_tokens_details
1159            .as_ref()
1160            .and_then(|d| d.reasoning_tokens)
1161            .unwrap_or(0)
1162    }
1163}
1164
1165/// `status:"incomplete"` refinement: `"max_output_tokens" | "content_filter"`.
1166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1167pub struct ResponsesIncompleteDetails {
1168    pub reason: String,
1169}
1170
1171/// The inline error body carried on a failed [`ResponsesResponse`] (`status:"failed"`);
1172/// the proto's `ErrorBody`. Distinct from an HTTP error envelope (which surfaces as
1173/// [`crate::Error::Api`]) and from the streaming `"error"` event.
1174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1175pub struct ResponsesError {
1176    pub message: String,
1177    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1178    pub kind: Option<String>,
1179    #[serde(default, skip_serializing_if = "Option::is_none")]
1180    pub code: Option<String>,
1181}
1182
1183/// The response object (`object:"response"`), also the snapshot carried by the
1184/// `response.created` / `response.in_progress` / terminal stream events. Wire fields this
1185/// SDK does not model (tools, truncation, parallel_tool_calls, …) are ignored on decode.
1186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1187pub struct ResponsesResponse {
1188    pub id: String,
1189    /// `"response"`.
1190    pub object: String,
1191    #[serde(default)]
1192    pub created_at: i64,
1193    /// `"completed" | "in_progress" | "incomplete" | "failed"` — a plain wire string.
1194    pub status: String,
1195    #[serde(default, skip_serializing_if = "Option::is_none")]
1196    pub incomplete_details: Option<ResponsesIncompleteDetails>,
1197    #[serde(default, skip_serializing_if = "Option::is_none")]
1198    pub error: Option<ResponsesError>,
1199    #[serde(default)]
1200    pub model: String,
1201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1202    pub output: Vec<ResponseItem>,
1203    /// `null` on in-flight snapshots; set on the terminal one.
1204    #[serde(default, skip_serializing_if = "Option::is_none")]
1205    pub usage: Option<ResponsesUsage>,
1206    /// llmleaf always answers `false` (SPEC.md).
1207    #[serde(default, skip_serializing_if = "Option::is_none")]
1208    pub store: Option<bool>,
1209    #[serde(default, skip_serializing_if = "Option::is_none")]
1210    pub instructions: Option<String>,
1211    #[serde(default, skip_serializing_if = "Option::is_none")]
1212    pub max_output_tokens: Option<u32>,
1213    #[serde(default, skip_serializing_if = "Option::is_none")]
1214    pub temperature: Option<f32>,
1215    #[serde(default, skip_serializing_if = "Option::is_none")]
1216    pub top_p: Option<f32>,
1217    #[serde(default, skip_serializing_if = "Option::is_none")]
1218    pub reasoning: Option<ResponsesReasoning>,
1219}
1220
1221impl ResponsesResponse {
1222    /// The assembled assistant text: every `output_text` part of every output message,
1223    /// concatenated in order.
1224    pub fn output_text(&self) -> String {
1225        let mut out = String::new();
1226        for item in &self.output {
1227            let ResponseItem::Message(msg) = item else {
1228                continue;
1229            };
1230            match &msg.content {
1231                Some(ResponseContent::Text(t)) => out.push_str(t),
1232                Some(ResponseContent::Parts(parts)) => {
1233                    for part in parts {
1234                        if let ResponseContentPart::OutputText { text, .. } = part {
1235                            out.push_str(text);
1236                        }
1237                    }
1238                }
1239                None => {}
1240            }
1241        }
1242        out
1243    }
1244
1245    /// The function calls in `output`, in order.
1246    pub fn function_calls(&self) -> Vec<&ResponseFunctionCallItem> {
1247        self.output
1248            .iter()
1249            .filter_map(|item| match item {
1250                ResponseItem::FunctionCall(f) => Some(f),
1251                _ => None,
1252            })
1253            .collect()
1254    }
1255}
1256
1257/// One streaming SSE event. Unlike chat streaming there is NO `data: [DONE]` sentinel: the
1258/// stream ends after the terminal `response.completed` / `response.incomplete` /
1259/// `response.failed` event. This is a flat superset of every event's fields — [`kind`] says
1260/// which are meaningful; see the accessor helpers.
1261///
1262/// [`kind`]: Self::kind
1263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1264pub struct ResponsesStreamEvent {
1265    /// The event type, e.g. `"response.created"`, `"response.output_text.delta"`, `"error"`.
1266    #[serde(rename = "type")]
1267    pub kind: String,
1268    #[serde(default)]
1269    pub sequence_number: u64,
1270    /// Present on `response.created` / `response.in_progress` / the terminal events.
1271    #[serde(default, skip_serializing_if = "Option::is_none")]
1272    pub response: Option<ResponsesResponse>,
1273    #[serde(default, skip_serializing_if = "Option::is_none")]
1274    pub output_index: Option<u32>,
1275    #[serde(default, skip_serializing_if = "Option::is_none")]
1276    pub item_id: Option<String>,
1277    #[serde(default, skip_serializing_if = "Option::is_none")]
1278    pub content_index: Option<u32>,
1279    /// Present on `response.output_item.added` / `.done`.
1280    #[serde(default, skip_serializing_if = "Option::is_none")]
1281    pub item: Option<ResponseItem>,
1282    /// Present on `response.content_part.added` / `.done`.
1283    #[serde(default, skip_serializing_if = "Option::is_none")]
1284    pub part: Option<ResponseContentPart>,
1285    /// Present on `*.delta` events (text / reasoning / arguments).
1286    #[serde(default, skip_serializing_if = "Option::is_none")]
1287    pub delta: Option<String>,
1288    /// Present on `response.output_text.done` / `response.reasoning_text.done`.
1289    #[serde(default, skip_serializing_if = "Option::is_none")]
1290    pub text: Option<String>,
1291    /// Present on `response.function_call_arguments.done`.
1292    #[serde(default, skip_serializing_if = "Option::is_none")]
1293    pub arguments: Option<String>,
1294    /// Present on the `"error"` event.
1295    #[serde(default, skip_serializing_if = "Option::is_none")]
1296    pub message: Option<String>,
1297}
1298
1299impl ResponsesStreamEvent {
1300    /// Whether this is a terminal event (`response.completed` / `.incomplete` / `.failed`),
1301    /// after which the stream ends.
1302    pub fn is_terminal(&self) -> bool {
1303        matches!(
1304            self.kind.as_str(),
1305            "response.completed" | "response.incomplete" | "response.failed"
1306        )
1307    }
1308
1309    /// Whether this is the `"error"` event.
1310    pub fn is_error(&self) -> bool {
1311        self.kind == "error"
1312    }
1313
1314    /// The incremental output text of a `response.output_text.delta` event; `None` otherwise.
1315    /// Accumulate these for the assembled text.
1316    pub fn output_text_delta(&self) -> Option<&str> {
1317        (self.kind == "response.output_text.delta")
1318            .then_some(self.delta.as_deref())
1319            .flatten()
1320    }
1321
1322    /// The terminal `response` snapshot (with the full output and usage), if this is a
1323    /// terminal event carrying one.
1324    pub fn terminal_response(&self) -> Option<&ResponsesResponse> {
1325        self.is_terminal().then_some(self.response.as_ref()).flatten()
1326    }
1327
1328    /// Whether the stream parser recognises this event's namespace (`response.*` or
1329    /// `"error"`). Unrecognised events are skipped by the transport (SPEC.md forward compat).
1330    pub(crate) fn is_recognised(&self) -> bool {
1331        self.kind == "error" || self.kind.starts_with("response.")
1332    }
1333
1334    /// The message an `"error"` event carries, falling back to a failed snapshot's error body.
1335    pub(crate) fn error_message(&self) -> String {
1336        self.message
1337            .clone()
1338            .or_else(|| {
1339                self.response
1340                    .as_ref()
1341                    .and_then(|r| r.error.as_ref().map(|e| e.message.clone()))
1342            })
1343            .unwrap_or_default()
1344    }
1345}
1346
1347// ---------------------------------------------------------------------------
1348// Embeddings
1349// ---------------------------------------------------------------------------
1350
1351/// `input`: the wire accepts a bare string or an array of strings. Untagged.
1352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1353#[serde(untagged)]
1354pub enum EmbeddingInput {
1355    One(String),
1356    Many(Vec<String>),
1357}
1358
1359impl From<String> for EmbeddingInput {
1360    fn from(s: String) -> Self {
1361        EmbeddingInput::One(s)
1362    }
1363}
1364
1365impl From<&str> for EmbeddingInput {
1366    fn from(s: &str) -> Self {
1367        EmbeddingInput::One(s.to_string())
1368    }
1369}
1370
1371impl From<Vec<String>> for EmbeddingInput {
1372    fn from(v: Vec<String>) -> Self {
1373        EmbeddingInput::Many(v)
1374    }
1375}
1376
1377/// `POST /v1/embeddings` request body.
1378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1379pub struct EmbeddingRequest {
1380    pub model: String,
1381    pub input: EmbeddingInput,
1382    #[serde(default, skip_serializing_if = "Option::is_none")]
1383    pub dimensions: Option<u32>,
1384    /// `"float"` | `"base64"`. When `"base64"`, the transport decodes each embedding back
1385    /// into a float vector before returning (see SPEC.md).
1386    #[serde(default, skip_serializing_if = "Option::is_none")]
1387    pub encoding_format: Option<String>,
1388    /// Raw JSON object passthrough.
1389    #[serde(default, skip_serializing_if = "Option::is_none")]
1390    pub extra: Option<serde_json::Value>,
1391}
1392
1393impl EmbeddingRequest {
1394    /// Start an embedding request.
1395    pub fn new(model: impl Into<String>, input: impl Into<EmbeddingInput>) -> Self {
1396        EmbeddingRequest {
1397            model: model.into(),
1398            input: input.into(),
1399            dimensions: None,
1400            encoding_format: None,
1401            extra: None,
1402        }
1403    }
1404}
1405
1406/// A single embedding vector. The transport always presents `embedding` as floats, even
1407/// when the wire carried base64.
1408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1409pub struct Embedding {
1410    pub object: String,
1411    #[serde(default)]
1412    pub index: u32,
1413    pub embedding: Vec<f32>,
1414}
1415
1416/// `POST /v1/embeddings` response (`object:"list"`).
1417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1418pub struct EmbeddingResponse {
1419    pub object: String,
1420    pub data: Vec<Embedding>,
1421    pub model: String,
1422    #[serde(default, skip_serializing_if = "Option::is_none")]
1423    pub usage: Option<Usage>,
1424}
1425
1426// ---------------------------------------------------------------------------
1427// Audio — speech / voices
1428// ---------------------------------------------------------------------------
1429
1430/// `POST /v1/audio/speech` request body.
1431#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1432pub struct SpeechRequest {
1433    pub model: String,
1434    pub input: String,
1435    pub voice: String,
1436    /// `mp3|opus|aac|flac|wav|pcm`.
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub response_format: Option<String>,
1439    #[serde(default, skip_serializing_if = "Option::is_none")]
1440    pub speed: Option<f32>,
1441    /// Raw JSON object passthrough.
1442    #[serde(default, skip_serializing_if = "Option::is_none")]
1443    pub extra: Option<serde_json::Value>,
1444}
1445
1446impl SpeechRequest {
1447    /// Start a speech request.
1448    pub fn new(
1449        model: impl Into<String>,
1450        input: impl Into<String>,
1451        voice: impl Into<String>,
1452    ) -> Self {
1453        SpeechRequest {
1454            model: model.into(),
1455            input: input.into(),
1456            voice: voice.into(),
1457            response_format: None,
1458            speed: None,
1459            extra: None,
1460        }
1461    }
1462}
1463
1464/// A TTS voice.
1465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1466pub struct Voice {
1467    pub id: String,
1468    #[serde(default, skip_serializing_if = "Option::is_none")]
1469    pub name: Option<String>,
1470    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1471    pub languages: Vec<String>,
1472}
1473
1474/// `GET /v1/audio/voices` response.
1475#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1476pub struct VoicesResponse {
1477    pub model: String,
1478    pub voices: Vec<Voice>,
1479}
1480
1481// ---------------------------------------------------------------------------
1482// Audio — transcriptions (STT)
1483// ---------------------------------------------------------------------------
1484
1485/// The accompanying form fields for `POST /v1/audio/transcriptions`. The audio bytes are
1486/// the multipart `file` part and are passed separately to the SDK call.
1487#[derive(Debug, Clone, PartialEq, Default)]
1488pub struct TranscriptionRequest {
1489    pub model: String,
1490    /// ISO-639-1 hint.
1491    pub language: Option<String>,
1492    /// Decoding bias.
1493    pub prompt: Option<String>,
1494    /// `json|text|verbose_json|srt|vtt`.
1495    pub response_format: Option<String>,
1496    pub temperature: Option<f32>,
1497}
1498
1499impl TranscriptionRequest {
1500    /// Start a transcription request for `model`.
1501    pub fn new(model: impl Into<String>) -> Self {
1502        TranscriptionRequest {
1503            model: model.into(),
1504            ..Default::default()
1505        }
1506    }
1507}
1508
1509/// Structured transcription result (for `response_format` json / verbose_json).
1510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1511pub struct TranscriptionResponse {
1512    pub text: String,
1513    #[serde(default, skip_serializing_if = "Option::is_none")]
1514    pub task: Option<String>,
1515    #[serde(default, skip_serializing_if = "Option::is_none")]
1516    pub language: Option<String>,
1517    #[serde(default, skip_serializing_if = "Option::is_none")]
1518    pub duration: Option<f32>,
1519    #[serde(default, skip_serializing_if = "Option::is_none")]
1520    pub usage: Option<Usage>,
1521}
1522
1523/// Result of a transcription: a structured object (json/verbose_json) or a plain-text
1524/// body (text/srt/vtt) — SPEC.md returns text directly for the latter.
1525#[derive(Debug, Clone, PartialEq)]
1526pub enum Transcription {
1527    Json(TranscriptionResponse),
1528    Text(String),
1529}
1530
1531// ---------------------------------------------------------------------------
1532// Model catalog
1533// ---------------------------------------------------------------------------
1534
1535/// Model architecture metadata.
1536#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1537pub struct Architecture {
1538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539    pub input_modalities: Vec<String>,
1540    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1541    pub output_modalities: Vec<String>,
1542    #[serde(default, skip_serializing_if = "Option::is_none")]
1543    pub modality: Option<String>,
1544    #[serde(default)]
1545    pub tokenizer: String,
1546    #[serde(default, skip_serializing_if = "Option::is_none")]
1547    pub instruct_type: Option<String>,
1548}
1549
1550/// Per-token pricing (decimal strings, USD).
1551#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1552pub struct Pricing {
1553    pub prompt: String,
1554    pub completion: String,
1555}
1556
1557/// Top-provider capabilities for a model.
1558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1559pub struct TopProvider {
1560    #[serde(default, skip_serializing_if = "Option::is_none")]
1561    pub context_length: Option<u32>,
1562    #[serde(default, skip_serializing_if = "Option::is_none")]
1563    pub max_completion_tokens: Option<u32>,
1564    #[serde(default)]
1565    pub is_moderated: bool,
1566    #[serde(default, skip_serializing_if = "Option::is_none")]
1567    pub max_thinking_tokens: Option<u32>,
1568}
1569
1570/// Admin-only fallback-chain entry (present only with a valid `x-admin-token`).
1571#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1572pub struct ModelEndpoint {
1573    pub provider: String,
1574    pub model: String,
1575    #[serde(default)]
1576    pub down: bool,
1577    /// `"route" | "prefix"`.
1578    pub source: String,
1579}
1580
1581/// One model in the catalog.
1582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1583pub struct ModelEntry {
1584    pub id: String,
1585    #[serde(default)]
1586    pub canonical_slug: String,
1587    #[serde(default)]
1588    pub name: String,
1589    #[serde(default)]
1590    pub created: i64,
1591    #[serde(default)]
1592    pub description: String,
1593    #[serde(default, skip_serializing_if = "Option::is_none")]
1594    pub context_length: Option<u32>,
1595    #[serde(default, skip_serializing_if = "Option::is_none")]
1596    pub architecture: Option<Architecture>,
1597    #[serde(default, skip_serializing_if = "Option::is_none")]
1598    pub pricing: Option<Pricing>,
1599    #[serde(default, skip_serializing_if = "Option::is_none")]
1600    pub top_provider: Option<TopProvider>,
1601    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1602    pub supported_parameters: Vec<String>,
1603    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1604    pub unsupported_parameters: Vec<String>,
1605    /// Raw JSON object.
1606    #[serde(default, skip_serializing_if = "Option::is_none")]
1607    pub default_parameters: Option<serde_json::Value>,
1608    /// Admin-only.
1609    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1610    pub endpoints: Vec<ModelEndpoint>,
1611}
1612
1613/// `GET /v1/models` response.
1614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1615pub struct ListModelsResponse {
1616    #[serde(default)]
1617    pub data: Vec<ModelEntry>,
1618}
1619
1620/// `type` query filter for `GET /v1/models`.
1621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1622pub enum ModelType {
1623    All,
1624    Llm,
1625    Tts,
1626    Stt,
1627    Embedding,
1628}
1629
1630impl ModelType {
1631    pub(crate) fn as_str(self) -> &'static str {
1632        match self {
1633            ModelType::All => "all",
1634            ModelType::Llm => "llm",
1635            ModelType::Tts => "tts",
1636            ModelType::Stt => "stt",
1637            ModelType::Embedding => "embedding",
1638        }
1639    }
1640}
1641
1642// ---------------------------------------------------------------------------
1643// Batches
1644// ---------------------------------------------------------------------------
1645
1646/// One item in a batch create request: a `custom_id` plus a `ChatRequest` body.
1647#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1648pub struct BatchRequestItem {
1649    pub custom_id: String,
1650    pub body: ChatRequest,
1651}
1652
1653/// `POST /v1/batches` request body.
1654#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1655pub struct BatchCreateRequest {
1656    pub requests: Vec<BatchRequestItem>,
1657}
1658
1659/// Aggregate counts on a batch handle.
1660#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
1661pub struct BatchCounts {
1662    #[serde(default)]
1663    pub total: u64,
1664    #[serde(default)]
1665    pub processing: u64,
1666    #[serde(default)]
1667    pub succeeded: u64,
1668    #[serde(default)]
1669    pub errored: u64,
1670    #[serde(default)]
1671    pub canceled: u64,
1672    #[serde(default)]
1673    pub expired: u64,
1674}
1675
1676/// A batch handle returned by create / retrieve / cancel.
1677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1678pub struct BatchHandle {
1679    pub id: String,
1680    pub status: BatchStatus,
1681    #[serde(default)]
1682    pub counts: BatchCounts,
1683    #[serde(default, skip_serializing_if = "Option::is_none")]
1684    pub created_at: Option<i64>,
1685    #[serde(default, skip_serializing_if = "Option::is_none")]
1686    pub expires_at: Option<i64>,
1687    #[serde(default, skip_serializing_if = "Option::is_none")]
1688    pub ended_at: Option<i64>,
1689    #[serde(default, skip_serializing_if = "Option::is_none")]
1690    pub endpoint: Option<String>,
1691}
1692
1693/// A successful per-item batch response.
1694#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1695pub struct BatchResponse {
1696    pub status_code: u32,
1697    pub body: ChatResponse,
1698}
1699
1700/// A failed per-item batch response.
1701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1702pub struct BatchError {
1703    pub code: String,
1704    pub message: String,
1705}
1706
1707/// One line of the JSONL results stream.
1708#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1709pub struct BatchResultLine {
1710    pub custom_id: String,
1711    #[serde(default, skip_serializing_if = "Option::is_none")]
1712    pub response: Option<BatchResponse>,
1713    #[serde(default, skip_serializing_if = "Option::is_none")]
1714    pub error: Option<BatchError>,
1715}