use std::pin::Pin;
use derive_builder::Builder;
use futures::Stream;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::error::OpenAIError;
pub use async_openai::types::chat::{
ChatChoiceLogprobs,
ChatCompletionAudio,
ChatCompletionAudioFormat,
ChatCompletionAudioVoice,
ChatCompletionFunctionCall,
ChatCompletionFunctions,
ChatCompletionFunctionsArgs,
ChatCompletionRequestAssistantMessageAudio,
ChatCompletionRequestAssistantMessageContent,
ChatCompletionRequestAssistantMessageContentPart,
ChatCompletionRequestDeveloperMessage,
ChatCompletionRequestDeveloperMessageArgs,
ChatCompletionRequestDeveloperMessageContent,
ChatCompletionRequestFunctionMessage,
ChatCompletionRequestFunctionMessageArgs,
ChatCompletionRequestMessageContentPartAudio,
ChatCompletionRequestMessageContentPartRefusal,
ChatCompletionRequestMessageContentPartText,
ChatCompletionRequestSystemMessage,
ChatCompletionRequestSystemMessageArgs,
ChatCompletionRequestSystemMessageContent,
ChatCompletionRequestSystemMessageContentPart,
ChatCompletionRequestToolMessage,
ChatCompletionRequestToolMessageArgs,
ChatCompletionRequestToolMessageContent,
ChatCompletionRequestToolMessageContentPart,
ChatCompletionResponseMessageAudio,
ChatCompletionTokenLogprob,
Choice,
CompletionFinishReason,
CompletionTokensDetails,
CompletionUsage,
FunctionObject,
FunctionObjectArgs,
InputAudio,
InputAudioFormat,
Logprobs,
PredictionContent,
PredictionContentContent,
Prompt,
PromptTokensDetails,
ReasoningEffort,
ResponseFormat,
ResponseFormatJsonSchema,
Role,
ServiceTier,
TopLogprobs,
WebSearchContextSize,
WebSearchLocation,
WebSearchOptions,
WebSearchUserLocation,
WebSearchUserLocationType,
};
pub use async_openai::types::chat::StopConfiguration as Stop;
pub use async_openai::types::chat::FinishReason;
pub use async_openai::types::chat::FunctionType;
fn deserialize_arguments<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => Ok(s),
v @ serde_json::Value::Object(_) => {
Ok(serde_json::to_string(&v).unwrap())
}
other => Err(D::Error::custom(format!(
"expected string or object for `arguments`, got {other}"
))),
}
}
fn deserialize_arguments_opt<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
match value {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s)),
Some(v @ serde_json::Value::Object(_)) => serde_json::to_string(&v)
.map(Some)
.map_err(|e| D::Error::custom(e.to_string())),
Some(other) => Err(D::Error::custom(format!(
"expected string or object for `arguments`, got {other}"
))),
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
pub struct FunctionCall {
pub name: String,
#[serde(deserialize_with = "deserialize_arguments")]
pub arguments: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
pub struct FunctionCallStream {
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_arguments_opt")]
pub arguments: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
pub struct ChatCompletionMessageToolCallChunk {
pub index: u32,
pub id: Option<String>,
pub r#type: Option<FunctionType>,
pub function: Option<FunctionCallStream>,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
#[default]
Auto,
Low,
High,
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "ChatCompletionRequestMessageContentPartImageArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionRequestMessageContentPartImage {
pub image_url: ImageUrl,
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "ImageUrlArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ImageUrl {
pub url: Url,
pub detail: Option<ImageDetail>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<Uuid>,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatCompletionToolType {
#[default]
Function,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct FunctionName {
pub name: String,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionNamedToolChoice {
pub r#type: ChatCompletionToolType,
pub function: FunctionName,
}
fn default_function_type() -> FunctionType {
FunctionType::Function
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionMessageToolCall {
pub id: String,
#[serde(default = "default_function_type")]
pub r#type: FunctionType,
pub function: FunctionCall,
}
#[derive(Clone, Serialize, Default, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatCompletionToolChoiceOption {
#[default]
None,
Auto,
Required,
#[serde(untagged)]
Named(ChatCompletionNamedToolChoice),
}
#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
#[builder(name = "ChatCompletionToolArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionTool {
#[builder(default = "ChatCompletionToolType::Function")]
pub r#type: ChatCompletionToolType,
pub function: FunctionObject,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum StopReason {
String(String),
Int(i64),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum ReasoningContent {
Text(String),
Segments(Vec<String>),
}
impl ReasoningContent {
pub fn to_flat_string(&self) -> String {
match self {
ReasoningContent::Text(s) => s.clone(),
ReasoningContent::Segments(segs) => segs
.iter()
.filter(|s| !s.is_empty())
.cloned()
.collect::<Vec<_>>()
.join("\n"),
}
}
pub fn segments(&self) -> Option<&[String]> {
match self {
ReasoningContent::Segments(segs) => Some(segs),
ReasoningContent::Text(_) => None,
}
}
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionResponseContentPartText {
pub text: String,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionResponseContentPartImageUrl {
pub image_url: ImageUrlResponse,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionResponseContentPartVideoUrl {
pub video_url: VideoUrlResponse,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ChatCompletionResponseContentPartAudioUrl {
pub audio_url: AudioUrlResponse,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct ImageUrlResponse {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct VideoUrlResponse {
pub url: String,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
pub struct AudioUrlResponse {
pub url: String,
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChatCompletionResponseContentPart {
Text(ChatCompletionResponseContentPartText),
ImageUrl(ChatCompletionResponseContentPartImageUrl),
VideoUrl(ChatCompletionResponseContentPartVideoUrl),
AudioUrl(ChatCompletionResponseContentPartAudioUrl),
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ChatCompletionMessageContent {
Text(String),
Parts(Vec<ChatCompletionResponseContentPart>),
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "VideoUrlArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct VideoUrl {
pub url: Url,
pub detail: Option<ImageDetail>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "ChatCompletionRequestMessageContentPartVideoArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionRequestMessageContentPartVideo {
pub video_url: VideoUrl,
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "AudioUrlArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct AudioUrl {
pub url: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<Uuid>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Builder, PartialEq)]
#[builder(name = "ChatCompletionRequestMessageContentPartAudioUrlArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionRequestMessageContentPartAudioUrl {
pub audio_url: AudioUrl,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum ChatCompletionRequestUserMessageContent {
Text(String),
Array(Vec<ChatCompletionRequestUserMessageContentPart>),
}
#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
#[builder(name = "ChatCompletionRequestUserMessageArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionRequestUserMessage {
pub content: ChatCompletionRequestUserMessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Default for ChatCompletionRequestUserMessageContent {
fn default() -> Self {
Self::Text(String::new())
}
}
impl From<&str> for ChatCompletionRequestUserMessageContent {
fn from(value: &str) -> Self {
Self::Text(value.into())
}
}
impl From<String> for ChatCompletionRequestUserMessageContent {
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<Vec<ChatCompletionRequestUserMessageContentPart>>
for ChatCompletionRequestUserMessageContent
{
fn from(value: Vec<ChatCompletionRequestUserMessageContentPart>) -> Self {
Self::Array(value)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum ChatCompletionRequestUserMessageContentPart {
Text(ChatCompletionRequestMessageContentPartText),
ImageUrl(ChatCompletionRequestMessageContentPartImage),
VideoUrl(ChatCompletionRequestMessageContentPartVideo),
AudioUrl(ChatCompletionRequestMessageContentPartAudioUrl),
InputAudio(ChatCompletionRequestMessageContentPartAudio),
}
#[derive(Debug, Serialize, Deserialize, Default, Clone, Builder, PartialEq)]
#[builder(name = "ChatCompletionRequestAssistantMessageArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct ChatCompletionRequestAssistantMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ChatCompletionRequestAssistantMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<ReasoningContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<ChatCompletionRequestAssistantMessageAudio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
#[deprecated]
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<FunctionCall>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "role")]
#[serde(rename_all = "lowercase")]
pub enum ChatCompletionRequestMessage {
Developer(ChatCompletionRequestDeveloperMessage),
System(ChatCompletionRequestSystemMessage),
User(ChatCompletionRequestUserMessage),
Assistant(ChatCompletionRequestAssistantMessage),
Tool(ChatCompletionRequestToolMessage),
Function(ChatCompletionRequestFunctionMessage),
}
#[derive(Clone, Serialize, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ServiceTierResponse {
Scale,
Default,
Flex,
Priority,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ChatCompletionResponseMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ChatCompletionMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
#[deprecated]
pub function_call: Option<FunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<ChatCompletionResponseMessageAudio>,
pub reasoning_content: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub struct ChatCompletionStreamOptions {
pub include_usage: bool,
#[serde(default)]
pub continuous_usage_stats: bool,
}
#[derive(Clone, Serialize, Default, Debug, Builder, Deserialize, PartialEq)]
#[builder(name = "CreateChatCompletionRequestArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct CreateChatCompletionRequest {
pub messages: Vec<ChatCompletionRequestMessage>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mm_processor_kwargs: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<std::collections::HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<u8>,
#[deprecated]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_completion_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub n: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modalities: Option<Vec<async_openai::types::chat::ResponseModalities>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prediction: Option<PredictionContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<ChatCompletionAudio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Stop>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<ChatCompletionStreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ChatCompletionTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ChatCompletionToolChoiceOption>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[deprecated]
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<ChatCompletionFunctionCall>,
#[deprecated]
#[serde(skip_serializing_if = "Option::is_none")]
pub functions: Option<Vec<ChatCompletionFunctions>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub web_search_options: Option<WebSearchOptions>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ChatChoice {
pub index: u32,
pub message: ChatCompletionResponseMessage,
pub finish_reason: Option<FinishReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
pub logprobs: Option<ChatChoiceLogprobs>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
pub struct CreateChatCompletionResponse {
pub id: String,
pub choices: Vec<ChatChoice>,
pub created: u32,
pub model: String,
pub service_tier: Option<ServiceTierResponse>,
pub system_fingerprint: Option<String>,
pub object: String,
pub usage: Option<CompletionUsage>,
}
pub type ChatCompletionResponseStream =
Pin<Box<dyn Stream<Item = Result<CreateChatCompletionStreamResponse, OpenAIError>> + Send>>;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ChatCompletionStreamResponseDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ChatCompletionMessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function_call: Option<ChatCompletionStreamResponseDeltaFunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ChatCompletionMessageToolCallChunk>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct ChatCompletionStreamResponseDeltaFunctionCall {
pub name: Option<String>,
#[serde(default, deserialize_with = "deserialize_arguments_opt")]
pub arguments: Option<String>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
pub struct ChatChoiceStream {
pub index: u32,
pub delta: ChatCompletionStreamResponseDelta,
pub finish_reason: Option<FinishReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
pub logprobs: Option<ChatChoiceLogprobs>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
pub struct CreateChatCompletionStreamResponse {
pub id: String,
pub choices: Vec<ChatChoiceStream>,
pub created: u32,
pub model: String,
pub service_tier: Option<ServiceTierResponse>,
pub system_fingerprint: Option<String>,
pub object: String,
pub usage: Option<CompletionUsage>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_call_defaults_type_on_deserialize() {
let tool_call: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
"id": "call_123",
"function": {
"name": "get_weather",
"arguments": "{\"location\":\"SF\"}"
}
}))
.unwrap();
assert_eq!(tool_call.r#type, FunctionType::Function);
}
#[test]
fn tool_call_serializes_type_for_wire_compat() {
let tool_call = ChatCompletionMessageToolCall {
id: "call_123".into(),
r#type: FunctionType::Function,
function: FunctionCall {
name: "get_weather".into(),
arguments: "{\"location\":\"SF\"}".into(),
},
};
let json = serde_json::to_value(tool_call).unwrap();
assert_eq!(json["type"], "function");
}
#[test]
fn function_call_accepts_string_arguments() {
let fc: FunctionCall = serde_json::from_value(serde_json::json!({
"name": "get_weather",
"arguments": "{\"location\":\"SF\"}"
}))
.unwrap();
assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
}
#[test]
fn function_call_accepts_dict_arguments() {
let fc: FunctionCall = serde_json::from_value(serde_json::json!({
"name": "get_weather",
"arguments": {"location": "SF"}
}))
.unwrap();
assert_eq!(fc.arguments, "{\"location\":\"SF\"}");
}
#[test]
fn function_call_rejects_integer_arguments() {
let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
"name": "f",
"arguments": 42
}));
assert!(result.is_err());
}
#[test]
fn function_call_rejects_boolean_arguments() {
let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
"name": "f",
"arguments": true
}));
assert!(result.is_err());
}
#[test]
fn function_call_rejects_null_arguments() {
let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
"name": "f",
"arguments": null
}));
assert!(result.is_err());
}
#[test]
fn function_call_rejects_array_arguments() {
let result = serde_json::from_value::<FunctionCall>(serde_json::json!({
"name": "f",
"arguments": [1, 2, 3]
}));
assert!(result.is_err());
}
#[test]
fn function_call_stream_null_arguments_produces_none() {
let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
"name": "f",
"arguments": null
}))
.unwrap();
assert_eq!(fcs.arguments, None);
}
#[test]
fn function_call_stream_rejects_integer_arguments() {
let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
"name": "f",
"arguments": 42
}));
assert!(result.is_err());
}
#[test]
fn function_call_stream_rejects_boolean_arguments() {
let result = serde_json::from_value::<FunctionCallStream>(serde_json::json!({
"name": "f",
"arguments": true
}));
assert!(result.is_err());
}
#[test]
fn function_call_stream_accepts_dict_arguments() {
let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
"name": "get_weather",
"arguments": {"location": "SF"}
}))
.unwrap();
assert_eq!(fcs.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
}
#[test]
fn function_call_stream_accepts_null_arguments() {
let fcs: FunctionCallStream = serde_json::from_value(serde_json::json!({
"name": "get_weather"
}))
.unwrap();
assert_eq!(fcs.arguments, None);
}
#[test]
fn tool_call_with_dict_arguments_roundtrip() {
let tc: ChatCompletionMessageToolCall = serde_json::from_value(serde_json::json!({
"id": "call_abc",
"type": "function",
"function": {
"name": "search",
"arguments": {"query": "hello", "limit": 10}
}
}))
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap();
assert_eq!(parsed, serde_json::json!({"query": "hello", "limit": 10}));
let json = serde_json::to_value(&tc).unwrap();
assert!(json["function"]["arguments"].is_string());
}
#[test]
fn stream_delta_function_call_accepts_dict_arguments() {
let delta: ChatCompletionStreamResponseDeltaFunctionCall =
serde_json::from_value(serde_json::json!({
"name": "get_weather",
"arguments": {"location": "SF"}
}))
.unwrap();
assert_eq!(delta.arguments.as_deref(), Some("{\"location\":\"SF\"}"));
}
}