Skip to main content

cera_client/
types.rs

1//! Strongly typed request and response definitions for OpenAI and OpenRouter endpoints.
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5/// Chat message author role.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum Role {
9    /// System prompt setting instructions and context.
10    System,
11    /// Developer prompt for reasoning models (e.g. o1/o3).
12    Developer,
13    /// User input message.
14    User,
15    /// Model assistant reply.
16    Assistant,
17    /// Result returned from an executed tool call.
18    Tool,
19    /// Result returned from a legacy function call.
20    Function,
21}
22
23/// A message in a chat conversation.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct ChatMessage {
26    /// Role of the message author.
27    pub role: Role,
28
29    /// Text contents of the message. Optional when assistant returns tool calls.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub content: Option<String>,
32
33    /// Reasoning or chain-of-thought tokens from reasoning models (e.g. DeepSeek-R1, o1/o3).
34    #[serde(default, alias = "reasoning", skip_serializing_if = "Option::is_none")]
35    pub reasoning_content: Option<String>,
36
37    /// Refusal message if the model refused to respond.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub refusal: Option<String>,
40
41    /// Optional participant name.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub name: Option<String>,
44
45    /// Tool calls made by the assistant, if any.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub tool_calls: Option<Vec<ToolCall>>,
48
49    /// ID of the tool call this message is responding to (for `role = Role::Tool`).
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub tool_call_id: Option<String>,
52}
53
54impl ChatMessage {
55    /// Construct a new system message.
56    pub fn system(content: impl Into<String>) -> Self {
57        Self {
58            role: Role::System,
59            content: Some(content.into()),
60            reasoning_content: None,
61            refusal: None,
62            name: None,
63            tool_calls: None,
64            tool_call_id: None,
65        }
66    }
67
68    /// Construct a new developer instruction message for reasoning models.
69    pub fn developer(content: impl Into<String>) -> Self {
70        Self {
71            role: Role::Developer,
72            content: Some(content.into()),
73            reasoning_content: None,
74            refusal: None,
75            name: None,
76            tool_calls: None,
77            tool_call_id: None,
78        }
79    }
80
81    /// Construct a new user message.
82    pub fn user(content: impl Into<String>) -> Self {
83        Self {
84            role: Role::User,
85            content: Some(content.into()),
86            reasoning_content: None,
87            refusal: None,
88            name: None,
89            tool_calls: None,
90            tool_call_id: None,
91        }
92    }
93
94    /// Construct a new assistant message.
95    pub fn assistant(content: impl Into<String>) -> Self {
96        Self {
97            role: Role::Assistant,
98            content: Some(content.into()),
99            reasoning_content: None,
100            refusal: None,
101            name: None,
102            tool_calls: None,
103            tool_call_id: None,
104        }
105    }
106
107    /// Construct a new assistant message containing reasoning content.
108    pub fn assistant_with_reasoning(
109        content: impl Into<String>,
110        reasoning: impl Into<String>,
111    ) -> Self {
112        Self {
113            role: Role::Assistant,
114            content: Some(content.into()),
115            reasoning_content: Some(reasoning.into()),
116            refusal: None,
117            name: None,
118            tool_calls: None,
119            tool_call_id: None,
120        }
121    }
122
123    /// Construct a new assistant message containing tool calls.
124    pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
125        Self {
126            role: Role::Assistant,
127            content: None,
128            reasoning_content: None,
129            refusal: None,
130            name: None,
131            tool_calls: Some(tool_calls),
132            tool_call_id: None,
133        }
134    }
135
136    /// Construct a new tool reply message for a given tool call ID.
137    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
138        Self {
139            role: Role::Tool,
140            content: Some(content.into()),
141            reasoning_content: None,
142            refusal: None,
143            name: None,
144            tool_calls: None,
145            tool_call_id: Some(tool_call_id.into()),
146        }
147    }
148
149    /// Construct a new legacy function reply message.
150    pub fn function(name: impl Into<String>, content: impl Into<String>) -> Self {
151        Self {
152            role: Role::Function,
153            content: Some(content.into()),
154            reasoning_content: None,
155            refusal: None,
156            name: Some(name.into()),
157            tool_calls: None,
158            tool_call_id: None,
159        }
160    }
161
162    /// Set an optional participant name for the message.
163    pub fn name(mut self, name: impl Into<String>) -> Self {
164        self.name = Some(name.into());
165        self
166    }
167
168    /// Set an optional refusal message for an assistant reply.
169    pub fn refusal(mut self, refusal: impl Into<String>) -> Self {
170        self.refusal = Some(refusal.into());
171        self
172    }
173
174    /// Set the text content for the message.
175    pub fn content(mut self, content: impl Into<String>) -> Self {
176        self.content = Some(content.into());
177        self
178    }
179
180    /// Set reasoning content (thinking tokens) for the message.
181    pub fn reasoning_content(mut self, reasoning: impl Into<String>) -> Self {
182        self.reasoning_content = Some(reasoning.into());
183        self
184    }
185
186    /// Attach tool calls to the message.
187    pub fn tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
188        self.tool_calls = if tool_calls.is_empty() {
189            None
190        } else {
191            Some(tool_calls)
192        };
193        self
194    }
195}
196
197/// A tool call invoked by the model.
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct ToolCall {
200    /// Unique identifier for this tool call.
201    pub id: String,
202
203    /// Type of the tool call (usually `function`).
204    #[serde(rename = "type")]
205    pub call_type: String,
206
207    /// Function call details including name and arguments string.
208    pub function: FunctionCall,
209}
210
211impl ToolCall {
212    /// Construct a function tool call.
213    pub fn function(
214        id: impl Into<String>,
215        name: impl Into<String>,
216        arguments: impl Into<String>,
217    ) -> Self {
218        Self {
219            id: id.into(),
220            call_type: "function".to_string(),
221            function: FunctionCall::new(name, arguments),
222        }
223    }
224}
225
226/// Function invocation details in a tool call.
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct FunctionCall {
229    /// Name of the function to call.
230    pub name: String,
231
232    /// JSON string representing the arguments passed to the function.
233    pub arguments: String,
234}
235
236impl FunctionCall {
237    /// Construct a new function call with name and arguments string.
238    pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
239        Self {
240            name: name.into(),
241            arguments: arguments.into(),
242        }
243    }
244}
245
246/// Specification of a tool the model can call.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct ToolDefinition {
249    /// Type of the tool (currently `function`).
250    #[serde(rename = "type")]
251    pub tool_type: String,
252
253    /// Function definition schema.
254    pub function: FunctionDefinition,
255}
256
257impl ToolDefinition {
258    /// Construct a function tool definition.
259    pub fn function(
260        name: impl Into<String>,
261        description: Option<String>,
262        parameters: serde_json::Value,
263    ) -> Self {
264        Self {
265            tool_type: "function".to_string(),
266            function: FunctionDefinition {
267                name: name.into(),
268                description,
269                parameters,
270                strict: None,
271            },
272        }
273    }
274
275    /// Construct a function tool definition with strict schema adherence.
276    pub fn strict_function(
277        name: impl Into<String>,
278        description: Option<String>,
279        parameters: serde_json::Value,
280    ) -> Self {
281        Self {
282            tool_type: "function".to_string(),
283            function: FunctionDefinition {
284                name: name.into(),
285                description,
286                parameters,
287                strict: Some(true),
288            },
289        }
290    }
291}
292
293/// Schema definition for a callable function.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct FunctionDefinition {
296    /// Function name.
297    pub name: String,
298
299    /// Description explaining what the function does and when to call it.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub description: Option<String>,
302
303    /// JSON Schema describing the function parameter structure.
304    pub parameters: serde_json::Value,
305
306    /// Whether to enable strict schema adherence for structured outputs.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub strict: Option<bool>,
309}
310
311impl FunctionDefinition {
312    /// Create a function definition without description or strict enforcement.
313    pub fn new(name: impl Into<String>, parameters: serde_json::Value) -> Self {
314        Self {
315            name: name.into(),
316            description: None,
317            parameters,
318            strict: None,
319        }
320    }
321
322    /// Set function description.
323    pub fn description(mut self, description: impl Into<String>) -> Self {
324        self.description = Some(description.into());
325        self
326    }
327
328    /// Set strict schema mode.
329    pub fn strict(mut self, strict: bool) -> Self {
330        self.strict = Some(strict);
331        self
332    }
333}
334
335/// Request payload for `/chat/completions`.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct ChatCompletionRequest {
338    /// Model ID to query (e.g. `gpt-4o-mini`, `anthropic/claude-3.5-sonnet`).
339    pub model: String,
340
341    /// List of conversation messages.
342    pub messages: Vec<ChatMessage>,
343
344    /// Sampling temperature between 0.0 and 2.0.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub temperature: Option<f32>,
347
348    /// Nucleus sampling probability cutoff (top-p).
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub top_p: Option<f32>,
351
352    /// Maximum number of tokens to generate in the completion.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub max_tokens: Option<u32>,
355
356    /// Upper bound on completion tokens, used by reasoning models (e.g. o1/o3).
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub max_completion_tokens: Option<u32>,
359
360    /// Options for streaming responses, such as requesting token usage.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub stream_options: Option<serde_json::Value>,
363
364    /// Whether to stream back partial progress via Server-Sent Events.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub stream: Option<bool>,
367
368    /// List of tools available to the model.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub tools: Option<Vec<ToolDefinition>>,
371
372    /// Whether to enable parallel function calling during tool use.
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub parallel_tool_calls: Option<bool>,
375
376    /// Tool choice policy (`none`, `auto`, `required`, or specific function).
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub tool_choice: Option<serde_json::Value>,
379
380    /// Output formatting requirement (e.g. `{"type": "json_object"}`).
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub response_format: Option<serde_json::Value>,
383
384    /// Up to 4 sequences where the API will stop generating tokens.
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub stop: Option<Vec<String>>,
387
388    /// Presence penalty between -2.0 and 2.0.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub presence_penalty: Option<f32>,
391
392    /// Frequency penalty between -2.0 and 2.0.
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub frequency_penalty: Option<f32>,
395
396    /// Random seed for deterministic generation if supported by provider.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub seed: Option<i64>,
399
400    /// Optional end-user identifier for abuse detection.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub user: Option<String>,
403}
404
405impl ChatCompletionRequest {
406    /// Create a new chat completion request with required model and messages.
407    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
408        Self {
409            model: model.into(),
410            messages,
411            temperature: None,
412            top_p: None,
413            max_tokens: None,
414            max_completion_tokens: None,
415            stream_options: None,
416            stream: None,
417            tools: None,
418            parallel_tool_calls: None,
419            tool_choice: None,
420            response_format: None,
421            stop: None,
422            presence_penalty: None,
423            frequency_penalty: None,
424            seed: None,
425            user: None,
426        }
427    }
428
429    /// Set sampling temperature.
430    pub fn temperature(mut self, temperature: f32) -> Self {
431        self.temperature = Some(temperature);
432        self
433    }
434
435    /// Set nucleus sampling probability.
436    pub fn top_p(mut self, top_p: f32) -> Self {
437        self.top_p = Some(top_p);
438        self
439    }
440
441    /// Set maximum tokens to generate.
442    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
443        self.max_tokens = Some(max_tokens);
444        self
445    }
446
447    /// Set maximum completion tokens for reasoning models (e.g. o1/o3).
448    pub fn max_completion_tokens(mut self, max_completion_tokens: u32) -> Self {
449        self.max_completion_tokens = Some(max_completion_tokens);
450        self
451    }
452
453    /// Request token usage statistics in the final streaming chunk.
454    pub fn include_usage(mut self) -> Self {
455        self.stream_options = Some(serde_json::json!({ "include_usage": true }));
456        self
457    }
458
459    /// Set streaming flag.
460    pub fn stream(mut self, stream: bool) -> Self {
461        self.stream = Some(stream);
462        self
463    }
464
465    /// Attach tool definitions, omitting the parameter if empty.
466    pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
467        self.tools = if tools.is_empty() { None } else { Some(tools) };
468        self
469    }
470
471    /// Set whether parallel function calling is allowed during tool execution.
472    pub fn parallel_tool_calls(mut self, parallel: bool) -> Self {
473        self.parallel_tool_calls = Some(parallel);
474        self
475    }
476
477    /// Set stop sequences.
478    pub fn stop(mut self, stop: Vec<String>) -> Self {
479        self.stop = Some(stop);
480        self
481    }
482
483    /// Append a single stop sequence to the request.
484    pub fn stop_sequence(mut self, stop: impl Into<String>) -> Self {
485        let mut seqs = self.stop.unwrap_or_default();
486        seqs.push(stop.into());
487        self.stop = Some(seqs);
488        self
489    }
490
491    /// Set response format to JSON object (`{"type": "json_object"}`).
492    pub fn json_mode(mut self) -> Self {
493        self.response_format = Some(serde_json::json!({ "type": "json_object" }));
494        self
495    }
496
497    /// Set presence penalty between -2.0 and 2.0.
498    pub fn presence_penalty(mut self, presence_penalty: f32) -> Self {
499        self.presence_penalty = Some(presence_penalty);
500        self
501    }
502
503    /// Set frequency penalty between -2.0 and 2.0.
504    pub fn frequency_penalty(mut self, frequency_penalty: f32) -> Self {
505        self.frequency_penalty = Some(frequency_penalty);
506        self
507    }
508
509    /// Set random seed for deterministic sampling.
510    pub fn seed(mut self, seed: i64) -> Self {
511        self.seed = Some(seed);
512        self
513    }
514
515    /// Set end-user identifier.
516    pub fn user(mut self, user: impl Into<String>) -> Self {
517        self.user = Some(user.into());
518        self
519    }
520
521    /// Set tool choice policy (`none`, `auto`, `required`, or specific function).
522    pub fn tool_choice(mut self, tool_choice: serde_json::Value) -> Self {
523        self.tool_choice = Some(tool_choice);
524        self
525    }
526
527    /// Set arbitrary response format requirement.
528    pub fn response_format(mut self, response_format: serde_json::Value) -> Self {
529        self.response_format = Some(response_format);
530        self
531    }
532}
533
534/// Token usage details for request and response.
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
536pub struct Usage {
537    /// Tokens in the prompt.
538    pub prompt_tokens: u32,
539
540    /// Tokens generated in the completion.
541    pub completion_tokens: u32,
542
543    /// Total tokens consumed.
544    pub total_tokens: u32,
545}
546
547/// A choice generated by the model in a non-streaming completion.
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
549pub struct ChatChoice {
550    /// Choice index.
551    pub index: u32,
552
553    /// Message generated by the model.
554    pub message: ChatMessage,
555
556    /// Reason generation stopped (e.g. `stop`, `length`, `tool_calls`).
557    #[serde(default)]
558    pub finish_reason: Option<String>,
559}
560
561/// Response returned from `/chat/completions` for non-streaming requests.
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
563pub struct ChatCompletionResponse {
564    /// Unique response identifier.
565    pub id: String,
566
567    /// Object type (typically `chat.completion`).
568    #[serde(default)]
569    pub object: Option<String>,
570
571    /// Unix timestamp of completion creation.
572    pub created: u64,
573
574    /// Model that generated the completion.
575    pub model: String,
576
577    /// List of generated choices.
578    pub choices: Vec<ChatChoice>,
579
580    /// Token usage metrics, if reported by provider.
581    #[serde(default)]
582    pub usage: Option<Usage>,
583}
584
585/// Streaming chunk delta for a message.
586#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
587pub struct ChatChunkDelta {
588    /// Author role if emitted on first chunk.
589    #[serde(default)]
590    pub role: Option<Role>,
591
592    /// Generated text content increment.
593    #[serde(default)]
594    pub content: Option<String>,
595
596    /// Incremental reasoning or thinking tokens emitted by reasoning models.
597    #[serde(default, alias = "reasoning")]
598    pub reasoning_content: Option<String>,
599
600    /// Incremental refusal message if model refused to answer.
601    #[serde(default)]
602    pub refusal: Option<String>,
603
604    /// Incremental tool calls data.
605    #[serde(default)]
606    pub tool_calls: Option<Vec<ChunkToolCall>>,
607}
608
609/// Tool call delta within a streaming chunk.
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
611pub struct ChunkToolCall {
612    /// Index of the tool call in the array.
613    pub index: u32,
614
615    /// Tool call ID, usually sent in the first chunk for this tool.
616    #[serde(default)]
617    pub id: Option<String>,
618
619    /// Type string, usually `function`.
620    #[serde(rename = "type", default)]
621    pub call_type: Option<String>,
622
623    /// Incremental function name and arguments fragments.
624    #[serde(default)]
625    pub function: Option<ChunkFunctionCall>,
626}
627
628/// Function call fragments in streaming chunks.
629#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
630pub struct ChunkFunctionCall {
631    /// Name fragment, if emitted.
632    #[serde(default)]
633    pub name: Option<String>,
634
635    /// Arguments JSON fragment.
636    #[serde(default)]
637    pub arguments: Option<String>,
638}
639
640/// A choice in a streaming chunk.
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642pub struct ChatChunkChoice {
643    /// Choice index.
644    pub index: u32,
645
646    /// Incremental delta for this choice.
647    #[serde(default)]
648    pub delta: ChatChunkDelta,
649
650    /// Reason generation stopped on the final chunk.
651    #[serde(default)]
652    pub finish_reason: Option<String>,
653}
654
655/// Server-Sent Events chunk payload for streaming chat completions.
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657pub struct ChatCompletionChunk {
658    /// Unique response identifier.
659    pub id: String,
660
661    /// Object type (typically `chat.completion.chunk`).
662    #[serde(default)]
663    pub object: Option<String>,
664
665    /// Unix timestamp of chunk creation.
666    #[serde(default)]
667    pub created: u64,
668
669    /// Model name.
670    #[serde(default)]
671    pub model: String,
672
673    /// Array of choice deltas.
674    #[serde(default)]
675    pub choices: Vec<ChatChunkChoice>,
676
677    /// Token usage metrics if stream_options requested them.
678    #[serde(default)]
679    pub usage: Option<Usage>,
680}
681
682/// Input text(s) for generating vector embeddings.
683#[derive(Debug, Clone, PartialEq, Eq)]
684pub enum EmbeddingInput {
685    /// Single text prompt.
686    Single(String),
687    /// Batch of text prompts.
688    Multiple(Vec<String>),
689}
690
691impl From<&str> for EmbeddingInput {
692    fn from(s: &str) -> Self {
693        Self::Single(s.to_string())
694    }
695}
696
697impl From<String> for EmbeddingInput {
698    fn from(s: String) -> Self {
699        Self::Single(s)
700    }
701}
702
703impl From<Vec<String>> for EmbeddingInput {
704    fn from(v: Vec<String>) -> Self {
705        Self::Multiple(v)
706    }
707}
708
709impl From<Vec<&str>> for EmbeddingInput {
710    fn from(v: Vec<&str>) -> Self {
711        Self::Multiple(v.into_iter().map(String::from).collect())
712    }
713}
714
715impl From<&[&str]> for EmbeddingInput {
716    fn from(s: &[&str]) -> Self {
717        Self::Multiple(s.iter().copied().map(String::from).collect())
718    }
719}
720
721impl Serialize for EmbeddingInput {
722    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
723    where
724        S: Serializer,
725    {
726        match self {
727            Self::Single(text) => serializer.serialize_str(text),
728            Self::Multiple(texts) => texts.serialize(serializer),
729        }
730    }
731}
732
733impl<'de> Deserialize<'de> for EmbeddingInput {
734    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
735    where
736        D: Deserializer<'de>,
737    {
738        #[derive(Deserialize)]
739        #[serde(untagged)]
740        enum Helper {
741            Single(String),
742            Multiple(Vec<String>),
743        }
744        match Helper::deserialize(deserializer)? {
745            Helper::Single(s) => Ok(Self::Single(s)),
746            Helper::Multiple(v) => Ok(Self::Multiple(v)),
747        }
748    }
749}
750
751/// Request payload for `/embeddings`.
752#[derive(Debug, Clone, Serialize, Deserialize)]
753pub struct EmbeddingRequest {
754    /// Model ID (e.g. `text-embedding-3-small`).
755    pub model: String,
756
757    /// Input text or array of texts to embed.
758    pub input: EmbeddingInput,
759
760    /// Optional target dimensions for models supporting truncation.
761    #[serde(skip_serializing_if = "Option::is_none")]
762    pub dimensions: Option<u32>,
763
764    /// Optional user ID.
765    #[serde(skip_serializing_if = "Option::is_none")]
766    pub user: Option<String>,
767}
768
769impl EmbeddingRequest {
770    /// Construct a new embedding request.
771    pub fn new(model: impl Into<String>, input: impl Into<EmbeddingInput>) -> Self {
772        Self {
773            model: model.into(),
774            input: input.into(),
775            dimensions: None,
776            user: None,
777        }
778    }
779
780    /// Set dimensions.
781    pub fn dimensions(mut self, dims: u32) -> Self {
782        self.dimensions = Some(dims);
783        self
784    }
785}
786
787/// Vector embedding for a single input text item.
788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
789pub struct EmbeddingData {
790    /// Index of this embedding in the input list.
791    pub index: u32,
792
793    /// Object type (typically `embedding`).
794    pub object: String,
795
796    /// Float vector embedding.
797    pub embedding: Vec<f32>,
798}
799
800/// Response returned from `/embeddings`.
801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
802pub struct EmbeddingResponse {
803    /// Object type (`list`).
804    pub object: String,
805
806    /// List of embedding vectors.
807    pub data: Vec<EmbeddingData>,
808
809    /// Model used for embeddings.
810    pub model: String,
811
812    /// Token usage metrics.
813    #[serde(default)]
814    pub usage: Option<Usage>,
815}
816
817/// Information describing a model available on the endpoint.
818#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
819pub struct ModelInfo {
820    /// Unique model identifier (e.g. `gpt-4o`).
821    pub id: String,
822
823    /// Object type (typically `model`, optional on OpenRouter and Ollama).
824    #[serde(default)]
825    pub object: Option<String>,
826
827    /// Unix timestamp when the model was created or added.
828    #[serde(default)]
829    pub created: Option<u64>,
830
831    /// Organization or owner of the model.
832    #[serde(default)]
833    pub owned_by: Option<String>,
834}
835
836/// Response returned from `/models`.
837#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
838pub struct ListModelsResponse {
839    /// Object type (`list`, optional on some proxies).
840    #[serde(default)]
841    pub object: Option<String>,
842
843    /// List of available models.
844    pub data: Vec<ModelInfo>,
845}
846
847#[cfg(test)]
848mod tests {
849    use super::*;
850
851    #[test]
852    fn test_chat_message_constructors_and_serialization() {
853        let msg = ChatMessage::system("System prompt");
854        assert_eq!(msg.role, Role::System);
855        let json = serde_json::to_string(&msg).unwrap();
856        assert!(json.contains("\"role\":\"system\""));
857        assert!(json.contains("\"content\":\"System prompt\""));
858
859        let user_msg = ChatMessage::user("Hello");
860        assert_eq!(user_msg.role, Role::User);
861
862        let tool_msg = ChatMessage::tool("call_123", "{\"result\": 42}");
863        assert_eq!(tool_msg.role, Role::Tool);
864        assert_eq!(tool_msg.tool_call_id.as_deref(), Some("call_123"));
865
866        let assistant_refusal = ChatMessage::assistant("Cannot comply")
867            .name("safety_agent")
868            .refusal("I cannot assist with that request.");
869        assert_eq!(assistant_refusal.name.as_deref(), Some("safety_agent"));
870        assert_eq!(
871            assistant_refusal.refusal.as_deref(),
872            Some("I cannot assist with that request.")
873        );
874        let refusal_json = serde_json::to_string(&assistant_refusal).unwrap();
875        assert!(refusal_json.contains("\"name\":\"safety_agent\""));
876        assert!(refusal_json.contains("\"refusal\":\"I cannot assist with that request.\""));
877
878        let chained_assistant = ChatMessage::assistant("Here is the tool invocation:")
879            .reasoning_content("Let me check the weather.")
880            .tool_calls(vec![ToolCall::function("call_1", "get_weather", "{}")]);
881        assert_eq!(
882            chained_assistant.reasoning_content.as_deref(),
883            Some("Let me check the weather.")
884        );
885        assert_eq!(chained_assistant.tool_calls.as_ref().unwrap().len(), 1);
886    }
887
888    #[test]
889    fn test_chat_completion_request_builder() {
890        let tool = ToolDefinition::function(
891            "get_weather",
892            Some("Fetch weather for location".to_string()),
893            serde_json::json!({
894                "type": "object",
895                "properties": {
896                    "location": {"type": "string"}
897                },
898                "required": ["location"]
899            }),
900        );
901
902        let req = ChatCompletionRequest::new(
903            "gpt-4o-mini",
904            vec![ChatMessage::user("What is the weather?")],
905        )
906        .temperature(0.5)
907        .top_p(0.9)
908        .max_tokens(100)
909        .tools(vec![tool])
910        .json_mode()
911        .stop(vec!["\n".to_string()]);
912
913        let json = serde_json::to_value(&req).unwrap();
914        assert_eq!(json["model"], "gpt-4o-mini");
915        assert_eq!(json["temperature"], 0.5);
916        assert_eq!(json["max_tokens"], 100);
917        assert_eq!(json["tools"][0]["function"]["name"], "get_weather");
918        assert_eq!(json["response_format"]["type"], "json_object");
919        assert_eq!(json["stop"][0], "\n");
920
921        let empty_tools_req =
922            ChatCompletionRequest::new("gpt-4o-mini", vec![ChatMessage::user("Hi")])
923                .tools(Vec::new());
924        let empty_json = serde_json::to_value(&empty_tools_req).unwrap();
925        assert!(empty_json.get("tools").is_none());
926    }
927
928    #[test]
929    fn test_chat_completion_response_deserialization() {
930        let raw = r#"{
931            "id": "chatcmpl-123",
932            "object": "chat.completion",
933            "created": 1677652288,
934            "model": "gpt-4o-mini",
935            "choices": [{
936                "index": 0,
937                "message": {
938                    "role": "assistant",
939                    "content": "Hello there!"
940                },
941                "finish_reason": "stop"
942            }],
943            "usage": {
944                "prompt_tokens": 9,
945                "completion_tokens": 12,
946                "total_tokens": 21
947            }
948        }"#;
949
950        let res: ChatCompletionResponse = serde_json::from_str(raw).unwrap();
951        assert_eq!(res.id, "chatcmpl-123");
952        assert_eq!(res.choices.len(), 1);
953        assert_eq!(res.choices[0].finish_reason.as_deref(), Some("stop"));
954        assert_eq!(
955            res.choices[0].message.content.as_deref(),
956            Some("Hello there!")
957        );
958        assert_eq!(res.usage.unwrap().total_tokens, 21);
959    }
960
961    #[test]
962    fn test_chat_completion_chunk_deserialization() {
963        let raw = r#"{
964            "id": "chatcmpl-chunk-1",
965            "object": "chat.completion.chunk",
966            "created": 1677652288,
967            "model": "gpt-4o",
968            "choices": [{
969                "index": 0,
970                "delta": {
971                    "role": "assistant",
972                    "content": "part"
973                },
974                "finish_reason": null
975            }]
976        }"#;
977
978        let chunk: ChatCompletionChunk = serde_json::from_str(raw).unwrap();
979        assert_eq!(chunk.id, "chatcmpl-chunk-1");
980        assert_eq!(chunk.choices[0].delta.content.as_deref(), Some("part"));
981        assert_eq!(chunk.choices[0].delta.role, Some(Role::Assistant));
982    }
983
984    #[test]
985    fn test_embedding_request_and_response() {
986        let req_single =
987            EmbeddingRequest::new("text-embedding-3-small", "test text").dimensions(512);
988        let val_single = serde_json::to_value(&req_single).unwrap();
989        assert_eq!(val_single["input"], "test text");
990        assert_eq!(val_single["dimensions"], 512);
991
992        let req_multi = EmbeddingRequest::new(
993            "text-embedding-3-small",
994            vec!["item1".to_string(), "item2".to_string()],
995        );
996        let val_multi = serde_json::to_value(&req_multi).unwrap();
997        assert_eq!(val_multi["input"][0], "item1");
998        assert_eq!(val_multi["input"][1], "item2");
999
1000        let req_slice_vec =
1001            EmbeddingRequest::new("text-embedding-3-small", vec!["slice1", "slice2"]);
1002        let val_slice_vec = serde_json::to_value(&req_slice_vec).unwrap();
1003        assert_eq!(val_slice_vec["input"][0], "slice1");
1004        assert_eq!(val_slice_vec["input"][1], "slice2");
1005
1006        let items: &[&str] = &["item_a", "item_b"];
1007        let req_slice = EmbeddingRequest::new("text-embedding-3-small", items);
1008        let val_slice = serde_json::to_value(&req_slice).unwrap();
1009        assert_eq!(val_slice["input"][0], "item_a");
1010        assert_eq!(val_slice["input"][1], "item_b");
1011
1012        let raw_res = r#"{
1013            "object": "list",
1014            "data": [
1015                {
1016                    "object": "embedding",
1017                    "index": 0,
1018                    "embedding": [0.1, -0.2, 0.3]
1019                }
1020            ],
1021            "model": "text-embedding-3-small",
1022            "usage": {
1023                "prompt_tokens": 5,
1024                "total_tokens": 5,
1025                "completion_tokens": 0
1026            }
1027        }"#;
1028        let res: EmbeddingResponse = serde_json::from_str(raw_res).unwrap();
1029        assert_eq!(res.data.len(), 1);
1030        assert_eq!(res.data[0].embedding, vec![0.1, -0.2, 0.3]);
1031    }
1032
1033    #[test]
1034    fn test_models_list_deserialization() {
1035        let raw = r#"{
1036            "object": "list",
1037            "data": [
1038                {
1039                    "id": "gpt-4o",
1040                    "object": "model",
1041                    "created": 1700000000,
1042                    "owned_by": "openai"
1043                }
1044            ]
1045        }"#;
1046        let res: ListModelsResponse = serde_json::from_str(raw).unwrap();
1047        assert_eq!(res.data.len(), 1);
1048        assert_eq!(res.data[0].id, "gpt-4o");
1049    }
1050
1051    #[test]
1052    fn test_function_definition_and_role_serialization() {
1053        let msg = ChatMessage::function("calc", "42");
1054        let val = serde_json::to_value(&msg).unwrap();
1055        assert_eq!(val["role"], "function");
1056        assert_eq!(val["name"], "calc");
1057        assert_eq!(val["content"], "42");
1058
1059        let tool_def = ToolDefinition::strict_function(
1060            "get_weather",
1061            Some("Fetch current weather".to_string()),
1062            serde_json::json!({
1063                "type": "object",
1064                "properties": { "location": { "type": "string" } },
1065                "required": ["location"],
1066                "additionalProperties": false
1067            }),
1068        );
1069        let val_tool = serde_json::to_value(&tool_def).unwrap();
1070        assert_eq!(val_tool["type"], "function");
1071        assert_eq!(val_tool["function"]["name"], "get_weather");
1072        assert_eq!(val_tool["function"]["strict"], true);
1073    }
1074
1075    #[test]
1076    fn test_reasoning_content_and_request_builder_methods() {
1077        let msg = ChatMessage::assistant_with_reasoning("The answer is 42", "Let me compute 6 * 7");
1078        let val = serde_json::to_value(&msg).unwrap();
1079        assert_eq!(val["role"], "assistant");
1080        assert_eq!(val["content"], "The answer is 42");
1081        assert_eq!(val["reasoning_content"], "Let me compute 6 * 7");
1082
1083        let raw_chunk = r#"{
1084            "id": "chunk-r1",
1085            "created": 12345,
1086            "model": "deepseek-r1",
1087            "choices": [{
1088                "index": 0,
1089                "delta": {
1090                    "content": null,
1091                    "reasoning": "step 1"
1092                }
1093            }]
1094        }"#;
1095        let chunk: ChatCompletionChunk = serde_json::from_str(raw_chunk).unwrap();
1096        assert_eq!(
1097            chunk.choices[0].delta.reasoning_content.as_deref(),
1098            Some("step 1")
1099        );
1100
1101        let raw_non_streaming_reasoning = r#"{
1102            "role": "assistant",
1103            "content": "Result",
1104            "reasoning": "thought process"
1105        }"#;
1106        let non_streaming_msg: ChatMessage =
1107            serde_json::from_str(raw_non_streaming_reasoning).unwrap();
1108        assert_eq!(
1109            non_streaming_msg.reasoning_content.as_deref(),
1110            Some("thought process")
1111        );
1112
1113        let raw_chunk_omitted_delta = r#"{
1114            "id": "chunk-term",
1115            "choices": [{
1116                "index": 0,
1117                "finish_reason": "stop"
1118            }]
1119        }"#;
1120        let chunk_term: ChatCompletionChunk =
1121            serde_json::from_str(raw_chunk_omitted_delta).unwrap();
1122        assert_eq!(chunk_term.choices[0].finish_reason.as_deref(), Some("stop"));
1123        assert_eq!(chunk_term.choices[0].delta.content, None);
1124        assert_eq!(chunk_term.model, "");
1125
1126        let raw_chunk_usage_only = r#"{
1127            "id": "chunk-usage",
1128            "usage": {
1129                "prompt_tokens": 5,
1130                "completion_tokens": 10,
1131                "total_tokens": 15
1132            }
1133        }"#;
1134        let chunk_usage: ChatCompletionChunk = serde_json::from_str(raw_chunk_usage_only).unwrap();
1135        assert!(chunk_usage.choices.is_empty());
1136        assert_eq!(chunk_usage.usage.unwrap().total_tokens, 15);
1137
1138        let tool_call = ToolCall::function("call_1", "get_stock", r#"{"symbol":"AAPL"}"#);
1139        assert_eq!(tool_call.id, "call_1");
1140        assert_eq!(tool_call.call_type, "function");
1141        assert_eq!(tool_call.function.name, "get_stock");
1142
1143        let req = ChatCompletionRequest::new("gpt-4o", vec![ChatMessage::user("Hello")])
1144            .presence_penalty(0.5)
1145            .frequency_penalty(-0.2)
1146            .seed(42)
1147            .user("user_123")
1148            .parallel_tool_calls(true)
1149            .stop_sequence("END")
1150            .tool_choice(serde_json::json!("auto"))
1151            .response_format(serde_json::json!({ "type": "text" }));
1152        let val_req = serde_json::to_value(&req).unwrap();
1153        assert_eq!(val_req["presence_penalty"], 0.5);
1154        assert!((val_req["frequency_penalty"].as_f64().unwrap() - -0.2).abs() < 1e-6);
1155        assert_eq!(val_req["seed"], 42);
1156        assert_eq!(val_req["user"], "user_123");
1157        assert_eq!(val_req["parallel_tool_calls"], true);
1158        assert_eq!(val_req["stop"][0], "END");
1159        assert_eq!(val_req["tool_choice"], "auto");
1160        assert_eq!(val_req["response_format"]["type"], "text");
1161    }
1162}