From 6103828bdc11a56b98566c8a51568e3e29726755 Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Mon, 10 Mar 2025 05:30:06 +0800 Subject: [PATCH 01/10] Add json_schema to ChatCompletionResponseFormat --- src/chat.rs | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 9b366b6..905bcf3 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -277,23 +277,53 @@ pub struct VeniceParameters { pub include_venice_system_prompt: bool, } +#[derive(Serialize, Debug, Clone, Eq, PartialEq)] +pub struct ChatCompletionResponseFormatJsonSchema { + /// The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + pub name: String, + /// A description of what the response format is for, used by the model to determine how to respond in the format. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The schema for the response format, described as a JSON Schema object. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, + /// Whether to enable strict schema adherence when generating the output. + /// If set to true, the model will always follow the exact schema defined in the schema field. + /// Only a subset of JSON Schema is supported when strict is true. + /// To learn more, read the [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + /// + /// defaults to false + #[serde(skip_serializing_if = "Option::is_none")] + pub strict: Option, +} + #[derive(Serialize, Debug, Clone, Eq, PartialEq)] pub struct ChatCompletionResponseFormat { - /// Must be one of text or json_object (defaults to text) + /// Must be one of text, json_object, or json_schema (defaults to text) #[serde(rename = "type")] typ: String, + /// JSON schema for the response format + #[serde(skip_serializing_if = "Option::is_none")] + json_schema: Option, } impl ChatCompletionResponseFormat { + pub fn text() -> Self { + ChatCompletionResponseFormat { + typ: "text".to_string(), + json_schema: None, + } + } pub fn json_object() -> Self { ChatCompletionResponseFormat { typ: "json_object".to_string(), + json_schema: None, } } - - pub fn text() -> Self { + pub fn json_schema(schema: ChatCompletionResponseFormatJsonSchema) -> Self { ChatCompletionResponseFormat { - typ: "text".to_string(), + typ: "json_schema".to_string(), + json_schema: Some(schema), } } } From 5167781954789539173fe76232c7461acaaaeb2e Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Mon, 10 Mar 2025 07:20:51 +0800 Subject: [PATCH 02/10] ChatCompletionResponseFormatJsonSchema add method new --- Cargo.toml | 1 + src/chat.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 02e32d7..b1f62f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ tokio = { version = "1.26.0", features = ["full"] } anyhow = "1.0.70" futures-util = "0.3.28" bytes = "1.4.0" +schemars = "0.8.22" [dev-dependencies] dotenvy = "0.15.7" diff --git a/src/chat.rs b/src/chat.rs index 905bcf3..724e8a8 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -6,6 +6,7 @@ use derive_builder::Builder; use futures_util::StreamExt; use reqwest::Method; use reqwest_eventsource::{CannotCloneRequestError, Event, EventSource}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -297,6 +298,25 @@ pub struct ChatCompletionResponseFormatJsonSchema { pub strict: Option, } +impl ChatCompletionResponseFormatJsonSchema { + pub fn new(strict: bool) -> Self { + let mut settings = schemars::r#gen::SchemaSettings::default(); + settings.option_add_null_type = true; + settings.option_nullable = false; + settings.inline_subschemas = true; + let mut generator = schemars::SchemaGenerator::new(settings); + let mut schema = T::json_schema(&mut generator).into_object(); + let description = schema.metadata().description.clone(); + let schema = serde_json::to_value(schema).expect("unreachable"); + ChatCompletionResponseFormatJsonSchema { + name: T::schema_name(), + description, + schema: Some(schema), + strict: Some(strict), + } + } +} + #[derive(Serialize, Debug, Clone, Eq, PartialEq)] pub struct ChatCompletionResponseFormat { /// Must be one of text, json_object, or json_schema (defaults to text) From 6026fc3cea8322b3427059fee2ca28668f0109af Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Mon, 10 Mar 2025 16:15:25 +0800 Subject: [PATCH 03/10] Add tools / deprecate functions in chat --- src/chat.rs | 161 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 134 insertions(+), 27 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 724e8a8..3cf08f4 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -61,6 +61,8 @@ pub struct ChatCompletionMessage { /// The function that ChatGPT called. This should be "None" usually, and is returned by ChatGPT and not provided by the developer /// /// [API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat/create-function_call) + /// + /// Deprecated, use `tool_calls` instead #[serde(skip_serializing_if = "Option::is_none")] pub function_call: Option, /// Tool call that this message is responding to. @@ -87,6 +89,8 @@ pub struct ChatCompletionMessageDelta { /// The function that ChatGPT called /// /// [API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat/create-function_call) + /// + /// Deprecated, use `tool_calls` instead #[serde(skip_serializing_if = "Option::is_none")] pub function_call: Option, /// Tool call that this message is responding to. @@ -97,7 +101,73 @@ pub struct ChatCompletionMessageDelta { /// Can only be populated if the role is `Assistant`, /// otherwise it should be empty. #[serde(skip_serializing_if = "is_none_or_empty_vec")] - pub tool_calls: Option>, + pub tool_calls: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct ChatCompletionTool { + /// The type of the tool. Currently, only `function` is supported. + pub r#type: String, + /// The name of the tool. + pub function: ToolCallFunctionDefinition, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct ToolCallFunctionDefinition { + /// A description of what the function does, used by the model to choose when and how to call the function. + pub description: Option, + /// The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + pub name: String, + /// The parameters the functions accepts, described as a JSON Schema object. + /// See the [guide](https://platform.openai.com/docs/guides/function-calling) for examples, + /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/reference) for documentation about the format. + /// Omitting `parameters` defines a function with an empty parameter list. + pub parameters: Option, + /// Whether to enable strict schema adherence when generating the function call. + /// If set to true, the model will follow the exact schema defined in the `parameters` field. + /// Only a subset of JSON Schema is supported when `strict` is `true`. + /// Learn more about Structured Outputs in the [function calling guide](https://platform.openai.com/docs/api-reference/chat/docs/guides/function-calling). + pub strict: Option, +} + +impl ToolCallFunctionDefinition { + pub fn new(strict: Option) -> Self { + let mut settings = schemars::r#gen::SchemaSettings::default(); + settings.option_add_null_type = true; + settings.option_nullable = false; + settings.inline_subschemas = true; + let mut generator = schemars::SchemaGenerator::new(settings); + let mut schema = T::json_schema(&mut generator).into_object(); + let description = schema.metadata().description.clone(); + let schema = serde_json::to_value(schema).expect("unreachable"); + ToolCallFunctionDefinition { + description, + name: T::schema_name(), + parameters: Some(schema), + strict, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub enum ToolChoice { + /// `none` means the model will not call any tool and instead generates a message. + /// `auto` means the model can pick between generating a message or calling one or more tools. + /// `required` means the model must call one or more tools. + Mode(String), + /// The model will call the function with the given name. + Function { + /// The type of the tool. Currently, only `function` is supported. + r#type: String, + /// The function that the model called. + function: FunctionChoice, + }, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct FunctionChoice { + /// The name of the function to call. + name: String, } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] @@ -110,6 +180,17 @@ pub struct ToolCall { pub function: ToolCallFunction, } +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct ToolCallDelta { + pub index: i64, + /// The ID of the tool call. + pub id: String, + /// The type of the tool. Currently, only `function` is supported. + pub r#type: String, + /// The function that the model called. + pub function: ToolCallFunction, +} + #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] pub struct ToolCallFunction { /// The name of the function to call. @@ -166,6 +247,14 @@ pub enum ChatCompletionMessageRole { Developer, } +#[derive(Deserialize, Serialize, Debug, Clone, Copy, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ChatCompletionReasoningEffort { + Low, + Medium, + High, +} + #[derive(Serialize, Builder, Debug, Clone)] #[builder(derive(Clone, Debug, PartialEq))] #[builder(pattern = "owned")] @@ -177,6 +266,12 @@ pub struct ChatCompletionRequest { model: String, /// The messages to generate chat completions for, in the [chat format](https://platform.openai.com/docs/guides/chat/introduction). messages: Vec, + /// Constrains effort on reasoning for (reasoning models)[https://platform.openai.com/docs/guides/reasoning]. + /// Currently supported values are low, medium, and high (Defaults to medium). + /// Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + reasoning_effort: Option, /// What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. /// /// We generally recommend altering this or `top_p` but not both. @@ -205,6 +300,7 @@ pub struct ChatCompletionRequest { #[serde(skip_serializing_if = "Option::is_none")] seed: Option, /// The maximum number of tokens allowed for the generated answer. By default, the number of tokens the model can return will be (4096 - prompt tokens). + #[deprecated(note = "Use max_completion_tokens instead")] #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] max_tokens: Option, @@ -235,12 +331,32 @@ pub struct ChatCompletionRequest { #[builder(default)] #[serde(skip_serializing_if = "String::is_empty")] user: String, + /// A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. A max of 128 functions are supported. + #[builder(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + tools: Vec, + /// Controls which (if any) tool is called by the model. + /// `none` means the model will not call any tool and instead generates a message. + /// `auto` means the model can pick between generating a message or calling one or more tools. + /// `required` means the model must call one or more tools. + /// Specifying a particular tool via `{"type": "function", "function": {"name": "my_function"}}` forces the model to call that tool. + /// + /// `none` is the default when no tools are present. `auto` is the default if tools are present. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + tool_choice: Option, + /// Whether to enable parallel function calling during tool use. + /// Defaults to true. + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + parallel_tool_calls: Option, /// Describe functions that ChatGPT can call /// The latest models of ChatGPT support function calling, which allows you to define functions that can be called from the prompt. /// For example, you can define a function called "get_weather" that returns the weather in a given city /// /// [Function calling API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat/create-functions) /// [See more information about function calling in ChatGPT.](https://platform.openai.com/docs/guides/gpt/function-calling) + #[deprecated(note = "Use tools instead")] #[builder(default)] #[serde(skip_serializing_if = "Vec::is_empty")] functions: Vec, @@ -253,6 +369,7 @@ pub struct ChatCompletionRequest { /// - Specifying a particular function via {"name":\ "my_function"} forces the model to call that function. /// /// "none" is the default when no functions are present. "auto" is the default if functions are present. + #[deprecated(note = "Use tool_choice instead")] #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] function_call: Option, @@ -299,7 +416,7 @@ pub struct ChatCompletionResponseFormatJsonSchema { } impl ChatCompletionResponseFormatJsonSchema { - pub fn new(strict: bool) -> Self { + pub fn new(strict: Option) -> Self { let mut settings = schemars::r#gen::SchemaSettings::default(); settings.option_add_null_type = true; settings.option_nullable = false; @@ -312,42 +429,32 @@ impl ChatCompletionResponseFormatJsonSchema { name: T::schema_name(), description, schema: Some(schema), - strict: Some(strict), + strict, } } } #[derive(Serialize, Debug, Clone, Eq, PartialEq)] -pub struct ChatCompletionResponseFormat { - /// Must be one of text, json_object, or json_schema (defaults to text) - #[serde(rename = "type")] - typ: String, - /// JSON schema for the response format - #[serde(skip_serializing_if = "Option::is_none")] - json_schema: Option, +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChatCompletionResponseFormat { + Text, + JsonObject, + JsonSchema { + json_schema: ChatCompletionResponseFormatJsonSchema, + }, } impl ChatCompletionResponseFormat { pub fn text() -> Self { - ChatCompletionResponseFormat { - typ: "text".to_string(), - json_schema: None, - } + ChatCompletionResponseFormat::Text } pub fn json_object() -> Self { - ChatCompletionResponseFormat { - typ: "json_object".to_string(), - json_schema: None, - } + ChatCompletionResponseFormat::JsonObject } - pub fn json_schema(schema: ChatCompletionResponseFormatJsonSchema) -> Self { - ChatCompletionResponseFormat { - typ: "json_schema".to_string(), - json_schema: Some(schema), - } + pub fn json_schema(json_schema: ChatCompletionResponseFormatJsonSchema) -> Self { + ChatCompletionResponseFormat::JsonSchema { json_schema } } } - impl ChatCompletionGeneric { pub fn builder( model: &str, @@ -610,7 +717,7 @@ mod tests { }], ) .temperature(0.0) - .response_format(ChatCompletionResponseFormat::text()) + .response_format(ChatCompletionResponseFormat::Text) .credentials(credentials) .create() .await @@ -796,7 +903,7 @@ mod tests { ) .temperature(0.0) .seed(1337u64) - .response_format(ChatCompletionResponseFormat::json_object()) + .response_format(ChatCompletionResponseFormat::JsonObject) .credentials(credentials) .create() .await @@ -897,7 +1004,7 @@ mod tests { content: Some("the result is 25903.061423199997".to_string()), name: None, function_call: None, - tool_call_id: Some("the_tool_call".to_owned()), + tool_call_id: Some("the_tool_call".to_string()), tool_calls: Some(Vec::new()), }, ], From 7fc25fdb8dd62dac1edf4541c3f7ef7e5f04001f Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Tue, 11 Mar 2025 05:05:41 +0800 Subject: [PATCH 04/10] fix: need all fields to be required to use structured outputs --- src/chat.rs | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 3cf08f4..d073e72 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -130,8 +130,41 @@ pub struct ToolCallFunctionDefinition { pub strict: Option, } +/// To use Structured Outputs, all fields or function parameters must be specified as `required`. +pub fn add_required(schema: &mut Value) { + match schema { + Value::Array(arr) => { + arr.iter_mut().for_each(|v| add_required(v)); + } + Value::Object(obj) => { + if let Some(properties) = obj.get("properties") { + match properties { + Value::Object(p) => { + let required = p + .iter() + .map(|(r, _)| Value::String(r.clone())) + .collect::>(); + obj.insert("required".to_string(), Value::Array(required)); + if obj.get("additionalProperties").is_none() { + obj.insert( + "additionalProperties".to_string(), + Value::Bool(false), + ); + } + } + _ => {} + } + } + for (_, v) in obj.iter_mut() { + add_required(v); + } + } + _ => {} + } +} + impl ToolCallFunctionDefinition { - pub fn new(strict: Option) -> Self { + pub fn new(strict: bool) -> Self { let mut settings = schemars::r#gen::SchemaSettings::default(); settings.option_add_null_type = true; settings.option_nullable = false; @@ -139,12 +172,13 @@ impl ToolCallFunctionDefinition { let mut generator = schemars::SchemaGenerator::new(settings); let mut schema = T::json_schema(&mut generator).into_object(); let description = schema.metadata().description.clone(); - let schema = serde_json::to_value(schema).expect("unreachable"); + let mut schema = serde_json::to_value(schema).expect("unreachable"); + add_required(&mut schema); ToolCallFunctionDefinition { description, name: T::schema_name(), parameters: Some(schema), - strict, + strict: Some(strict), } } } @@ -416,7 +450,7 @@ pub struct ChatCompletionResponseFormatJsonSchema { } impl ChatCompletionResponseFormatJsonSchema { - pub fn new(strict: Option) -> Self { + pub fn new(strict: bool) -> Self { let mut settings = schemars::r#gen::SchemaSettings::default(); settings.option_add_null_type = true; settings.option_nullable = false; @@ -424,12 +458,13 @@ impl ChatCompletionResponseFormatJsonSchema { let mut generator = schemars::SchemaGenerator::new(settings); let mut schema = T::json_schema(&mut generator).into_object(); let description = schema.metadata().description.clone(); - let schema = serde_json::to_value(schema).expect("unreachable"); + let mut schema = serde_json::to_value(schema).expect("unreachable"); + add_required(&mut schema); ChatCompletionResponseFormatJsonSchema { name: T::schema_name(), description, schema: Some(schema), - strict, + strict: Some(strict), } } } From 13d6926cfdd7934cc16e97ab675decc34e2f874a Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Wed, 12 Mar 2025 17:12:50 +0800 Subject: [PATCH 05/10] fix: adjust structured output schema and pass added tests --- src/chat.rs | 255 ++++++++++++++++++---------------- src/chat/structured_output.rs | 177 +++++++++++++++++++++++ 2 files changed, 313 insertions(+), 119 deletions(-) create mode 100644 src/chat/structured_output.rs diff --git a/src/chat.rs b/src/chat.rs index d073e72..e92af09 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,4 +1,5 @@ //! Given a chat conversation, the model will return a chat completion response. +pub mod structured_output; use super::{openai_post, ApiResponseOrError, Credentials, Usage}; use crate::openai_request_stream; @@ -10,6 +11,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use structured_output::{ + ChatCompletionResponseFormatJsonSchema, JsonSchemaStyle, ToolCallFunctionDefinition, +}; use tokio::sync::mpsc::{channel, Receiver, Sender}; /// A full chat completion. @@ -105,111 +109,78 @@ pub struct ChatCompletionMessageDelta { } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] -pub struct ChatCompletionTool { - /// The type of the tool. Currently, only `function` is supported. - pub r#type: String, - /// The name of the tool. - pub function: ToolCallFunctionDefinition, +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChatCompletionTool { + Function { + function: ToolCallFunctionDefinition, + }, +} + +impl ChatCompletionTool { + pub fn new(strict: bool, json_style: JsonSchemaStyle) -> Self { + let function = ToolCallFunctionDefinition::new::(strict, json_style); + ChatCompletionTool::Function { function } + } } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] -pub struct ToolCallFunctionDefinition { - /// A description of what the function does, used by the model to choose when and how to call the function. - pub description: Option, - /// The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. - pub name: String, - /// The parameters the functions accepts, described as a JSON Schema object. - /// See the [guide](https://platform.openai.com/docs/guides/function-calling) for examples, - /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/reference) for documentation about the format. - /// Omitting `parameters` defines a function with an empty parameter list. - pub parameters: Option, - /// Whether to enable strict schema adherence when generating the function call. - /// If set to true, the model will follow the exact schema defined in the `parameters` field. - /// Only a subset of JSON Schema is supported when `strict` is `true`. - /// Learn more about Structured Outputs in the [function calling guide](https://platform.openai.com/docs/api-reference/chat/docs/guides/function-calling). - pub strict: Option, +pub enum ToolChoiceMode { + None, + Auto, + Required, } -/// To use Structured Outputs, all fields or function parameters must be specified as `required`. -pub fn add_required(schema: &mut Value) { - match schema { - Value::Array(arr) => { - arr.iter_mut().for_each(|v| add_required(v)); - } - Value::Object(obj) => { - if let Some(properties) = obj.get("properties") { - match properties { - Value::Object(p) => { - let required = p - .iter() - .map(|(r, _)| Value::String(r.clone())) - .collect::>(); - obj.insert("required".to_string(), Value::Array(required)); - if obj.get("additionalProperties").is_none() { - obj.insert( - "additionalProperties".to_string(), - Value::Bool(false), - ); - } - } - _ => {} - } - } - for (_, v) in obj.iter_mut() { - add_required(v); - } - } - _ => {} - } +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct FunctionChoice { + /// The name of the function to call. + name: String, } -impl ToolCallFunctionDefinition { - pub fn new(strict: bool) -> Self { - let mut settings = schemars::r#gen::SchemaSettings::default(); - settings.option_add_null_type = true; - settings.option_nullable = false; - settings.inline_subschemas = true; - let mut generator = schemars::SchemaGenerator::new(settings); - let mut schema = T::json_schema(&mut generator).into_object(); - let description = schema.metadata().description.clone(); - let mut schema = serde_json::to_value(schema).expect("unreachable"); - add_required(&mut schema); - ToolCallFunctionDefinition { - description, - name: T::schema_name(), - parameters: Some(schema), - strict: Some(strict), +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FunctionLiteral; +impl Serialize for FunctionLiteral { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str("function") + } +} +impl<'de> Deserialize<'de> for FunctionLiteral { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: &str = serde::Deserialize::deserialize(deserializer)?; + if s != "function" { + return Err(serde::de::Error::custom("expected function")); } + Ok(FunctionLiteral) } } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +#[serde(untagged)] pub enum ToolChoice { /// `none` means the model will not call any tool and instead generates a message. /// `auto` means the model can pick between generating a message or calling one or more tools. /// `required` means the model must call one or more tools. - Mode(String), + Mode(ToolChoiceMode), /// The model will call the function with the given name. Function { /// The type of the tool. Currently, only `function` is supported. - r#type: String, + r#type: FunctionLiteral, /// The function that the model called. function: FunctionChoice, }, } -#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] -pub struct FunctionChoice { - /// The name of the function to call. - name: String, -} - #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] pub struct ToolCall { /// The ID of the tool call. pub id: String, /// The type of the tool. Currently, only `function` is supported. - pub r#type: String, + pub r#type: FunctionLiteral, /// The function that the model called. pub function: ToolCallFunction, } @@ -220,7 +191,7 @@ pub struct ToolCallDelta { /// The ID of the tool call. pub id: String, /// The type of the tool. Currently, only `function` is supported. - pub r#type: String, + pub r#type: FunctionLiteral, /// The function that the model called. pub function: ToolCallFunction, } @@ -429,46 +400,6 @@ pub struct VeniceParameters { pub include_venice_system_prompt: bool, } -#[derive(Serialize, Debug, Clone, Eq, PartialEq)] -pub struct ChatCompletionResponseFormatJsonSchema { - /// The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. - pub name: String, - /// A description of what the response format is for, used by the model to determine how to respond in the format. - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - /// The schema for the response format, described as a JSON Schema object. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema: Option, - /// Whether to enable strict schema adherence when generating the output. - /// If set to true, the model will always follow the exact schema defined in the schema field. - /// Only a subset of JSON Schema is supported when strict is true. - /// To learn more, read the [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). - /// - /// defaults to false - #[serde(skip_serializing_if = "Option::is_none")] - pub strict: Option, -} - -impl ChatCompletionResponseFormatJsonSchema { - pub fn new(strict: bool) -> Self { - let mut settings = schemars::r#gen::SchemaSettings::default(); - settings.option_add_null_type = true; - settings.option_nullable = false; - settings.inline_subschemas = true; - let mut generator = schemars::SchemaGenerator::new(settings); - let mut schema = T::json_schema(&mut generator).into_object(); - let description = schema.metadata().description.clone(); - let mut schema = serde_json::to_value(schema).expect("unreachable"); - add_required(&mut schema); - ChatCompletionResponseFormatJsonSchema { - name: T::schema_name(), - description, - schema: Some(schema), - strict: Some(strict), - } - } -} - #[derive(Serialize, Debug, Clone, Eq, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ChatCompletionResponseFormat { @@ -486,10 +417,12 @@ impl ChatCompletionResponseFormat { pub fn json_object() -> Self { ChatCompletionResponseFormat::JsonObject } - pub fn json_schema(json_schema: ChatCompletionResponseFormatJsonSchema) -> Self { + pub fn json_schema(strict: bool, json_style: JsonSchemaStyle) -> Self { + let json_schema = ChatCompletionResponseFormatJsonSchema::new::(strict, json_style); ChatCompletionResponseFormat::JsonSchema { json_schema } } } + impl ChatCompletionGeneric { pub fn builder( model: &str, @@ -996,6 +929,90 @@ mod tests { merged.unwrap().into() } + #[derive(JsonSchema, Deserialize, Debug, Eq, PartialEq)] + enum Race { + Black, + White, + Asian, + Other(String), + } + #[derive(JsonSchema, Deserialize, Debug, PartialEq)] + enum Species { + Human(Race), + Orc { color: String, leader: String }, + } + #[derive(JsonSchema, Deserialize, Debug, PartialEq)] + struct Character { + pub name: String, + pub age: i64, + pub power: f64, + pub skills: Vec, + pub species: Species, + } + #[derive(JsonSchema, Deserialize, Debug, PartialEq)] + struct Skill { + pub name: String, + pub description: Option, + pub dont_use_this_property: Option, + } + + #[tokio::test] + async fn chat_structured_output_completion() { + dotenv().ok(); + let credentials = Credentials::from_env(); + + let format = + ChatCompletionResponseFormat::json_schema::(true, JsonSchemaStyle::OpenAI); + let chat_completion = ChatCompletion::builder( + "gpt-4o-mini", + [ChatCompletionMessage { + role: ChatCompletionMessageRole::User, + content: Some( + "Create a DND character, don't use the dont_use_this_property field" + .to_string(), + ), + ..Default::default() + }], + ) + .credentials(credentials) + .response_format(format) + .create() + .await + .unwrap(); + let character_str = chat_completion.choices[0].message.content.as_ref().unwrap(); + let _character: Character = serde_json::from_str(character_str).unwrap(); + } + + #[tokio::test] + async fn chat_tool_use_completion() { + dotenv().ok(); + let credentials = Credentials::from_env(); + let schema = ChatCompletionTool::new::(true, JsonSchemaStyle::OpenAI); + let chat_completion = ChatCompletion::builder( + "gpt-4o-mini", + [ChatCompletionMessage { + role: ChatCompletionMessageRole::User, + content: Some("create a random DND character directly with tools".to_string()), + ..Default::default() + }], + ) + .credentials(credentials) + .tools(vec![schema]) + .tool_choice(ToolChoice::Function { + r#type: FunctionLiteral, + function: FunctionChoice { + name: "Character".to_string(), + }, + }) + .create() + .await + .unwrap(); + let msg = chat_completion.choices[0].message.clone(); + let tool_calls = msg.tool_calls.as_ref(); + let tool_call: &ToolCall = tool_calls.unwrap().first().unwrap(); + let _character: Character = serde_json::from_str(&tool_call.function.arguments).unwrap(); + } + #[tokio::test] async fn chat_tool_response_completion() { dotenv().ok(); @@ -1027,7 +1044,7 @@ mod tests { tool_call_id: None, tool_calls: Some(vec![ToolCall { id: "the_tool_call".to_string(), - r#type: "function".to_string(), + r#type: FunctionLiteral, function: ToolCallFunction { name: "mul".to_string(), arguments: "not_required_to_be_valid_here".to_string(), diff --git a/src/chat/structured_output.rs b/src/chat/structured_output.rs new file mode 100644 index 0000000..b0adea5 --- /dev/null +++ b/src/chat/structured_output.rs @@ -0,0 +1,177 @@ +use std::mem::take; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum JsonSchemaStyle { + OpenAI, + Grok, +} + +#[derive(Serialize, Debug, Clone, Eq, PartialEq)] +pub struct ChatCompletionResponseFormatJsonSchema { + /// The name of the response format. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + pub name: String, + /// A description of what the response format is for, used by the model to determine how to respond in the format. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The schema for the response format, described as a JSON Schema object. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, + /// Whether to enable strict schema adherence when generating the output. + /// If set to true, the model will always follow the exact schema defined in the schema field. + /// Only a subset of JSON Schema is supported when strict is true. + /// To learn more, read the [Structured Outputs guide](https://platform.openai.com/docs/guides/structured-outputs). + /// + /// defaults to false + #[serde(skip_serializing_if = "Option::is_none")] + pub strict: Option, +} + +impl ChatCompletionResponseFormatJsonSchema { + pub fn new(strict: bool, json_style: JsonSchemaStyle) -> Self { + let (schema, description) = generate_json_schema::(json_style); + ChatCompletionResponseFormatJsonSchema { + name: T::schema_name(), + description, + schema: Some(schema), + strict: Some(strict), + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] +pub struct ToolCallFunctionDefinition { + /// The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + pub name: String, + /// A description of what the function does, used by the model to choose when and how to call the function. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// The parameters the functions accepts, described as a JSON Schema object. + /// See the [guide](https://platform.openai.com/docs/guides/function-calling) for examples, + /// and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/reference) for documentation about the format. + /// Omitting `parameters` defines a function with an empty parameter list. + #[serde(skip_serializing_if = "Option::is_none")] + pub parameters: Option, + /// Whether to enable strict schema adherence when generating the function call. + /// If set to true, the model will follow the exact schema defined in the `parameters` field. + /// Only a subset of JSON Schema is supported when `strict` is `true`. + /// Learn more about Structured Outputs in the [function calling guide](https://platform.openai.com/docs/api-reference/chat/docs/guides/function-calling). + /// + /// defaults to false + #[serde(skip_serializing_if = "Option::is_none")] + pub strict: Option, +} + +impl ToolCallFunctionDefinition { + /// Create a new ToolCallFunctionDefinition with the given strictness and JSON Schema style. + /// + /// Note: Grok does not support strict schema adherence. + pub fn new(strict: bool, json_style: JsonSchemaStyle) -> Self { + let (schema, description) = generate_json_schema::(json_style); + let strict = match json_style { + JsonSchemaStyle::OpenAI => Some(strict), + JsonSchemaStyle::Grok => None, + }; + ToolCallFunctionDefinition { + description, + name: T::schema_name(), + parameters: Some(schema), + strict, + } + } +} + +fn structured_output_post_process(schema: &mut Value, style: JsonSchemaStyle) { + let obj = match schema { + Value::Object(obj) => obj, + _ => return, + }; + // OpenAI uses `anyOf` instead of `oneOf` + if let Some(v) = obj.remove("oneOf") { + obj.insert("anyOf".to_string(), v); + } + if let Some(Value::Array(objs)) = obj.get_mut("anyOf") { + for v in objs.iter_mut() { + structured_output_post_process(v, style); + } + } + let ty = match obj.get("type") { + Some(Value::String(s)) => s, + _ => { + return; + } + }; + match ty.as_str() { + "array" => { + if let Some(v) = obj.get_mut("items") { + structured_output_post_process(v, style); + } + } + "object" => { + let properties = if let Some(Value::Object(p)) = obj.get_mut("properties") { + p + } else { + return; + }; + let mut required = Vec::new(); + for (k, v) in properties.iter_mut() { + // v must be a json schema object + structured_output_post_process(v, style); + required.push(Value::String(k.clone())); + } + if style == JsonSchemaStyle::OpenAI { + // OpenAI: All fields or function parameters must be specified as `required`; + obj.insert("required".to_string(), Value::Array(required)); + // OpenAI: Need to add `additionalProperties`; + if obj.get("additionalProperties").is_none() { + obj.insert("additionalProperties".to_string(), Value::Bool(false)); + } + } + } + "string" => { + *obj = take(obj) + .into_iter() + .filter(|(k, _)| ["type", "enum"].contains(&k.as_str())) + .collect(); + } + "number" => { + // Remove constraints like `multipleOf` for floating point types; + *obj = take(obj) + .into_iter() + .filter(|(k, _)| ["type"].contains(&k.as_str())) + .collect(); + } + "integer" => { + // Remove constraints like `format` and `minimum` for integer types; + *obj = take(obj) + .into_iter() + .filter(|(k, _)| ["type"].contains(&k.as_str())) + .collect(); + } + _ => {} + } +} + +/// Generate a JSON Schema with the given style. +/// +/// IMPORTANT: Both OpenAI and Grok do not support the `format` and `minimum` JSON Schema attributes. +/// As a result, numeric type constraints (like `u8`, `i32`, etc) cannot be enforced - all integers +/// will be treated as `i64` and all floating point numbers as `f64`. +pub fn generate_json_schema(json_style: JsonSchemaStyle) -> (Value, Option) { + let mut settings = schemars::r#gen::SchemaSettings::default(); + settings.option_nullable = false; + settings.inline_subschemas = true; + settings.option_add_null_type = match json_style { + JsonSchemaStyle::OpenAI => true, + JsonSchemaStyle::Grok => false, + }; + let mut generator = schemars::SchemaGenerator::new(settings); + let mut schema = T::json_schema(&mut generator).into_object(); + let description = schema.metadata().description.clone(); + let mut schema = serde_json::to_value(schema).expect("unreachable"); + structured_output_post_process(&mut schema, json_style); + (schema, description) +} From 7e3099a91dbaceeed564285baf2b7209e4220c6e Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Thu, 13 Mar 2025 13:21:58 +0800 Subject: [PATCH 06/10] refactor: use schemars visitor to post process --- src/chat/structured_output.rs | 116 ++++++++++++---------------------- 1 file changed, 42 insertions(+), 74 deletions(-) diff --git a/src/chat/structured_output.rs b/src/chat/structured_output.rs index b0adea5..1a5c901 100644 --- a/src/chat/structured_output.rs +++ b/src/chat/structured_output.rs @@ -1,6 +1,10 @@ use std::mem::take; -use schemars::JsonSchema; +use schemars::{ + schema::{Schema, SchemaObject}, + visit::{visit_schema_object, Visitor}, + JsonSchema, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -84,77 +88,6 @@ impl ToolCallFunctionDefinition { } } -fn structured_output_post_process(schema: &mut Value, style: JsonSchemaStyle) { - let obj = match schema { - Value::Object(obj) => obj, - _ => return, - }; - // OpenAI uses `anyOf` instead of `oneOf` - if let Some(v) = obj.remove("oneOf") { - obj.insert("anyOf".to_string(), v); - } - if let Some(Value::Array(objs)) = obj.get_mut("anyOf") { - for v in objs.iter_mut() { - structured_output_post_process(v, style); - } - } - let ty = match obj.get("type") { - Some(Value::String(s)) => s, - _ => { - return; - } - }; - match ty.as_str() { - "array" => { - if let Some(v) = obj.get_mut("items") { - structured_output_post_process(v, style); - } - } - "object" => { - let properties = if let Some(Value::Object(p)) = obj.get_mut("properties") { - p - } else { - return; - }; - let mut required = Vec::new(); - for (k, v) in properties.iter_mut() { - // v must be a json schema object - structured_output_post_process(v, style); - required.push(Value::String(k.clone())); - } - if style == JsonSchemaStyle::OpenAI { - // OpenAI: All fields or function parameters must be specified as `required`; - obj.insert("required".to_string(), Value::Array(required)); - // OpenAI: Need to add `additionalProperties`; - if obj.get("additionalProperties").is_none() { - obj.insert("additionalProperties".to_string(), Value::Bool(false)); - } - } - } - "string" => { - *obj = take(obj) - .into_iter() - .filter(|(k, _)| ["type", "enum"].contains(&k.as_str())) - .collect(); - } - "number" => { - // Remove constraints like `multipleOf` for floating point types; - *obj = take(obj) - .into_iter() - .filter(|(k, _)| ["type"].contains(&k.as_str())) - .collect(); - } - "integer" => { - // Remove constraints like `format` and `minimum` for integer types; - *obj = take(obj) - .into_iter() - .filter(|(k, _)| ["type"].contains(&k.as_str())) - .collect(); - } - _ => {} - } -} - /// Generate a JSON Schema with the given style. /// /// IMPORTANT: Both OpenAI and Grok do not support the `format` and `minimum` JSON Schema attributes. @@ -171,7 +104,42 @@ pub fn generate_json_schema(json_style: JsonSchemaStyle) -> (Valu let mut generator = schemars::SchemaGenerator::new(settings); let mut schema = T::json_schema(&mut generator).into_object(); let description = schema.metadata().description.clone(); - let mut schema = serde_json::to_value(schema).expect("unreachable"); - structured_output_post_process(&mut schema, json_style); + let mut processor = SchemaPostProcessor { style: json_style }; + processor.visit_schema_object(&mut schema); + let schema = serde_json::to_value(schema).expect("unreachable"); (schema, description) } + +pub struct SchemaPostProcessor { + pub style: JsonSchemaStyle, +} + +impl Visitor for SchemaPostProcessor { + fn visit_schema_object(&mut self, schema: &mut SchemaObject) { + if let Some(sub) = &mut schema.subschemas { + sub.any_of = take(&mut sub.one_of); + } + schema.format = None; + if let Some(sub) = &mut schema.object { + if self.style == JsonSchemaStyle::OpenAI { + if sub.additional_properties.is_none() { + sub.additional_properties = Some(Box::new(Schema::Bool(false))); + } + sub.required = sub.properties.keys().map(|s| s.clone()).collect(); + } + } + if let Some(num) = &mut schema.number { + num.multiple_of = None; + num.exclusive_maximum = None; + num.exclusive_minimum = None; + num.maximum = None; + num.minimum = None; + } + if let Some(str) = &mut schema.string { + str.max_length = None; + str.min_length = None; + str.pattern = None; + } + visit_schema_object(self, schema); + } +} From 77cce34de4b4152f9b91ed2de3effa31516cd306 Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Sat, 15 Mar 2025 18:48:31 +0800 Subject: [PATCH 07/10] fix: tool definitions don't need to have restrictions --- src/chat.rs | 20 ++++++++++++++++---- src/chat/structured_output.rs | 18 ++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e92af09..e81214c 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -117,8 +117,8 @@ pub enum ChatCompletionTool { } impl ChatCompletionTool { - pub fn new(strict: bool, json_style: JsonSchemaStyle) -> Self { - let function = ToolCallFunctionDefinition::new::(strict, json_style); + pub fn new(strict: Option) -> Self { + let function = ToolCallFunctionDefinition::new::(strict); ChatCompletionTool::Function { function } } } @@ -133,7 +133,7 @@ pub enum ToolChoiceMode { #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] pub struct FunctionChoice { /// The name of the function to call. - name: String, + pub name: String, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -175,6 +175,18 @@ pub enum ToolChoice { }, } +impl ToolChoice { + pub fn mode(mode: ToolChoiceMode) -> Self { + ToolChoice::Mode(mode) + } + pub fn function(name: String) -> Self { + ToolChoice::Function { + r#type: FunctionLiteral, + function: FunctionChoice { name }, + } + } +} + #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] pub struct ToolCall { /// The ID of the tool call. @@ -987,7 +999,7 @@ mod tests { async fn chat_tool_use_completion() { dotenv().ok(); let credentials = Credentials::from_env(); - let schema = ChatCompletionTool::new::(true, JsonSchemaStyle::OpenAI); + let schema = ChatCompletionTool::new::(None); let chat_completion = ChatCompletion::builder( "gpt-4o-mini", [ChatCompletionMessage { diff --git a/src/chat/structured_output.rs b/src/chat/structured_output.rs index 1a5c901..385adaf 100644 --- a/src/chat/structured_output.rs +++ b/src/chat/structured_output.rs @@ -2,11 +2,12 @@ use std::mem::take; use schemars::{ schema::{Schema, SchemaObject}, + schema_for, visit::{visit_schema_object, Visitor}, JsonSchema, }; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Value}; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum JsonSchemaStyle { @@ -72,17 +73,18 @@ pub struct ToolCallFunctionDefinition { impl ToolCallFunctionDefinition { /// Create a new ToolCallFunctionDefinition with the given strictness and JSON Schema style. /// - /// Note: Grok does not support strict schema adherence. - pub fn new(strict: bool, json_style: JsonSchemaStyle) -> Self { - let (schema, description) = generate_json_schema::(json_style); - let strict = match json_style { - JsonSchemaStyle::OpenAI => Some(strict), - JsonSchemaStyle::Grok => None, + /// Note: Grok tools does not support strict schema adherence, need to set `strict` to None. + pub fn new(strict: Option) -> Self { + let schema = schema_for!(T); + let description = if let Some(metadata) = &schema.schema.metadata { + metadata.description.clone() + } else { + None }; ToolCallFunctionDefinition { description, name: T::schema_name(), - parameters: Some(schema), + parameters: Some(json!(schema)), strict, } } From 5788c6bf96cb6932235643d7606181f588e685e2 Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Thu, 8 May 2025 18:05:31 +0800 Subject: [PATCH 08/10] use enum FunctionType instead of FunctionLiteral --- src/chat.rs | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index e81214c..4d649f6 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -136,27 +136,10 @@ pub struct FunctionChoice { pub name: String, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct FunctionLiteral; -impl Serialize for FunctionLiteral { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str("function") - } -} -impl<'de> Deserialize<'de> for FunctionLiteral { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s: &str = serde::Deserialize::deserialize(deserializer)?; - if s != "function" { - return Err(serde::de::Error::custom("expected function")); - } - Ok(FunctionLiteral) - } +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum FunctionType { + Function, } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] @@ -169,7 +152,7 @@ pub enum ToolChoice { /// The model will call the function with the given name. Function { /// The type of the tool. Currently, only `function` is supported. - r#type: FunctionLiteral, + r#type: FunctionType, /// The function that the model called. function: FunctionChoice, }, @@ -181,7 +164,7 @@ impl ToolChoice { } pub fn function(name: String) -> Self { ToolChoice::Function { - r#type: FunctionLiteral, + r#type: FunctionType::Function, function: FunctionChoice { name }, } } @@ -192,7 +175,7 @@ pub struct ToolCall { /// The ID of the tool call. pub id: String, /// The type of the tool. Currently, only `function` is supported. - pub r#type: FunctionLiteral, + pub r#type: FunctionType, /// The function that the model called. pub function: ToolCallFunction, } @@ -203,7 +186,7 @@ pub struct ToolCallDelta { /// The ID of the tool call. pub id: String, /// The type of the tool. Currently, only `function` is supported. - pub r#type: FunctionLiteral, + pub r#type: FunctionType, /// The function that the model called. pub function: ToolCallFunction, } @@ -1011,7 +994,7 @@ mod tests { .credentials(credentials) .tools(vec![schema]) .tool_choice(ToolChoice::Function { - r#type: FunctionLiteral, + r#type: FunctionType::Function, function: FunctionChoice { name: "Character".to_string(), }, @@ -1056,7 +1039,7 @@ mod tests { tool_call_id: None, tool_calls: Some(vec![ToolCall { id: "the_tool_call".to_string(), - r#type: FunctionLiteral, + r#type: FunctionType::Function, function: ToolCallFunction { name: "mul".to_string(), arguments: "not_required_to_be_valid_here".to_string(), From ef4f35ed3ed255311e3daf844d3d740952e546fc Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Thu, 8 May 2025 18:06:35 +0800 Subject: [PATCH 09/10] add deprecated attribute on function_call --- src/chat.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 4d649f6..8730597 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -65,8 +65,7 @@ pub struct ChatCompletionMessage { /// The function that ChatGPT called. This should be "None" usually, and is returned by ChatGPT and not provided by the developer /// /// [API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat/create-function_call) - /// - /// Deprecated, use `tool_calls` instead + #[deprecated(note = "Use `tool_calls` instead")] #[serde(skip_serializing_if = "Option::is_none")] pub function_call: Option, /// Tool call that this message is responding to. @@ -93,8 +92,7 @@ pub struct ChatCompletionMessageDelta { /// The function that ChatGPT called /// /// [API Reference](https://platform.openai.com/docs/api-reference/chat/create#chat/create-function_call) - /// - /// Deprecated, use `tool_calls` instead + #[deprecated(note = "Use `tool_calls` instead")] #[serde(skip_serializing_if = "Option::is_none")] pub function_call: Option, /// Tool call that this message is responding to. From b706aef8a2022a1e1c0db2c6afeb5c2fe81b047f Mon Sep 17 00:00:00 2001 From: Maverick Liu Date: Fri, 9 May 2025 14:58:37 +0800 Subject: [PATCH 10/10] fix ToolCallDelta deserialize issue --- src/chat.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 8730597..19a3402 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -182,11 +182,11 @@ pub struct ToolCall { pub struct ToolCallDelta { pub index: i64, /// The ID of the tool call. - pub id: String, + pub id: Option, /// The type of the tool. Currently, only `function` is supported. - pub r#type: FunctionType, + pub r#type: Option, /// The function that the model called. - pub function: ToolCallFunction, + pub function: Option, } #[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)] @@ -516,6 +516,7 @@ impl ChatCompletionChoiceDelta { // merge function calls // function call names are concatenated // arguments are merged by concatenating them + #[allow(deprecated)] match self.delta.function_call.as_mut() { Some(function_call) => { match &other.delta.function_call { @@ -550,6 +551,7 @@ impl ChatCompletionChoiceDelta { impl From for ChatCompletion { fn from(delta: ChatCompletionDelta) -> Self { + #[allow(deprecated)] ChatCompletion { id: delta.id, object: delta.object, @@ -657,6 +659,7 @@ impl Default for ChatCompletionMessageRole { } #[cfg(test)] +#[allow(deprecated)] mod tests { use super::*; use dotenvy::dotenv;