addressing feedback: remove rust client integration tests

This commit is contained in:
Salman Paracha 2025-08-06 22:05:23 -07:00
parent 6255595b8c
commit 7490571f66
3 changed files with 8 additions and 833 deletions

292
crates/Cargo.lock generated
View file

@ -104,43 +104,6 @@ version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
[[package]]
name = "async-openai"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31acf814d6b499e33ec894bb0fd7ddaf2665b44fbdd42b858d736449271fde0c"
dependencies = [
"async-openai-macros",
"backoff",
"base64 0.22.1",
"bytes",
"derive_builder",
"eventsource-stream",
"futures",
"rand 0.8.5",
"reqwest",
"reqwest-eventsource",
"secrecy",
"serde",
"serde_json",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
]
[[package]]
name = "async-openai-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0289cba6d5143bfe8251d57b4a8cac036adf158525a76533a7082ba65ec76398"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "async-trait"
version = "0.1.88"
@ -175,20 +138,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
dependencies = [
"futures-core",
"getrandom 0.2.16",
"instant",
"pin-project-lite",
"rand 0.8.5",
"tokio",
]
[[package]]
name = "backtrace"
version = "0.3.75"
@ -338,12 +287,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.41"
@ -411,16 +354,6 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -482,7 +415,7 @@ dependencies = [
"hashbrown 0.14.5",
"log",
"regalloc2",
"rustc-hash 1.1.0",
"rustc-hash",
"smallvec",
"target-lexicon",
]
@ -676,37 +609,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.101",
]
[[package]]
name = "diff"
version = "0.1.13"
@ -970,12 +872,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
@ -1033,10 +929,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@ -1046,11 +940,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
@ -1193,12 +1085,10 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
name = "hermesllm"
version = "0.1.0"
dependencies = [
"async-openai",
"serde",
"serde_json",
"serde_with",
"thiserror 2.0.12",
"tokio",
]
[[package]]
@ -1340,7 +1230,7 @@ dependencies = [
"hyper 0.14.32",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.24.1",
]
@ -1355,7 +1245,6 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.27",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.2",
@ -1594,15 +1483,6 @@ dependencies = [
"serde",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@ -1780,12 +1660,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mach2"
version = "0.4.2"
@ -1831,16 +1705,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1885,7 +1749,7 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework 2.11.1",
"security-framework",
"security-framework-sys",
"tempfile",
]
@ -2335,61 +2199,6 @@ dependencies = [
"cc",
]
[[package]]
name = "quinn"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.1",
"rustls 0.23.27",
"socket2",
"thiserror 2.0.12",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
dependencies = [
"bytes",
"getrandom 0.3.3",
"lru-slab",
"rand 0.9.1",
"ring",
"rustc-hash 2.1.1",
"rustls 0.23.27",
"rustls-pki-types",
"slab",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "quote"
version = "1.0.40"
@ -2532,7 +2341,7 @@ checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6"
dependencies = [
"hashbrown 0.13.2",
"log",
"rustc-hash 1.1.0",
"rustc-hash",
"slice-group-by",
"smallvec",
]
@ -2605,14 +2414,10 @@ dependencies = [
"js-sys",
"log",
"mime",
"mime_guess",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.27",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"serde",
"serde_json",
@ -2620,7 +2425,6 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.26.2",
"tokio-util",
"tower 0.5.2",
"tower-http",
@ -2632,22 +2436,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "reqwest-eventsource"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde"
dependencies = [
"eventsource-stream",
"futures-core",
"futures-timer",
"mime",
"nom",
"pin-project-lite",
"reqwest",
"thiserror 1.0.69",
]
[[package]]
name = "ring"
version = "0.17.14"
@ -2674,12 +2462,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
@ -2725,7 +2507,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki 0.103.3",
"subtle",
@ -2741,19 +2522,7 @@ dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework 2.11.1",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework 3.3.0",
"security-framework",
]
[[package]]
@ -2771,7 +2540,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"web-time",
"zeroize",
]
@ -2860,16 +2628,6 @@ version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"serde",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2877,20 +2635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@ -3218,7 +2963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"core-foundation",
"system-configuration-sys",
]
@ -3331,7 +3076,7 @@ dependencies = [
"fancy-regex",
"lazy_static",
"parking_lot",
"rustc-hash 1.1.0",
"rustc-hash",
]
[[package]]
@ -3375,21 +3120,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
@ -3706,12 +3436,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.18"

View file

@ -8,7 +8,3 @@ serde = {version = "1.0.219", features = ["derive"]}
serde_json = "1.0.140"
serde_with = "3.12.0"
thiserror = "2.0.12"
[dev-dependencies]
async-openai = "0.29.0"
tokio = { version = "1.0", features = ["full"] }

View file

@ -838,549 +838,4 @@ mod tests {
assert!(converted.name.is_none());
assert!(converted.tool_call_id.is_none());
}
// ============================================================================
// INTEGRATION TESTS WITH async-openai
// ============================================================================
mod integration_tests {
use super::*;
use async_openai::{
types::{
ChatCompletionRequestUserMessageArgs,
CreateChatCompletionRequestArgs,
ChatCompletionToolArgs,
ChatCompletionToolType, FunctionObjectArgs,
Role as AsyncOpenAIRole,
},
};
/// Helper to convert our Role to async-openai Role
#[allow(dead_code)]
fn convert_role(role: Role) -> AsyncOpenAIRole {
match role {
Role::System => AsyncOpenAIRole::System,
Role::User => AsyncOpenAIRole::User,
Role::Assistant => AsyncOpenAIRole::Assistant,
Role::Tool => AsyncOpenAIRole::Tool,
}
}
/// Helper to convert async-openai Role to our Role
#[allow(dead_code)]
fn convert_role_back(role: AsyncOpenAIRole) -> Role {
match role {
AsyncOpenAIRole::System => Role::System,
AsyncOpenAIRole::User => Role::User,
AsyncOpenAIRole::Assistant => Role::Assistant,
AsyncOpenAIRole::Tool => Role::Tool,
AsyncOpenAIRole::Function => Role::Tool, // Map Function to Tool (legacy support)
}
}
#[tokio::test]
async fn test_required_fields_compatibility() {
// Test 1: Required fields req/response compatibility
// Create our minimal request
let our_request = ChatCompletionsRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: Role::User,
content: MessageContent::Text("Hello, world!".to_string()),
name: None,
tool_calls: None,
tool_call_id: None,
}],
frequency_penalty: None,
function_call: None,
functions: None,
logit_bias: None,
logprobs: None,
max_completion_tokens: None,
max_tokens: None,
modalities: None,
metadata: None,
n: None,
presence_penalty: None,
parallel_tool_calls: None,
prediction: None,
response_format: None,
seed: None,
service_tier: None,
stop: None,
store: None,
stream: None,
stream_options: None,
temperature: None,
tool_choice: None,
tools: None,
top_p: None,
top_logprobs: None,
user: None,
};
// Create equivalent async-openai request
let async_openai_request = CreateChatCompletionRequestArgs::default()
.model("gpt-4")
.messages(vec![
ChatCompletionRequestUserMessageArgs::default()
.content("Hello, world!")
.build()
.unwrap().into(),
])
.build()
.unwrap();
// Serialize both and compare JSON structure
let our_json = serde_json::to_value(&our_request).unwrap();
let async_openai_json = serde_json::to_value(&async_openai_request).unwrap();
// Both should have the required fields
assert_eq!(our_json["model"], "gpt-4");
assert_eq!(async_openai_json["model"], "gpt-4");
// Messages should be structured the same way
let our_messages = our_json["messages"].as_array().unwrap();
let async_openai_messages = async_openai_json["messages"].as_array().unwrap();
assert_eq!(our_messages.len(), 1);
assert_eq!(async_openai_messages.len(), 1);
assert_eq!(our_messages[0]["role"], "user");
assert_eq!(async_openai_messages[0]["role"], "user");
assert_eq!(our_messages[0]["content"], "Hello, world!");
assert_eq!(async_openai_messages[0]["content"], "Hello, world!");
}
#[tokio::test]
async fn test_optional_fields_compatibility() {
// Test 2: Optional fields req/response compatibility
let our_request = ChatCompletionsRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: Role::User,
content: MessageContent::Text("Test with options".to_string()),
name: Some("test_user".to_string()),
tool_calls: None,
tool_call_id: None,
}],
temperature: Some(0.7),
max_tokens: Some(150),
stream: Some(false),
top_p: Some(0.95),
frequency_penalty: Some(0.1),
presence_penalty: Some(0.2),
stop: Some(vec!["STOP".to_string()]),
user: Some("user123".to_string()),
// Set remaining fields to None
function_call: None,
functions: None,
logit_bias: None,
logprobs: None,
max_completion_tokens: None,
modalities: None,
metadata: None,
n: None,
parallel_tool_calls: None,
prediction: None,
response_format: None,
seed: None,
service_tier: None,
store: None,
stream_options: None,
tool_choice: None,
tools: None,
top_logprobs: None,
};
let async_openai_request = CreateChatCompletionRequestArgs::default()
.model("gpt-4")
.messages(vec![
ChatCompletionRequestUserMessageArgs::default()
.content("Test with options")
.name("test_user")
.build()
.unwrap().into(),
])
.temperature(0.7)
.max_tokens(150u32)
.stream(false)
.top_p(0.95)
.frequency_penalty(0.1)
.presence_penalty(0.2)
.stop(vec!["STOP".to_string()])
.user("user123")
.build()
.unwrap();
let our_json = serde_json::to_value(&our_request).unwrap();
let async_openai_json = serde_json::to_value(&async_openai_request).unwrap();
// Check that optional fields match
assert!((our_json["temperature"].as_f64().unwrap() - 0.7).abs() < 1e-6);
assert!((async_openai_json["temperature"].as_f64().unwrap() - 0.7).abs() < 1e-6);
assert_eq!(our_json["max_tokens"], 150);
assert_eq!(async_openai_json["max_tokens"], 150);
assert_eq!(our_json["stream"], false);
assert_eq!(async_openai_json["stream"], false);
assert_eq!(our_json["user"], "user123");
assert_eq!(async_openai_json["user"], "user123");
// Check message name field
let our_message = &our_json["messages"].as_array().unwrap()[0];
let async_openai_message = &async_openai_json["messages"].as_array().unwrap()[0];
assert_eq!(our_message["name"], "test_user");
assert_eq!(async_openai_message["name"], "test_user");
}
#[tokio::test]
async fn test_streaming_response_compatibility() {
// Test 3: Streaming response compatibility
// Create a mock streaming response using our types
let our_stream_response = ChatCompletionsStreamResponse {
id: "chatcmpl-123".to_string(),
object: "chat.completion.chunk".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![StreamChoice {
index: 0,
delta: MessageDelta {
role: Some(Role::Assistant),
content: Some("Hello! ".to_string()),
refusal: None,
function_call: None,
tool_calls: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: Some("fp_44709d6fcb".to_string()),
service_tier: None,
};
let our_json = serde_json::to_value(&our_stream_response).unwrap();
// Verify structure matches expected streaming response format
assert_eq!(our_json["id"], "chatcmpl-123");
assert_eq!(our_json["object"], "chat.completion.chunk");
assert_eq!(our_json["created"], 1677652288);
assert_eq!(our_json["model"], "gpt-4");
let choices = our_json["choices"].as_array().unwrap();
assert_eq!(choices.len(), 1);
let choice = &choices[0];
assert_eq!(choice["index"], 0);
assert_eq!(choice["delta"]["role"], "assistant");
assert_eq!(choice["delta"]["content"], "Hello! ");
assert!(!choice["delta"].as_object().unwrap().contains_key("refusal"));
// Test final streaming chunk with usage
let final_chunk = ChatCompletionsStreamResponse {
id: "chatcmpl-123".to_string(),
object: "chat.completion.chunk".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![StreamChoice {
index: 0,
delta: MessageDelta {
role: None,
content: None,
refusal: None,
function_call: None,
tool_calls: None,
},
finish_reason: Some(FinishReason::Stop),
logprobs: None,
}],
usage: Some(Usage {
prompt_tokens: 20,
completion_tokens: 30,
total_tokens: 50,
prompt_tokens_details: None,
completion_tokens_details: None,
}),
system_fingerprint: Some("fp_44709d6fcb".to_string()),
service_tier: None,
};
let final_json = serde_json::to_value(&final_chunk).unwrap();
assert_eq!(final_json["choices"][0]["finish_reason"], "stop");
assert_eq!(final_json["usage"]["total_tokens"], 50);
}
#[tokio::test]
async fn test_tool_calls_compatibility() {
// Test 4: Request with tool calls and streaming response with tools
// Create our tool definition
let weather_tool = Tool {
tool_type: "function".to_string(),
function: Function {
name: "get_weather".to_string(),
description: Some("Get the current weather in a location".to_string()),
parameters: json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}),
strict: None,
},
};
// Create request with tools
let our_request = ChatCompletionsRequest {
model: "gpt-4".to_string(),
messages: vec![Message {
role: Role::User,
content: MessageContent::Text("What's the weather like in San Francisco?".to_string()),
name: None,
tool_calls: None,
tool_call_id: None,
}],
tools: Some(vec![weather_tool]),
tool_choice: Some(ToolChoice::String("auto".to_string())),
stream: Some(true),
stream_options: Some(StreamOptions {
include_usage: Some(true),
}),
// Set all other optional fields to None
frequency_penalty: None,
function_call: None,
functions: None,
logit_bias: None,
logprobs: None,
max_completion_tokens: None,
max_tokens: None,
modalities: None,
metadata: None,
n: None,
presence_penalty: None,
parallel_tool_calls: None,
prediction: None,
response_format: None,
seed: None,
service_tier: None,
stop: None,
store: None,
temperature: None,
top_p: None,
top_logprobs: None,
user: None,
};
// Create equivalent async-openai request with tools
let async_openai_tool = ChatCompletionToolArgs::default()
.r#type(ChatCompletionToolType::Function)
.function(
FunctionObjectArgs::default()
.name("get_weather")
.description("Get the current weather in a location")
.parameters(json!({
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}))
.build()
.unwrap(),
)
.build()
.unwrap();
let async_openai_request = CreateChatCompletionRequestArgs::default()
.model("gpt-4")
.messages(vec![
ChatCompletionRequestUserMessageArgs::default()
.content("What's the weather like in San Francisco?")
.build()
.unwrap().into(),
])
.tools(vec![async_openai_tool])
.tool_choice("auto")
.build()
.unwrap();
// Compare JSON structures
let our_json = serde_json::to_value(&our_request).unwrap();
let async_openai_json = serde_json::to_value(&async_openai_request).unwrap();
// Verify tool structure matches
let our_tools = our_json["tools"].as_array().unwrap();
let async_openai_tools = async_openai_json["tools"].as_array().unwrap();
assert_eq!(our_tools.len(), 1);
assert_eq!(async_openai_tools.len(), 1);
let our_tool = &our_tools[0];
let async_openai_tool = &async_openai_tools[0];
assert_eq!(our_tool["type"], "function");
assert_eq!(async_openai_tool["type"], "function");
assert_eq!(our_tool["function"]["name"], "get_weather");
assert_eq!(async_openai_tool["function"]["name"], "get_weather");
// Test streaming response with tool calls
let tool_call_stream_chunk = ChatCompletionsStreamResponse {
id: "chatcmpl-tool-123".to_string(),
object: "chat.completion.chunk".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![StreamChoice {
index: 0,
delta: MessageDelta {
role: Some(Role::Assistant),
content: None,
refusal: None,
function_call: None,
tool_calls: Some(vec![ToolCallDelta {
index: 0,
id: Some("call_abc123".to_string()),
call_type: Some("function".to_string()),
function: Some(FunctionCallDelta {
name: Some("get_weather".to_string()),
arguments: Some(r#"{"location":"San Francisco, CA"}"#.to_string()),
}),
}]),
},
finish_reason: Some(FinishReason::ToolCalls),
logprobs: None,
}],
usage: None,
system_fingerprint: Some("fp_44709d6fcb".to_string()),
service_tier: None,
};
let tool_call_json = serde_json::to_value(&tool_call_stream_chunk).unwrap();
// Verify tool call structure in streaming response
let choice = &tool_call_json["choices"][0];
assert_eq!(choice["finish_reason"], "tool_calls");
let tool_calls = choice["delta"]["tool_calls"].as_array().unwrap();
assert_eq!(tool_calls.len(), 1);
let tool_call = &tool_calls[0];
assert_eq!(tool_call["index"], 0);
assert_eq!(tool_call["id"], "call_abc123");
assert_eq!(tool_call["type"], "function");
assert_eq!(tool_call["function"]["name"], "get_weather");
assert_eq!(tool_call["function"]["arguments"], r#"{"location":"San Francisco, CA"}"#);
// Test tool response message
let tool_response_message = Message {
role: Role::Tool,
content: MessageContent::Text(r#"{"temperature": "72°F", "condition": "sunny"}"#.to_string()),
name: None,
tool_calls: None,
tool_call_id: Some("call_abc123".to_string()),
};
let tool_response_json = serde_json::to_value(&tool_response_message).unwrap();
assert_eq!(tool_response_json["role"], "tool");
assert_eq!(tool_response_json["tool_call_id"], "call_abc123");
assert_eq!(tool_response_json["content"], r#"{"temperature": "72°F", "condition": "sunny"}"#);
}
#[test]
fn test_cross_conversion_compatibility() {
// Test conversion of complex response using JSON-first approach
let complex_response_json = json!({
"id": "chatcmpl-complex",
"object": "chat.completion",
"created": 1677652288u64,
"model": "gpt-4",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "I can help you with that!",
"tool_calls": [
{
"id": "call_xyz789",
"type": "function",
"function": {
"name": "search_database",
"arguments": r#"{"query":"user question"}"#
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 35,
"total_tokens": 60,
"prompt_tokens_details": {
"cached_tokens": 10
},
"completion_tokens_details": {
"reasoning_tokens": 5
}
},
"system_fingerprint": "fp_complex"
});
// Deserialize JSON into ChatCompletionsResponse
let deserialized: ChatCompletionsResponse = serde_json::from_value(complex_response_json.clone()).unwrap();
// Verify fields are properly deserialized
assert_eq!(deserialized.id, "chatcmpl-complex");
assert_eq!(deserialized.object, "chat.completion");
assert_eq!(deserialized.created, 1677652288);
assert_eq!(deserialized.model, "gpt-4");
assert_eq!(deserialized.choices.len(), 1);
assert_eq!(deserialized.usage.total_tokens, 60);
assert_eq!(deserialized.usage.prompt_tokens, 25);
assert_eq!(deserialized.usage.completion_tokens, 35);
assert_eq!(
deserialized.usage.prompt_tokens_details.as_ref().unwrap().cached_tokens,
Some(10)
);
assert_eq!(
deserialized.usage.completion_tokens_details.as_ref().unwrap().reasoning_tokens,
Some(5)
);
assert_eq!(deserialized.system_fingerprint, Some("fp_complex".to_string()));
let choice = &deserialized.choices[0];
assert_eq!(choice.index, 0);
assert_eq!(choice.finish_reason, Some(FinishReason::ToolCalls));
assert!(choice.message.tool_calls.is_some());
assert_eq!(choice.message.role, Role::Assistant);
assert_eq!(choice.message.content, Some("I can help you with that!".to_string()));
let tool_call = &choice.message.tool_calls.as_ref().unwrap()[0];
assert_eq!(tool_call.id, "call_xyz789");
assert_eq!(tool_call.call_type, "function");
assert_eq!(tool_call.function.name, "search_database");
assert_eq!(tool_call.function.arguments, r#"{"query":"user question"}"#);
// Serialize back to JSON and verify round-trip compatibility
let serialized_json = serde_json::to_value(&deserialized).unwrap();
assert_eq!(complex_response_json, serialized_json);
}
}
}