diff --git a/aimux-providers/layout.toml b/aimux-providers/layout.toml index bc2bafced..de6e0328f 100644 --- a/aimux-providers/layout.toml +++ b/aimux-providers/layout.toml @@ -498,3 +498,191 @@ rust = "vertex/partner_provider.rs" status = "mapped" [vertex.files.tests] + +[openai_compatible] +package = "@ai-sdk/openai-compatible" +# chat/openai-compatible-chat-content-array.test.ts has no source file of its own name; +# every case in it is ported, so it needs no exclusions. + +[[openai_compatible.files]] +upstream = "src/chat/convert-openai-compatible-chat-usage.ts" +rust = "openai_compatible/chat.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/convert-to-openai-compatible-chat-messages.ts" +rust = "openai_compatible/convert.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/get-response-metadata.ts" +rust = "openai_compatible/chat.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/map-openai-compatible-finish-reason.ts" +rust = "openai_compatible/convert.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/openai-compatible-api-types.ts" +rust = "openai_compatible/types.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/openai-compatible-chat-language-model-options.ts" +rust = "openai_compatible/config.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/openai-compatible-chat-language-model.ts" +rust = "openai_compatible/chat.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/openai-compatible-metadata-extractor.ts" +rust = "openai_compatible/config.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/chat/openai-compatible-prepare-tools.ts" +rust = "openai_compatible/convert.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/convert-openai-compatible-completion-usage.ts" +rust = "openai_compatible/completion.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/convert-to-openai-compatible-completion-prompt.ts" +rust = "openai_compatible/completion.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/get-response-metadata.ts" +rust = "openai_compatible/completion.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/map-openai-compatible-finish-reason.ts" +rust = "openai_compatible/convert.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/openai-compatible-completion-language-model-options.ts" +rust = "openai_compatible/completion.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/completion/openai-compatible-completion-language-model.ts" +rust = "openai_compatible/completion.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/embedding/openai-compatible-embedding-model-options.ts" +rust = "openai_compatible/embedding.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/embedding/openai-compatible-embedding-model.ts" +rust = "openai_compatible/embedding.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/image/openai-compatible-image-model-options.ts" +rust = "openai_compatible/image.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/image/openai-compatible-image-model.ts" +rust = "openai_compatible/image.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/image/openai-compatible-image-settings.ts" +rust = "openai_compatible/image.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/index.ts" +rust = "openai_compatible/mod.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/internal/index.ts" +rust = "openai_compatible/mod.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/openai-compatible-error.ts" +rust = "openai_compatible/config.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/openai-compatible-provider.ts" +rust = "openai_compatible/mod.rs" +status = "mapped" + +[openai_compatible.files.tests] +ts-only = [ + { case = "OpenAICompatibleProvider › supportsStructuredOutputs setting › should pass supportsStructuredOutputs to .chatModel() and .languageModel() only", reason = "checks the settings handed to the mocked model constructors; aimux's completion, embedding and image models share the provider's chat settings and never read supportsStructuredOutputs, so nothing observable differs" }, + { case = "OpenAICompatibleProvider › supportsMultiPartToolContent setting › should pass supportsMultiPartToolContent to chat models only", reason = "checks the settings handed to the mocked model constructors; aimux's completion, embedding and image models share the provider's chat settings and never read supportsMultiPartToolContent, so nothing observable differs" }, +] + +[[openai_compatible.files]] +upstream = "src/utils/to-camel-case.ts" +rust = "openai_compatible/convert.rs" +status = "mapped" + +[openai_compatible.files.tests] + +[[openai_compatible.files]] +upstream = "src/version.ts" +status = "not-applicable" diff --git a/aimux-providers/src/openai_compatible/chat.rs b/aimux-providers/src/openai_compatible/chat.rs index a122e740b..061765656 100644 --- a/aimux-providers/src/openai_compatible/chat.rs +++ b/aimux-providers/src/openai_compatible/chat.rs @@ -32,7 +32,7 @@ use crate::stream::{ /// of `aimux-provider-utils` use the injected or process-default transport. pub struct OpenAICompatibleChatModel { model_id: String, - config: CompatModelConfig, + pub(super) config: CompatModelConfig, } impl OpenAICompatibleChatModel { diff --git a/aimux-providers/src/openai_compatible/completion.rs b/aimux-providers/src/openai_compatible/completion.rs index db78661e6..1a63bdf8e 100644 --- a/aimux-providers/src/openai_compatible/completion.rs +++ b/aimux-providers/src/openai_compatible/completion.rs @@ -25,7 +25,7 @@ use super::convert::{parse_finish_reason, to_camel_case}; use crate::openai::config::OpenAIModelConfig; #[derive(Clone)] -enum CompletionConfig { +pub(super) enum CompletionConfig { Compatible(CompatModelConfig), Native(OpenAIModelConfig), } @@ -34,7 +34,7 @@ enum CompletionConfig { #[derive(Clone)] pub struct OpenAICompatibleCompletionModel { model_id: String, - config: CompletionConfig, + pub(super) config: CompletionConfig, } impl OpenAICompatibleCompletionModel { diff --git a/aimux-providers/src/openai_compatible/embedding.rs b/aimux-providers/src/openai_compatible/embedding.rs index b26f0f540..810ff34be 100644 --- a/aimux-providers/src/openai_compatible/embedding.rs +++ b/aimux-providers/src/openai_compatible/embedding.rs @@ -21,7 +21,7 @@ use super::convert::to_camel_case; /// An OpenAI-compatible embedding model. pub struct OpenAICompatibleEmbeddingModel { model_id: String, - config: CompatModelConfig, + pub(super) config: CompatModelConfig, } impl OpenAICompatibleEmbeddingModel { diff --git a/aimux-providers/src/openai_compatible/image.rs b/aimux-providers/src/openai_compatible/image.rs index 2077f50c6..55f399f78 100644 --- a/aimux-providers/src/openai_compatible/image.rs +++ b/aimux-providers/src/openai_compatible/image.rs @@ -25,7 +25,7 @@ use super::convert::to_camel_case; /// An OpenAI-compatible image generation / editing model. pub struct OpenAICompatibleImageModel { model_id: String, - config: CompatModelConfig, + pub(super) config: CompatModelConfig, } impl OpenAICompatibleImageModel { diff --git a/aimux-providers/src/openai_compatible/mod.rs b/aimux-providers/src/openai_compatible/mod.rs index 0e34cef56..853f4ff07 100644 --- a/aimux-providers/src/openai_compatible/mod.rs +++ b/aimux-providers/src/openai_compatible/mod.rs @@ -363,3 +363,6 @@ impl ProviderDiscovery for OpenAICompatibleProvider { Box::pin(async move { chat::list_models_once(&config).await }) } } + +#[cfg(test)] +mod tests; diff --git a/aimux-providers/src/openai_compatible/tests/chat/chat_content_array.rs b/aimux-providers/src/openai_compatible/tests/chat/chat_content_array.rs new file mode 100644 index 000000000..ef947c26b --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/chat_content_array.rs @@ -0,0 +1,165 @@ +use futures::StreamExt; +use serde_json::{Value, json}; + +use aimux_core::language_model::LanguageModel; +use aimux_core::options::CallOptions; + +use crate::openai_compatible::{OpenAICompatibleProviderSettings, create_openai_compatible}; +use crate::test_support::mock_fetch::{Canned, MockFetch}; + +fn sse(chunks: &[String]) -> Canned { + Canned { + status: 200, + headers: vec![("content-type".into(), "text/event-stream".into())], + body: chunks.concat().into_bytes(), + } +} + +fn data(value: &Value) -> String { + format!("data: {value}\n\n") +} + +fn prompt() -> CallOptions { + serde_json::from_value(json!({ + "prompt": [ + { + "role": "user", + "content": [{ "type": "text", "text": "What is 17 * 23?" }], + }, + ], + })) + .expect("call options") +} + +/// `model.doStream({ prompt })` against the mock transport; every event in its +/// JSON form, a stream error as an `error` event. +async fn do_stream(chunks: &[String]) -> Vec { + let mock = MockFetch::new(vec![sse(chunks)]); + let model = create_openai_compatible(OpenAICompatibleProviderSettings { + base_url: "https://my.api.com/v1".to_string(), + name: "test-provider".to_string(), + fetch: Some(mock.transport()), + ..Default::default() + }) + .expect("provider") + .chat("test-model"); + let result = model.do_stream(&prompt()).await.expect("stream"); + let events = result + .stream + .map(|part| match part { + Ok(part) => serde_json::to_value(part).expect("part serializes"), + Err(error) => json!({ "type": "error", "error": error.to_string() }), + }) + .collect::>() + .await; + let seen = mock.seen(); + assert_eq!(seen.len(), 1); + assert_eq!(seen[0].url, "https://my.api.com/v1/chat/completions"); + events +} + +fn of_type(events: &[Value], keep: impl Fn(&str) -> bool) -> Vec { + events + .iter() + .filter(|event| event["type"].as_str().is_some_and(&keep)) + .cloned() + .collect() +} + +/// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-content-array.test.ts › should stream text and thinking content parts +#[tokio::test] +async fn should_stream_text_and_thinking_content_parts() { + let events = do_stream(&[ + data(&json!({ + "choices": [ + { + "index": 0, + "delta": { + "content": [ + { + "type": "thinking", + "thinking": [{ "type": "text", "text": "Let me think." }], + }, + { "type": "text", "text": "The answer is 391." }, + ], + }, + "finish_reason": null, + }, + ], + })), + data(&json!({ + "choices": [ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + }, + ], + })), + "data: [DONE]\n\n".to_string(), + ]) + .await; + + assert_eq!(of_type(&events, |t| t == "error"), Vec::::new()); + assert_eq!( + of_type(&events, |t| t.ends_with("delta")), + vec![ + json!({ + "type": "reasoning-delta", + "id": "reasoning-0", + "delta": "Let me think.", + }), + json!({ + "type": "text-delta", + "id": "txt-0", + "delta": "The answer is 391.", + }), + ] + ); +} + +/// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-content-array.test.ts › should ignore unknown streamed content parts +#[tokio::test] +async fn should_ignore_unknown_streamed_content_parts() { + let events = do_stream(&[ + data(&json!({ + "choices": [ + { + "index": 0, + "delta": { + "content": [ + { + "type": "future-part", + "text": { "nested": true }, + "thinking": { "nested": true }, + }, + { "type": "text", "text": "The answer is 391." }, + ], + }, + "finish_reason": null, + }, + ], + })), + data(&json!({ + "choices": [ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + }, + ], + })), + "data: [DONE]\n\n".to_string(), + ]) + .await; + + assert_eq!(of_type(&events, |t| t == "error"), Vec::::new()); + assert_eq!( + of_type(&events, |t| t == "text-delta"), + vec![json!({ + "type": "text-delta", + "id": "txt-0", + "delta": "The answer is 391.", + })] + ); +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/chat_language_model.rs b/aimux-providers/src/openai_compatible/tests/chat/chat_language_model.rs new file mode 100644 index 000000000..b8f4b5cd4 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/chat_language_model.rs @@ -0,0 +1,4551 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use futures::StreamExt; +use serde_json::{Map, Value, json}; + +use aimux_core::error::AiMuxError; +use aimux_core::language_model::LanguageModel; +use aimux_core::language_model_message::{LanguageModelMessage, UserPart}; +use aimux_core::options::CallOptions; +use aimux_core::result::{GenerateResult, StreamResult}; +use aimux_core::shared::{FileBytes, FileData, provider_namespace}; +use aimux_core::stream_part::StreamPart; +use aimux_core::types::ProviderMetadata; + +use crate::openai_compatible::config::{BaseUrl, ChatSettings, CompatModelConfig}; +use crate::openai_compatible::{ + MetadataExtractor, OpenAICompatibleChatModel, OpenAICompatibleProvider, + OpenAICompatibleProviderSettings, StreamMetadataExtractor, create_openai_compatible, +}; +use crate::shared::{AuthScheme, Credential, ProviderHeaders}; +use crate::test_support::mock_fetch::{Canned, MockFetch, Seen}; + +/// The user agent aimux pins (upstream's test server sees `0.0.0-test` +/// versions and its header snapshots leave the header out). +const USER_AGENT: &str = "ai-sdk-openai-compatible/3.0.62 ai-sdk-provider-utils/5.0.53"; + +const TEST_SCHEMA_VALUE: &str = r#"{"type":"object","properties":{"value":{"type":"string"}},"required":["value"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}"#; + +/// `TEST_PROMPT`. +fn test_prompt() -> Value { + json!([{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }]) +} + +/// `LanguageModelV4CallOptions` from their JSON form; a missing `prompt` is +/// `TEST_PROMPT`. +fn call_options(mut options: Value) -> CallOptions { + options + .as_object_mut() + .expect("call options are an object") + .entry("prompt") + .or_insert_with(test_prompt); + serde_json::from_value(options).expect("valid call options") +} + +/// `{ type: 'function', name, inputSchema }`. +fn function_tool(name: &str, input_schema: Value) -> Value { + json!({ "type": "function", "name": name, "inputSchema": input_schema }) +} + +/// The `{ value: string }` input schema most tool tests use. +fn value_schema() -> Value { + serde_json::from_str(TEST_SCHEMA_VALUE).unwrap() +} + +/// `createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: +/// 'test-provider' })` on the mock transport; the struct-update base of +/// every provider and model below. +fn settings(fetch: &Arc) -> OpenAICompatibleProviderSettings { + OpenAICompatibleProviderSettings { + name: "test-provider".into(), + base_url: "https://my.api.com/v1/".into(), + fetch: Some(fetch.transport()), + ..Default::default() + } +} + +fn headers(pairs: &[(&str, &str)]) -> HashMap> { + pairs + .iter() + .map(|(name, value)| ((*name).to_string(), Some((*value).to_string()))) + .collect() +} + +/// The file-level `provider` (with `Authorization: Bearer test-api-key`). +fn provider(fetch: &Arc) -> OpenAICompatibleProvider { + create_openai_compatible(OpenAICompatibleProviderSettings { + headers: Some(headers(&[("Authorization", "Bearer test-api-key")])), + ..settings(fetch) + }) + .unwrap() +} + +/// The file-level `model` (`provider('grok-3')`). +fn model(fetch: &Arc) -> OpenAICompatibleChatModel { + provider(fetch).chat("grok-3") +} + +/// `new OpenAICompatibleChatLanguageModel(modelId, { provider: +/// 'test-provider', url: () => 'https://my.api.com/v1/chat/completions', +/// headers: () => ({}), ...config })`: the same config through the factory. +fn direct_model( + model_id: &str, + settings: OpenAICompatibleProviderSettings, +) -> OpenAICompatibleChatModel { + create_openai_compatible(settings).unwrap().chat(model_id) +} + +/// A `json-value` response of the test server: `content-type` and +/// `content-length` of the serialized body, then `headers`. +fn json_value(body: &Value, extra_headers: &[(&str, &str)]) -> Canned { + let body = serde_json::to_vec(body).unwrap(); + let mut headers = vec![ + ("content-type".to_string(), "application/json".to_string()), + ("content-length".to_string(), body.len().to_string()), + ]; + headers.extend( + extra_headers + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())), + ); + Canned { + status: 200, + headers, + body, + } +} + +/// A `stream-chunks` response of the test server. +fn stream_chunks>(chunks: &[S], extra_headers: &[(&str, &str)]) -> Canned { + let mut headers = vec![ + ("content-type".to_string(), "text/event-stream".to_string()), + ("cache-control".to_string(), "no-cache".to_string()), + ("connection".to_string(), "keep-alive".to_string()), + ]; + headers.extend( + extra_headers + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())), + ); + Canned { + status: 200, + headers, + body: chunks + .iter() + .map(AsRef::as_ref) + .collect::() + .into_bytes(), + } +} + +fn fixture(filename: &str) -> &'static str { + match filename { + "xai-text.json" => include_str!("fixtures/xai-text.json"), + "xai-tool-call.json" => include_str!("fixtures/xai-tool-call.json"), + "xai-text.chunks.txt" => include_str!("fixtures/xai-text.chunks.txt"), + "xai-tool-call.chunks.txt" => include_str!("fixtures/xai-tool-call.chunks.txt"), + "placeholder-first-chunk.chunks.txt" => { + include_str!("fixtures/placeholder-first-chunk.chunks.txt") + } + "anthropic-fallback-tool-call.sse" => { + include_str!("fixtures/anthropic-fallback-tool-call.sse") + } + other => panic!("no fixture {other}"), + } +} + +/// `prepareJsonFixtureResponse`. +fn prepare_json_fixture_response(filename: &str, extra_headers: &[(&str, &str)]) -> Canned { + let body: Value = serde_json::from_str(fixture(&format!("{filename}.json"))).unwrap(); + json_value(&body, extra_headers) +} + +/// `prepareChunksFixtureResponse`. +fn prepare_chunks_fixture_response(filename: &str, extra_headers: &[(&str, &str)]) -> Canned { + let mut chunks: Vec = fixture(&format!("{filename}.chunks.txt")) + .split('\n') + .filter(|line| !line.trim().is_empty()) + .map(|line| format!("data: {line}\n\n")) + .collect(); + chunks.push("data: [DONE]\n\n".to_string()); + stream_chunks(&chunks, extra_headers) +} + +/// `prepareSseFixtureResponse`. +fn prepare_sse_fixture_response(filename: &str) -> Canned { + stream_chunks(&[fixture(&format!("{filename}.sse"))], &[]) +} + +/// A `toMatchSnapshot()` value of a `doGenerate` result. Upstream's +/// `request.body` is the JSON string that was sent; aimux keeps the body as +/// the JSON value, so the snapshot's string is compared parsed. +fn generate_snapshot(text: &str) -> Value { + let mut snapshot: Value = serde_json::from_str(text).unwrap(); + if let Some(body) = snapshot.pointer_mut("/request/body") + && let Some(text) = body.as_str() + { + *body = serde_json::from_str(text).unwrap(); + } + snapshot +} + +fn result_json(result: &GenerateResult) -> Value { + serde_json::to_value(result).unwrap() +} + +fn request_body(fetch: &MockFetch) -> Value { + fetch.seen()[0].json_body() +} + +/// `server.calls[0].requestHeaders` without `user-agent`, which is checked +/// on its own against the pinned value. +fn request_headers(seen: &Seen) -> Value { + let mut headers = seen.headers.clone(); + assert_eq!(headers.remove("user-agent").as_deref(), Some(USER_AGENT)); + serde_json::to_value(headers).unwrap() +} + +/// The error of an error part as the snapshots show it: upstream passes an +/// error chunk's `error` object on as it is (aimux's `ApiCallError.data`); +/// thrown errors are `[AI_: ]`, here `{ name, message }`. +fn error_json(error: &AiMuxError) -> Value { + match error { + AiMuxError::ApiCall(detail) => detail.data.clone().unwrap_or(Value::Null), + AiMuxError::InvalidResponseData { message, .. } => { + json!({ "name": "AI_InvalidResponseDataError", "message": message }) + } + AiMuxError::JsonParse { message, .. } => { + json!({ "name": "AI_JSONParseError", "message": message }) + } + other => json!({ "name": format!("{other:?}"), "message": other.to_string() }), + } +} + +fn part_json(part: StreamPart) -> Value { + match part { + StreamPart::Error { error } => json!({ "type": "error", "error": error_json(&error) }), + part => serde_json::to_value(part).unwrap(), + } +} + +/// `convertReadableStreamToArray`: rejects with the error the stream ends +/// with. +async fn convert_stream_to_array(result: StreamResult) -> Result, AiMuxError> { + let mut stream = result.stream; + let mut parts = Vec::new(); + while let Some(item) = stream.next().await { + parts.push(part_json(item?)); + } + Ok(parts) +} + +async fn stream_parts(result: StreamResult) -> Vec { + convert_stream_to_array(result).await.unwrap() +} + +fn of_type<'a>(parts: &'a [Value], kind: &str) -> Vec<&'a Value> { + parts.iter().filter(|part| part["type"] == kind).collect() +} + +fn find<'a>(parts: &'a [Value], kind: &str) -> &'a Value { + parts + .iter() + .find(|part| part["type"] == kind) + .unwrap_or_else(|| panic!("no {kind} part")) +} + +/// `toMatchObject`: every property of `expected` is in `actual` and matches +/// recursively; arrays match element by element and have the same length. +#[track_caller] +fn assert_match_object(actual: &Value, expected: &Value) { + fn matches(actual: &Value, expected: &Value) -> bool { + match (actual, expected) { + (Value::Object(actual), Value::Object(expected)) => expected + .iter() + .all(|(key, value)| actual.get(key).is_some_and(|a| matches(a, value))), + (Value::Array(actual), Value::Array(expected)) => { + actual.len() == expected.len() + && actual.iter().zip(expected).all(|(a, e)| matches(a, e)) + } + (actual, expected) => actual == expected, + } + } + assert!( + matches(actual, expected), + "toMatchObject failed\n actual: {actual:#}\nexpected: {expected:#}" + ); +} + +/// `prepareJsonResponse` of the `doGenerate` block: `overrides` holds the +/// named arguments that differ from upstream's defaults. +fn prepare_json_response(overrides: Value) -> Canned { + let get = |key: &str, default: Value| overrides.get(key).cloned().unwrap_or(default); + let mut message = Map::new(); + message.insert("role".into(), json!("assistant")); + message.insert("content".into(), get("content", json!(""))); + message.insert( + "reasoning_content".into(), + get("reasoning_content", json!("")), + ); + message.insert("reasoning".into(), get("reasoning", json!(""))); + for key in ["tool_calls", "function_call"] { + if let Some(value) = overrides.get(key) { + message.insert(key.into(), value.clone()); + } + } + json_value( + &json!({ + "id": get("id", json!("chatcmpl-95ZTZkhr0mHNKqerQfiwkuox3PHAd")), + "object": "chat.completion", + "created": get("created", json!(1711115037)), + "model": get("model", json!("grok-3")), + "choices": [{ + "index": 0, + "message": message, + "finish_reason": get("finish_reason", json!("stop")), + }], + "usage": get("usage", json!({ "prompt_tokens": 4, "total_tokens": 34, "completion_tokens": 30 })), + "system_fingerprint": "fp_3bc1b5746c", + }), + &[], + ) +} + +/// `prepareStreamResponse` of the `doStream` block. +fn prepare_stream_response(content: &[&str], finish_reason: &str) -> Canned { + const PREFIX: &str = r#"data: {"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","#; + let mut chunks = vec![ + format!( + r#"{PREFIX}"system_fingerprint":null,"choices":[{{"index":0,"delta":{{"role":"assistant","content":""}},"finish_reason":null}}]}}"# + ) + "\n\n", + ]; + for text in content { + chunks.push( + format!( + r#"{PREFIX}"system_fingerprint":null,"choices":[{{"index":1,"delta":{{"content":"{text}"}},"finish_reason":null}}]}}"# + ) + "\n\n", + ); + } + chunks.push( + format!( + r#"{PREFIX}"system_fingerprint":null,"choices":[{{"index":0,"delta":{{}},"finish_reason":"{finish_reason}"}}]}}"# + ) + "\n\n", + ); + chunks.push( + format!( + concat!( + r#"data: {{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","#, + r#""system_fingerprint":"fp_10c08bf97d","choices":[{{"index":0,"delta":{{}},"finish_reason":"{finish_reason}"}}],"#, + r#""usage":{{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,"#, + r#""completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}}}"# + ), + finish_reason = finish_reason + ) + "\n\n", + ); + chunks.push("data: [DONE]\n\n".to_string()); + stream_chunks(&chunks, &[]) +} + +/// `data: \n\n` for every chunk, as the test bodies spell them. +fn data(chunks: &[&str]) -> Vec { + chunks + .iter() + .map(|chunk| format!("data: {chunk}\n\n")) + .collect() +} + +/// `data(chunks)` followed by `data: [DONE]`. +fn data_done(chunks: &[&str]) -> Vec { + let mut all = data(chunks); + all.push("data: [DONE]\n\n".to_string()); + all +} + +fn json_mock(body: Value) -> Arc { + MockFetch::new(vec![json_value(&body, &[])]) +} + +fn stream_mock(chunks: &[&str]) -> Arc { + MockFetch::new(vec![stream_chunks(&data_done(chunks), &[])]) +} + +/// `new OpenAICompatibleChatLanguageModel('gpt-5', { provider, url: () => '', +/// headers: () => ({}) })`. +fn config_model(provider: &str) -> OpenAICompatibleChatModel { + OpenAICompatibleChatModel::from_config( + "gpt-5".to_string(), + CompatModelConfig { + provider: provider.to_string(), + base_url: BaseUrl::Fixed(String::new()), + query_params: None, + headers: ProviderHeaders::new(Credential::None, AuthScheme::Bearer, Vec::new(), None), + fetch_headers: None, + fetch: None, + transform_request_body: None, + chat: ChatSettings::default(), + }, + ) +} + +/// The `testMetadataExtractor` of the `metadata extraction` block. +struct TestMetadataExtractor; + +impl MetadataExtractor for TestMetadataExtractor { + fn extract_metadata(&self, parsed_body: &Value) -> Option { + let value = parsed_body.as_object()?.get("test_field")?; + Some(provider_namespace("test-provider", json!({ "value": value })).unwrap()) + } + + fn create_stream_extractor(&self) -> Box { + Box::new(TestStreamMetadataExtractor { + accumulated_value: None, + }) + } +} + +struct TestStreamMetadataExtractor { + accumulated_value: Option, +} + +impl StreamMetadataExtractor for TestStreamMetadataExtractor { + fn process_chunk(&mut self, chunk: &Value) { + let stops = chunk + .get("choices") + .and_then(Value::as_array) + .and_then(|choices| choices.first()) + .and_then(|choice| choice.get("finish_reason")) + == Some(&json!("stop")); + if stops && let Some(value) = chunk.get("test_field") { + self.accumulated_value = Some(value.clone()); + } + } + + fn build_metadata(&self) -> Option { + // `accumulatedValue ? { ... } : undefined` + let value = self + .accumulated_value + .as_ref() + .filter(|value| value.as_str().is_none_or(|text| !text.is_empty()))?; + Some(provider_namespace("test-provider", json!({ "value": value })).unwrap()) + } +} + +mod config { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › config › should extract base name from provider string + #[test] + fn should_extract_base_name_from_provider_string() { + let model = config_model("anthropic.beta"); + + assert_eq!(model.config.provider_options_name(), "anthropic"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › config › should handle provider without dot notation + #[test] + fn should_handle_provider_without_dot_notation() { + let model = config_model("openai"); + + assert_eq!(model.config.provider_options_name(), "openai"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › config › should return empty for empty provider + #[test] + fn should_return_empty_for_empty_provider() { + let model = config_model(""); + + assert_eq!(model.config.provider_options_name(), ""); + } +} + +mod do_generate { + use super::*; + + mod text_fixture { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › text (fixture) › should extract text content + #[tokio::test] + async fn should_extract_text_content() { + let fetch = MockFetch::new(vec![prepare_json_fixture_response("xai-text", &[])]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result), + generate_snapshot(include_str!("snapshots/do_generate_text_fixture.json")) + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should reject a response without choices + #[tokio::test] + async fn should_reject_a_response_without_choices() { + let fetch = json_mock(json!({ + "id": "chatcmpl-empty", + "object": "chat.completion", + "created": 1711115037, + "model": "grok-3", + "choices": [], + "usage": { "prompt_tokens": 4, "total_tokens": 4, "completion_tokens": 0 }, + })); + + let error = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap_err(); + + assert!( + matches!(&error, AiMuxError::InvalidResponseData { message, .. } if message == "Response did not contain any choices."), + "{error:?}" + ); + } + + mod tool_call_fixture { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › tool call (fixture) › should extract tool call content + #[tokio::test] + async fn should_extract_tool_call_content() { + let fetch = MockFetch::new(vec![prepare_json_fixture_response("xai-tool-call", &[])]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result), + generate_snapshot(include_str!("snapshots/do_generate_tool_call_fixture.json")) + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should generate distinct IDs for parallel tool calls with empty IDs + #[tokio::test] + async fn should_generate_distinct_ids_for_parallel_tool_calls_with_empty_ids() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "finish_reason": "tool_calls", + "tool_calls": [ + { "id": "", "type": "function", "function": { "name": "first", "arguments": "{\"value\":1}" } }, + { "id": "", "type": "function", "function": { "name": "second", "arguments": "{\"value\":2}" } }, + ], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + let content = result_json(&result)["content"].clone(); + let tool_call_ids: Vec = content + .as_array() + .unwrap() + .iter() + .filter(|part| part["type"] == "tool-call") + .map(|part| part["toolCallId"].as_str().unwrap().to_string()) + .collect(); + + assert_eq!(tool_call_ids.len(), 2); + assert!(tool_call_ids.iter().all(|id| !id.is_empty())); + assert_eq!( + tool_call_ids + .iter() + .collect::>() + .len(), + 2 + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should extract usage + #[tokio::test] + async fn should_extract_usage() { + let fetch = MockFetch::new(vec![prepare_json_fixture_response("xai-text", &[])]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["usage"], + json!({ + "inputTokens": { + "cacheRead": 2, + "noCache": 10, + "total": 12 + }, + "outputTokens": { + "reasoning": 320, + "text": 0, + "total": 2 + }, + "raw": { + "completion_tokens": 2, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 320, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1641500, + "num_sources_used": 0, + "prompt_tokens": 12, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 2, + "image_tokens": 0, + "text_tokens": 12 + }, + "total_tokens": 334 + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should send additional response information + #[tokio::test] + async fn should_send_additional_response_information() { + let fetch = MockFetch::new(vec![prepare_json_fixture_response("xai-text", &[])]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + let response = result.response.unwrap(); + + assert_eq!( + response.id.as_deref(), + Some("edea4703-19aa-6d74-fedb-dc1c213543e0") + ); + assert_eq!( + response.timestamp.map(|timestamp| timestamp.to_string()), + Some("2026-02-11T01:08:10.000Z".to_string()) + ); + assert_eq!(response.model_id.as_deref(), Some("grok-3-mini")); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should expose the raw response headers + #[tokio::test] + async fn should_expose_the_raw_response_headers() { + let fetch = MockFetch::new(vec![prepare_json_fixture_response( + "xai-text", + &[("test-header", "test-value")], + )]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["response"]["headers"], + json!({ + "content-length": "2053", + "content-type": "application/json", + "test-header": "test-value" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass user setting to requests + #[tokio::test] + async fn should_pass_user_setting_to_requests() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "Hello, World!" }), + )]); + let model_with_user = provider(&fetch).chat("grok-3"); + + model_with_user + .do_generate(&call_options(json!({ + "providerOptions": { "xai": { "user": "test-user-id" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should extract reasoning from reasoning field when reasoning_content is not provided + #[tokio::test] + async fn should_extract_reasoning_from_reasoning_field_when_reasoning_content_is_not_provided() + { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": "Hello, World!", + "reasoning": "This is the reasoning from the reasoning field", + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { + "text": "Hello, World!", + "type": "text" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should prefer reasoning_content over reasoning field when both are provided + #[tokio::test] + async fn should_prefer_reasoning_content_over_reasoning_field_when_both_are_provided() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": "Hello, World!", + "reasoning_content": "This is from reasoning_content", + "reasoning": "This is from reasoning field", + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { + "text": "Hello, World!", + "type": "text" + }, + { + "text": "This is from reasoning_content", + "type": "reasoning" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should normalize text and thinking content parts + #[tokio::test] + async fn should_normalize_text_and_thinking_content_parts() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": [ + { + "type": "thinking", + "thinking": [ + { "type": "text", "text": "Let me think" }, + { "type": "text", "text": " this through." }, + ], + }, + { "type": "text", "text": "The answer is 391." }, + ], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { "type": "reasoning", "text": "Let me think this through." }, + { "type": "text", "text": "The answer is 391." }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should ignore unknown content parts + #[tokio::test] + async fn should_ignore_unknown_content_parts() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": [ + { + "type": "future-part", + "text": { "nested": true }, + "thinking": { "nested": true }, + }, + { "type": "text", "text": "The answer is 391." }, + ], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([{ "type": "text", "text": "The answer is 391." }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should support partial usage + #[tokio::test] + async fn should_support_partial_usage() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "usage": { "prompt_tokens": 20, "total_tokens": 20 }, + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["usage"], + json!({ + "inputTokens": { + "cacheRead": 0, + "noCache": 20, + "total": 20 + }, + "outputTokens": { + "reasoning": 0, + "text": 0, + "total": 0 + }, + "raw": { + "prompt_tokens": 20, + "total_tokens": 20 + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should support unknown finish reason + #[tokio::test] + async fn should_support_unknown_finish_reason() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "finish_reason": "eos" }), + )]); + + let response = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&response)["finishReason"], + json!({ + "raw": "eos", + "unified": "other" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass the model and the messages + #[tokio::test] + async fn should_pass_the_model_and_the_messages() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + + model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass video input as video_url in doGenerate + #[tokio::test] + async fn should_pass_video_input_as_video_url_in_do_generate() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + let mut options = call_options(json!({ + "prompt": [{ + "role": "user", + "content": [ + { "type": "text", "text": "Describe this video." }, + { "type": "file", "data": { "type": "data", "data": "" }, "mediaType": "video/mp4" }, + ], + }], + })); + // `data: new Uint8Array([0, 1, 2, 3])` + if let LanguageModelMessage::User { content, .. } = &mut options.prompt[0] + && let UserPart::File(file) = &mut content[1] + { + file.data = FileData::Data { + data: FileBytes::Binary(vec![0, 1, 2, 3]), + }; + } + + model(&fetch).do_generate(&options).await.unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ + "messages": [{ + "role": "user", + "content": [ + { "type": "text", "text": "Describe this video." }, + { "type": "video_url", "video_url": { "url": "data:video/mp4;base64,AAECAw==" } }, + ], + }], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass multi-part tool content when supported + #[tokio::test] + async fn should_pass_multi_part_tool_content_when_supported() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + let multi_part_provider = create_openai_compatible(OpenAICompatibleProviderSettings { + supports_multi_part_tool_content: Some(true), + ..settings(&fetch) + }) + .unwrap(); + + multi_part_provider + .chat("grok-3") + .do_generate(&call_options(json!({ + "prompt": [{ + "role": "tool", + "content": [{ + "type": "tool-result", + "toolCallId": "call-1", + "toolName": "useImage", + "output": { + "type": "content", + "value": [ + { "type": "text", "text": "image result" }, + { + "type": "file", + "data": { "type": "data", "data": "iVBORw0KGgo=" }, + "mediaType": "image/png", + }, + ], + }, + }], + }], + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ + "messages": [{ + "role": "tool", + "tool_call_id": "call-1", + "content": [ + { "type": "text", "text": "image result" }, + { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo=" } }, + ], + }], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass settings + #[tokio::test] + async fn should_pass_settings() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({}))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "openaiCompatible": { "user": "test-user-id" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "user": "test-user-id" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass settings with deprecated openai-compatible key and emit warning + #[tokio::test] + async fn should_pass_settings_with_deprecated_openai_compatible_key_and_emit_warning() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({}))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "openai-compatible": { "user": "test-user-id" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "user": "test-user-id" + }) + ); + assert_eq!( + result_json(&result)["warnings"], + json!([ + { + "message": "Use 'openaiCompatible' instead.", + "setting": "providerOptions key 'openai-compatible'", + "type": "deprecated" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should include provider-specific options + #[tokio::test] + async fn should_include_provider_specific_options() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({}))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "someCustomOption": "test-value" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should serialize thought signatures using the custom provider metadata key + #[tokio::test] + async fn should_serialize_thought_signatures_using_the_custom_provider_metadata_key() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "prompt": [{ + "role": "assistant", + "content": [{ + "type": "tool-call", + "toolCallId": "call-1", + "toolName": "test_tool", + "input": { "value": "test" }, + "providerOptions": { + "test-provider": { "thoughtSignature": "" }, + }, + }], + }], + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ + "messages": [{ + "role": "assistant", + "tool_calls": [{ + "extra_content": { "google": { "thought_signature": "" } }, + }], + }], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should not include provider-specific options for different provider + #[tokio::test] + async fn should_not_include_provider_specific_options_for_different_provider() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({}))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "notThisProviderName": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3" + }) + ); + } + + mod camel_case_provider_options { + use super::*; + + /// The usage of the two metadata-key tests. + fn prediction_usage() -> Value { + json!({ + "prompt_tokens": 20, + "completion_tokens": 30, + "total_tokens": 50, + "completion_tokens_details": { "accepted_prediction_tokens": 15 }, + }) + } + + fn signature_tool_calls() -> Value { + json!([{ + "id": "call-1", + "type": "function", + "function": { "name": "test_tool", "arguments": "{\"arg\":\"value\"}" }, + "extra_content": { "google": { "thought_signature": "" } }, + }]) + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should accept camelCase provider options key for hyphenated provider name + #[tokio::test] + async fn should_accept_camel_case_provider_options_key_for_hyphenated_provider_name() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "Hello!" }))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "testProvider": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ "someCustomOption": "test-value" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should prefer camelCase options over raw-name options + #[tokio::test] + async fn should_prefer_camel_case_options_over_raw_name_options() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "Hello!" }))]); + + provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { + "test-provider": { "someCustomOption": "raw-value" }, + "testProvider": { "someCustomOption": "camel-value" }, + }, + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ "someCustomOption": "camel-value" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should use camelCase metadata key when camelCase provider options are used + #[tokio::test] + async fn should_use_camel_case_metadata_key_when_camel_case_provider_options_are_used() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": "Hello!", + "usage": prediction_usage(), + }))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "testProvider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + let metadata = result.provider_metadata.unwrap(); + + assert!(metadata.contains_key("testProvider")); + assert!(!metadata.contains_key("test-provider")); + assert_match_object( + &Value::Object(metadata["testProvider"].clone()), + &json!({ "acceptedPredictionTokens": 15 }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should use raw metadata key when raw provider options are used + #[tokio::test] + async fn should_use_raw_metadata_key_when_raw_provider_options_are_used() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "content": "Hello!", + "usage": prediction_usage(), + }))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + let metadata = result.provider_metadata.unwrap(); + + assert!(metadata.contains_key("test-provider")); + assert_match_object( + &Value::Object(metadata["test-provider"].clone()), + &json!({ "acceptedPredictionTokens": 15 }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should emit deprecated warning when raw provider options key is used + #[tokio::test] + async fn should_emit_deprecated_warning_when_raw_provider_options_key_is_used() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "Hello!" }))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["warnings"], + json!([ + { + "message": "Use 'testProvider' instead.", + "setting": "providerOptions key 'test-provider'", + "type": "deprecated" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should not emit deprecated warning when camelCase provider options key is used + #[tokio::test] + async fn should_not_emit_deprecated_warning_when_camel_case_provider_options_key_is_used() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "Hello!" }))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "testProvider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + + assert_eq!(result_json(&result)["warnings"], json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should use raw metadata key when no provider options are passed + #[tokio::test] + async fn should_use_raw_metadata_key_when_no_provider_options_are_passed() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "Hello!" }))]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert!( + result + .provider_metadata + .unwrap() + .contains_key("test-provider") + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should include thought signature in providerMetadata with camelCase key + #[tokio::test] + async fn should_include_thought_signature_in_provider_metadata_with_camel_case_key() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "tool_calls": signature_tool_calls() }), + )]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "testProvider": {} }, + }))) + .await + .unwrap(); + + assert_match_object( + &result_json(&result)["content"], + &json!([{ + "type": "tool-call", + "providerMetadata": { "testProvider": { "thoughtSignature": "" } }, + }]), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › camelCase provider options › should include thought signature in providerMetadata with raw key + #[tokio::test] + async fn should_include_thought_signature_in_provider_metadata_with_raw_key() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "tool_calls": signature_tool_calls() }), + )]); + + let result = provider(&fetch) + .chat("grok-3") + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": {} }, + }))) + .await + .unwrap(); + + assert_match_object( + &result_json(&result)["content"], + &json!([{ + "type": "tool-call", + "providerMetadata": { "test-provider": { "thoughtSignature": "" } }, + }]), + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass tools and toolChoice + #[tokio::test] + async fn should_pass_tools_and_tool_choice() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + + model(&fetch) + .do_generate(&call_options(json!({ + "tools": [function_tool("test-tool", value_schema())], + "toolChoice": { "type": "tool", "toolName": "test-tool" }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "tool_choice": { + "function": { + "name": "test-tool" + }, + "type": "function" + }, + "tools": [ + { + "function": { + "name": "test-tool", + "parameters": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + } + }, + "type": "function" + } + ] + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + let provider = create_openai_compatible(OpenAICompatibleProviderSettings { + headers: Some(headers(&[ + ("Authorization", "Bearer test-api-key"), + ("Custom-Provider-Header", "provider-header-value"), + ])), + ..settings(&fetch) + }) + .unwrap(); + + provider + .chat("grok-3") + .do_generate(&call_options(json!({ + "headers": { "Custom-Request-Header": "request-header-value" }, + }))) + .await + .unwrap(); + + assert_eq!( + request_headers(&fetch.seen()[0]), + json!({ + "authorization": "Bearer test-api-key", + "content-type": "application/json", + "custom-provider-header": "provider-header-value", + "custom-request-header": "request-header-value" + }) + ); + } + + mod google_gemini_thought_signatures_openai_compatibility { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › Google Gemini thought signatures (OpenAI compatibility) › should parse thought signature from extra_content and include in providerMetadata + #[tokio::test] + async fn should_parse_thought_signature_from_extra_content_and_include_in_provider_metadata() + { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "tool_calls": [{ + "id": "function-call-1", + "type": "function", + "function": { "name": "check_flight", "arguments": "{\"flight\":\"AA100\"}" }, + "extra_content": { "google": { "thought_signature": "" } }, + }], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({ + "tools": [function_tool("check_flight", json!({ + "type": "object", + "properties": { "flight": { "type": "string" } }, + "required": ["flight"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { + "input": "{\"flight\":\"AA100\"}", + "providerMetadata": { + "test-provider": { + "thoughtSignature": "" + } + }, + "toolCallId": "function-call-1", + "toolName": "check_flight", + "type": "tool-call" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › Google Gemini thought signatures (OpenAI compatibility) › should handle parallel tool calls with signature only on first call + #[tokio::test] + async fn should_handle_parallel_tool_calls_with_signature_only_on_first_call() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "tool_calls": [ + { + "id": "function-call-paris", + "type": "function", + "function": { "name": "get_current_temperature", "arguments": "{\"location\":\"Paris\"}" }, + "extra_content": { "google": { "thought_signature": "" } }, + }, + { + "id": "function-call-london", + "type": "function", + "function": { "name": "get_current_temperature", "arguments": "{\"location\":\"London\"}" }, + }, + ], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({ + "tools": [function_tool("get_current_temperature", json!({ + "type": "object", + "properties": { "location": { "type": "string" } }, + "required": ["location"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { + "input": "{\"location\":\"Paris\"}", + "providerMetadata": { + "test-provider": { + "thoughtSignature": "" + } + }, + "toolCallId": "function-call-paris", + "toolName": "get_current_temperature", + "type": "tool-call" + }, + { + "input": "{\"location\":\"London\"}", + "toolCallId": "function-call-london", + "toolName": "get_current_temperature", + "type": "tool-call" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › Google Gemini thought signatures (OpenAI compatibility) › should not include providerMetadata when no thought signature is present + #[tokio::test] + async fn should_not_include_provider_metadata_when_no_thought_signature_is_present() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "tool_calls": [{ + "id": "call-1", + "type": "function", + "function": { "name": "some_tool", "arguments": "{\"param\":\"value\"}" }, + }], + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({ + "tools": [function_tool("some_tool", json!({ + "type": "object", + "properties": { "param": { "type": "string" } }, + "required": ["param"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["content"], + json!([ + { + "input": "{\"param\":\"value\"}", + "toolCallId": "call-1", + "toolName": "some_tool", + "type": "tool-call" + } + ]) + ); + } + } + + mod response_format { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should not send a response_format when response format is text + #[tokio::test] + async fn should_not_send_a_response_format_when_response_format_is_text() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(false), + ..settings(&fetch) + }, + ); + + model + .do_generate(&call_options( + json!({ "responseFormat": { "type": "text" } }), + )) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should forward json response format as "json_object" without schema + #[tokio::test] + async fn should_forward_json_response_format_as_json_object_without_schema() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = provider(&fetch).chat("gpt-4o-2024-08-06"); + + model + .do_generate(&call_options( + json!({ "responseFormat": { "type": "json" } }), + )) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "type": "json_object" + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should forward json response format as "json_object" and omit schema when structuredOutputs are disabled + #[tokio::test] + async fn should_forward_json_object_and_omit_schema_when_structured_outputs_are_disabled() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(false), + ..settings(&fetch) + }, + ); + + let result = model + .do_generate(&call_options(json!({ + "responseFormat": { "type": "json", "schema": value_schema() }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "type": "json_object" + } + }) + ); + assert_eq!( + result_json(&result)["warnings"], + json!([ + { + "details": "JSON response format schema is only supported with structuredOutputs", + "feature": "responseFormat", + "type": "unsupported" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should forward json response format as "json_object" and include schema when structuredOutputs are enabled + #[tokio::test] + async fn should_forward_json_object_and_include_schema_when_structured_outputs_are_enabled() + { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(true), + ..settings(&fetch) + }, + ); + + let result = model + .do_generate(&call_options(json!({ + "responseFormat": { "type": "json", "schema": value_schema() }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "json_schema": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "strict": true + }, + "type": "json_schema" + } + }) + ); + assert_eq!(result_json(&result)["warnings"], json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should pass reasoningEffort setting from providerOptions + #[tokio::test] + async fn should_pass_reasoning_effort_setting_from_provider_options() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"test\"}" }), + )]); + let model = direct_model("gpt-5", settings(&fetch)); + + model + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-5", + "reasoning_effort": "high" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should not duplicate reasoningEffort in request body + #[tokio::test] + async fn should_not_duplicate_reasoning_effort_in_request_body() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"test\"}" }), + )]); + let model = direct_model("gpt-5", settings(&fetch)); + + model + .do_generate(&call_options(json!({ + "providerOptions": { + "test-provider": { + "reasoningEffort": "high", + "customOption": "should-be-included", + }, + }, + }))) + .await + .unwrap(); + let body = request_body(&fetch); + + assert_eq!(body["reasoning_effort"], "high"); + assert_eq!(body.get("reasoningEffort"), None); + assert_eq!(body["customOption"], "should-be-included"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should pass top-level reasoning as reasoning_effort + #[tokio::test] + async fn should_pass_top_level_reasoning_as_reasoning_effort() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "test" }))]); + + model(&fetch) + .do_generate(&call_options(json!({ "reasoning": "medium" }))) + .await + .unwrap(); + + assert_eq!(request_body(&fetch)["reasoning_effort"], "medium"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should pass top-level reasoning none as reasoning_effort + #[tokio::test] + async fn should_pass_top_level_reasoning_none_as_reasoning_effort() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "test" }))]); + + model(&fetch) + .do_generate(&call_options(json!({ "reasoning": "none" }))) + .await + .unwrap(); + + assert_eq!(request_body(&fetch)["reasoning_effort"], "none"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should prefer providerOptions reasoningEffort over top-level reasoning + #[tokio::test] + async fn should_prefer_provider_options_reasoning_effort_over_top_level_reasoning() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "test" }))]); + let model = direct_model("gpt-5", settings(&fetch)); + + model + .do_generate(&call_options(json!({ + "reasoning": "medium", + "providerOptions": { "test-provider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + + assert_eq!(request_body(&fetch)["reasoning_effort"], "high"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should pass textVerbosity setting from providerOptions + #[tokio::test] + async fn should_pass_text_verbosity_setting_from_provider_options() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"test\"}" }), + )]); + let model = direct_model("gpt-5", settings(&fetch)); + + model + .do_generate(&call_options(json!({ + "providerOptions": { "test-provider": { "textVerbosity": "low" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-5", + "verbosity": "low" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should not duplicate textVerbosity in request body + #[tokio::test] + async fn should_not_duplicate_text_verbosity_in_request_body() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"test\"}" }), + )]); + let model = direct_model("gpt-5", settings(&fetch)); + + model + .do_generate(&call_options(json!({ + "providerOptions": { + "test-provider": { + "textVerbosity": "medium", + "customOption": "should-be-included", + }, + }, + }))) + .await + .unwrap(); + let body = request_body(&fetch); + + assert_eq!(body["verbosity"], "medium"); + assert_eq!(body.get("textVerbosity"), None); + assert_eq!(body["customOption"], "should-be-included"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should use json_schema & strict with responseFormat json when structuredOutputs are enabled + #[tokio::test] + async fn should_use_json_schema_and_strict_when_structured_outputs_are_enabled() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(true), + ..settings(&fetch) + }, + ); + + model + .do_generate(&call_options(json!({ + "responseFormat": { "type": "json", "schema": value_schema() }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "json_schema": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "strict": true + }, + "type": "json_schema" + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should set name & description with responseFormat json when structuredOutputs are enabled + #[tokio::test] + async fn should_set_name_and_description_when_structured_outputs_are_enabled() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(true), + ..settings(&fetch) + }, + ); + + model + .do_generate(&call_options(json!({ + "responseFormat": { + "type": "json", + "name": "test-name", + "description": "test description", + "schema": value_schema(), + }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "json_schema": { + "description": "test description", + "name": "test-name", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "strict": true + }, + "type": "json_schema" + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should send strict: false when strictJsonSchema is explicitly disabled + #[tokio::test] + async fn should_send_strict_false_when_strict_json_schema_is_explicitly_disabled() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(true), + ..settings(&fetch) + }, + ); + + model + .do_generate(&call_options(json!({ + "responseFormat": { + "type": "json", + "name": "test-name", + "description": "test description", + "schema": value_schema(), + }, + "providerOptions": { "test-provider": { "strictJsonSchema": false } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "json_schema": { + "description": "test description", + "name": "test-name", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ], + "type": "object" + }, + "strict": false + }, + "type": "json_schema" + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › response format › should allow for undefined schema with responseFormat json when structuredOutputs are enabled + #[tokio::test] + async fn should_allow_for_undefined_schema_when_structured_outputs_are_enabled() { + let fetch = MockFetch::new(vec![prepare_json_response( + json!({ "content": "{\"value\":\"Spark\"}" }), + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + supports_structured_outputs: Some(true), + ..settings(&fetch) + }, + ); + + model + .do_generate(&call_options(json!({ + "responseFormat": { + "type": "json", + "name": "test-name", + "description": "test description", + }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-4o-2024-08-06", + "response_format": { + "type": "json_object" + } + }) + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › should send request body + #[tokio::test] + async fn should_send_request_body() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ "content": "" }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + // Upstream's `request.body` is the JSON string; aimux keeps the value. + let mut expected = json!({ + "body": "{\"model\":\"grok-3\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}" + }); + expected["body"] = serde_json::from_str(expected["body"].as_str().unwrap()).unwrap(); + assert_eq!(result_json(&result)["request"], expected); + } + + mod usage_details { + use super::*; + + fn usage_response(usage: Value) -> Value { + json!({ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1711115037, + "model": "grok-3", + "choices": [{ + "index": 0, + "message": { "role": "assistant", "content": "Hello!" }, + "finish_reason": "stop", + }], + "usage": usage, + }) + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › usage details › should extract detailed token usage when available + #[tokio::test] + async fn should_extract_detailed_token_usage_when_available() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "usage": { + "prompt_tokens": 20, + "completion_tokens": 30, + "total_tokens": 50, + "prompt_tokens_details": { "cached_tokens": 5 }, + "completion_tokens_details": { + "reasoning_tokens": 10, + "accepted_prediction_tokens": 15, + "rejected_prediction_tokens": 5, + }, + }, + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + let result = result_json(&result); + + assert_eq!( + result["usage"], + json!({ + "inputTokens": { + "cacheRead": 5, + "noCache": 15, + "total": 20 + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30 + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "accepted_prediction_tokens": 15, + "reasoning_tokens": 10, + "rejected_prediction_tokens": 5 + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5 + }, + "total_tokens": 50 + } + }) + ); + assert_eq!( + result["providerMetadata"], + json!({ + "test-provider": { + "acceptedPredictionTokens": 15, + "rejectedPredictionTokens": 5 + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › usage details › should handle missing token details + #[tokio::test] + async fn should_handle_missing_token_details() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "usage": { "prompt_tokens": 20, "completion_tokens": 30 }, + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["providerMetadata"]["test-provider"], + json!({}) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › usage details › should handle partial token details + #[tokio::test] + async fn should_handle_partial_token_details() { + let fetch = MockFetch::new(vec![prepare_json_response(json!({ + "usage": { + "prompt_tokens": 20, + "completion_tokens": 30, + "total_tokens": 50, + "prompt_tokens_details": { "cached_tokens": 5 }, + "completion_tokens_details": { "reasoning_tokens": 10 }, + }, + }))]); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["usage"], + json!({ + "inputTokens": { + "cacheRead": 5, + "noCache": 15, + "total": 20 + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30 + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "reasoning_tokens": 10 + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5 + }, + "total_tokens": 50 + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › usage details › should preserve extra usage fields from provider-specific responses + #[tokio::test] + async fn should_preserve_extra_usage_fields_from_provider_specific_responses() { + let fetch = json_mock(usage_response(json!({ + "prompt_tokens": 18, + "completion_tokens": 439, + "total_tokens": 457, + "queue_time": 0.061348671, + "prompt_time": 0.000211569, + "completion_time": 0.798181818, + "total_time": 0.798393387, + }))); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["usage"]["raw"], + json!({ + "completion_time": 0.798181818, + "completion_tokens": 439, + "prompt_time": 0.000211569, + "prompt_tokens": 18, + "queue_time": 0.061348671, + "total_time": 0.798393387, + "total_tokens": 457 + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doGenerate › usage details › should preserve extra usage fields nested inside token details + #[tokio::test] + async fn should_preserve_extra_usage_fields_nested_inside_token_details() { + let fetch = json_mock(usage_response(json!({ + "prompt_tokens": 100, + "completion_tokens": 50, + "total_tokens": 150, + "prompt_tokens_details": { "cached_tokens": 80, "cache_type": "ephemeral" }, + "completion_tokens_details": { "reasoning_tokens": 10, "provider_specific_detail": 7 }, + }))); + + let result = model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["usage"]["raw"], + json!({ + "completion_tokens": 50, + "completion_tokens_details": { + "provider_specific_detail": 7, + "reasoning_tokens": 10 + }, + "prompt_tokens": 100, + "prompt_tokens_details": { + "cache_type": "ephemeral", + "cached_tokens": 80 + }, + "total_tokens": 150 + }) + ); + } + } +} + +mod do_stream { + use super::*; + + fn no_raw_chunks() -> Value { + json!({ "includeRawChunks": false }) + } + + /// `{ ...no_raw_chunks(), ...extra }`. + fn stream_options(extra: Value) -> CallOptions { + let mut options = no_raw_chunks(); + options + .as_object_mut() + .unwrap() + .extend(extra.as_object().unwrap().clone()); + call_options(options) + } + + /// `tools: [{ type: 'function', name: 'test-tool', inputSchema: { value } }]`. + fn test_tool_options() -> CallOptions { + stream_options(json!({ "tools": [function_tool("test-tool", value_schema())] })) + } + + mod text_fixture { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › text (fixture) › should stream text content + #[tokio::test] + async fn should_stream_text_content() { + let fetch = MockFetch::new(vec![prepare_chunks_fixture_response("xai-text", &[])]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + let expected: Value = + serde_json::from_str(include_str!("snapshots/do_stream_text_fixture.json")) + .unwrap(); + assert_eq!(Value::Array(stream_parts(result).await), expected); + } + } + + mod tool_call_fixture { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › tool call (fixture) › should stream tool call content + #[tokio::test] + async fn should_stream_tool_call_content() { + let fetch = MockFetch::new(vec![prepare_chunks_fixture_response("xai-tool-call", &[])]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + let expected: Value = + serde_json::from_str(include_str!("snapshots/do_stream_tool_call_fixture.json")) + .unwrap(); + assert_eq!(Value::Array(stream_parts(result).await), expected); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should expose the raw response headers + #[tokio::test] + async fn should_expose_the_raw_response_headers() { + let fetch = MockFetch::new(vec![prepare_chunks_fixture_response( + "xai-text", + &[("test-header", "test-value")], + )]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(result.response.unwrap().headers).unwrap(), + json!({ + "cache-control": "no-cache", + "connection": "keep-alive", + "content-type": "text/event-stream", + "test-header": "test-value" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should report an error when $scenario without a finish reason + #[tokio::test] + async fn should_report_an_error_when_scenario_without_a_finish_reason() { + let scenarios: [(&str, &[&str]); 2] = [ + ("the connection closes", &[]), + ("[DONE] is received", &["data: [DONE]\n\n"]), + ]; + for (scenario, final_chunks) in scenarios { + let mut chunks = data(&[ + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","choices":[{"index":0,"delta":{"content":" World"},"finish_reason":null}]}"#, + ]); + chunks.extend(final_chunks.iter().map(|chunk| (*chunk).to_string())); + let fetch = MockFetch::new(vec![stream_chunks(&chunks, &[])]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let events = stream_parts(result).await; + + assert_eq!( + of_type(&events, "text-delta"), + [ + &json!({ "type": "text-delta", "delta": "Hello", "id": "txt-0" }), + &json!({ "type": "text-delta", "delta": " World", "id": "txt-0" }), + ], + "{scenario}" + ); + let errors = of_type(&events, "error"); + assert_eq!(errors.len(), 1, "{scenario}"); + assert_eq!( + errors[0]["error"]["name"], "AI_InvalidResponseDataError", + "{scenario}" + ); + assert_match_object( + &errors[0]["error"], + &json!({ "message": "Response stream ended without a finish reason." }), + ); + let finishes = of_type(&events, "finish"); + assert_eq!(finishes.len(), 1, "{scenario}"); + assert_eq!(finishes[0]["type"], "finish", "{scenario}"); + assert_eq!( + finishes[0]["finishReason"], + json!({ "unified": "error" }), + "{scenario}" + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should handle empty string role in delta chunks + #[tokio::test] + async fn should_handle_empty_string_role_in_delta_chunks() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","choices":[{"index":0,"delta":{"role":"","content":"Hello"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","choices":[{"index":0,"delta":{"role":"","content":" World"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1702657020,"model":"grok-3","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let events = stream_parts(result).await; + + assert_eq!( + of_type(&events, "text-delta"), + [ + &json!({ "type": "text-delta", "delta": "Hello", "id": "txt-0" }), + &json!({ "type": "text-delta", "delta": " World", "id": "txt-0" }), + ] + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should respect the includeUsage option + #[tokio::test] + async fn should_respect_the_include_usage_option() { + let fetch = MockFetch::new(vec![prepare_stream_response( + &["Hello", ", ", "World!"], + "stop", + )]); + let model = direct_model( + "gpt-4o-2024-08-06", + OpenAICompatibleProviderSettings { + include_usage: Some(true), + ..settings(&fetch) + }, + ); + + model.do_stream(&stream_options(json!({}))).await.unwrap(); + let body = request_body(&fetch); + + assert_eq!(body["stream"], true); + assert_eq!( + body["stream_options"], + json!({ + "include_usage": true + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream reasoning content before text deltas + #[tokio::test] + async fn should_stream_reasoning_content_before_text_deltas() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":"", "reasoning_content":"Let me think"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"", "reasoning_content":" about this"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"Here's"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" my response"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":18,"completion_tokens":439}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "reasoning-0", + "type": "reasoning-start" + }, + { + "delta": "Let me think", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " about this", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "id": "reasoning-0", + "type": "reasoning-end" + }, + { + "id": "txt-0", + "type": "text-start" + }, + { + "delta": "Here's", + "id": "txt-0", + "type": "text-delta" + }, + { + "delta": " my response", + "id": "txt-0", + "type": "text-delta" + }, + { + "id": "txt-0", + "type": "text-end" + }, + { + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should keep reasoning active when deltas include empty tool calls + #[tokio::test] + async fn should_keep_reasoning_active_when_deltas_include_empty_tool_calls() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","model":"test-model","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"Think ","tool_calls":[]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","model":"test-model","choices":[{"index":0,"delta":{"content":"","reasoning_content":"more...","tool_calls":[]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","model":"test-model","choices":[{"index":0,"delta":{"content":"Hello","reasoning_content":"","tool_calls":[]},"finish_reason":"stop"}]}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let events = stream_parts(result).await; + + assert_eq!( + events + .iter() + .filter(|event| event["type"] + .as_str() + .is_some_and(|kind| kind.starts_with("reasoning-"))) + .collect::>(), + [ + &json!({ "type": "reasoning-start", "id": "reasoning-0" }), + &json!({ "type": "reasoning-delta", "id": "reasoning-0", "delta": "Think " }), + &json!({ "type": "reasoning-delta", "id": "reasoning-0", "delta": "more..." }), + &json!({ "type": "reasoning-end", "id": "reasoning-0" }), + ] + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream reasoning from reasoning field when reasoning_content is not provided + #[tokio::test] + async fn should_stream_reasoning_from_reasoning_field_when_reasoning_content_is_not_provided() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":"", "reasoning":"Let me consider"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"", "reasoning":" this carefully"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"My answer is"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":" correct"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":18,"completion_tokens":439}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "reasoning-0", + "type": "reasoning-start" + }, + { + "delta": "Let me consider", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " this carefully", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "id": "reasoning-0", + "type": "reasoning-end" + }, + { + "id": "txt-0", + "type": "text-start" + }, + { + "delta": "My answer is", + "id": "txt-0", + "type": "text-delta" + }, + { + "delta": " correct", + "id": "txt-0", + "type": "text-delta" + }, + { + "id": "txt-0", + "type": "text-end" + }, + { + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should prefer reasoning_content over reasoning field in streaming when both are provided + #[tokio::test] + async fn should_prefer_reasoning_content_over_reasoning_field_in_streaming() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":"", "reasoning_content":"From reasoning_content", "reasoning":"From reasoning"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"content":"Final response"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":18,"completion_tokens":439}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "reasoning-0", + "type": "reasoning-start" + }, + { + "delta": "From reasoning_content", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "id": "reasoning-0", + "type": "reasoning-end" + }, + { + "id": "txt-0", + "type": "text-start" + }, + { + "delta": "Final response", + "id": "txt-0", + "type": "text-delta" + }, + { + "id": "txt-0", + "type": "text-end" + }, + { + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_tokens": 439, + "prompt_tokens": 18 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream tool deltas + #[tokio::test] + async fn should_stream_tool_deltas() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}"#, + ]); + + let result = model(&fetch).do_stream(&test_tool_options()).await.unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-input-start" + }, + { + "delta": "{\"", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "value", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "\":\"", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "Spark", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "le", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": " Day", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "\"}", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-end" + }, + { + "input": "{\"value\":\"Sparkle Day\"}", + "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_time": 0.798181818, + "completion_tokens": 439, + "prompt_time": 0.000211569, + "prompt_tokens": 18, + "queue_time": 0.061348671, + "total_time": 0.798393387, + "total_tokens": 457 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream tool deltas when function.name arrives in a later chunk + #[tokio::test] + async fn should_stream_tool_deltas_when_function_name_arrives_in_a_later_chunk() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant","content":null},"finish_reason":null}]}"#, + // First tool_calls delta carries id and (empty) arguments but no function.name + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_late","type":"function","function":{"arguments":""}}]},"finish_reason":null}]}"#, + // function.name arrives in the next chunk together with the start of the arguments + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_late","type":"function","function":{"name":"test-tool","arguments":"{\""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"value"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\"hi\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-late-name","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":18,"completion_tokens":10,"total_tokens":28}}"#, + ]); + + let result = model(&fetch).do_stream(&test_tool_options()).await.unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-late-name", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "call_late", + "toolName": "test-tool", + "type": "tool-input-start" + }, + { + "delta": "{\"", + "id": "call_late", + "type": "tool-input-delta" + }, + { + "delta": "value", + "id": "call_late", + "type": "tool-input-delta" + }, + { + "delta": "\":\"hi\"}", + "id": "call_late", + "type": "tool-input-delta" + }, + { + "id": "call_late", + "type": "tool-input-end" + }, + { + "input": "{\"value\":\"hi\"}", + "toolCallId": "call_late", + "toolName": "test-tool", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 10, + "total": 10 + }, + "raw": { + "completion_tokens": 10, + "prompt_tokens": 18, + "total_tokens": 28 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream a tool call whose index starts at one + #[tokio::test] + async fn should_stream_a_tool_call_whose_index_starts_at_one() { + let fetch = MockFetch::new(vec![prepare_sse_fixture_response( + "anthropic-fallback-tool-call", + )]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("read_file", json!({ + "type": "object", + "properties": { "path": { "type": "string" } }, + }))], + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + of_type(&parts, "text-delta") + .iter() + .map(|part| part["delta"].as_str().unwrap()) + .collect::(), + "Reading it." + ); + assert_eq!( + of_type(&parts, "tool-call"), + [&json!({ + "type": "tool-call", + "toolCallId": "toolu_sanitized", + "toolName": "read_file", + "input": "{\"path\": \"a.txt\"}", + })] + ); + assert_eq!(parts.last().unwrap()["type"], "finish"); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should keep id-less tool calls distinct when the index is reused + #[tokio::test] + async fn should_keep_id_less_tool_calls_distinct_when_the_index_is_reused() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-reused-index","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"type":"function","function":{"name":"read_file","arguments":"{\"path\":\"p0\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-reused-index","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"name":"write_file","arguments":"{\"path\":\"p1\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-reused-index","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"name":"read_file","arguments":"{\"path\":\"p2\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-reused-index","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":18,"completion_tokens":10,"total_tokens":28}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let parts = stream_parts(result).await; + let tool_calls = of_type(&parts, "tool-call"); + + assert_eq!( + tool_calls + .iter() + .map(|call| json!({ "toolName": call["toolName"], "input": call["input"] })) + .collect::>(), + [ + json!({ "toolName": "read_file", "input": "{\"path\":\"p0\"}" }), + json!({ "toolName": "write_file", "input": "{\"path\":\"p1\"}" }), + json!({ "toolName": "read_file", "input": "{\"path\":\"p2\"}" }), + ] + ); + assert!( + tool_calls + .iter() + .all(|call| !call["toolCallId"].as_str().unwrap().trim().is_empty()) + ); + assert_eq!( + tool_calls + .iter() + .map(|call| call["toolCallId"].as_str().unwrap()) + .collect::>() + .len(), + 3 + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should error when streamed tool call never receives a function.name + #[tokio::test] + async fn should_error_when_streamed_tool_call_never_receives_a_function_name() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant","content":null},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_missing","type":"function","function":{"arguments":"{}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-no-name","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":18,"completion_tokens":10,"total_tokens":28}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("test-tool", json!({ + "type": "object", + "properties": { "value": { "type": "string" } }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + let error = convert_stream_to_array(result).await.unwrap_err(); + + // `[AI_InvalidResponseDataError: Expected 'function.name' to be a string.]` + assert_eq!( + error_json(&error), + json!({ + "name": "AI_InvalidResponseDataError", + "message": "Expected 'function.name' to be a string.", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream tool call with thought signature from extra_content + #[tokio::test] + async fn should_stream_tool_call_with_thought_signature_from_extra_content() { + let fetch = stream_mock(&[ + // First chunk with tool call start and thought signature in extra_content + r#"{"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"function-call-1","type":"function","function":{"name":"check_flight","arguments":""},"extra_content":{"google":{"thought_signature":""}}}]},"finish_reason":null}]}"#, + // Subsequent chunks with arguments + r#"{"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"flight\":"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"AA100\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-gemini-thought","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("check_flight", json!({ + "type": "object", + "properties": { "flight": { "type": "string" } }, + "required": ["flight"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + let result = stream_parts(result).await; + + assert_match_object( + find(&result, "tool-call"), + &json!({ + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": "{\"flight\":\"AA100\"}", + "providerMetadata": { "test-provider": { "thoughtSignature": "" } }, + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream parallel tool calls with signature only on first call + #[tokio::test] + async fn should_stream_parallel_tool_calls_with_signature_only_on_first_call() { + let fetch = stream_mock(&[ + // First chunk with two tool calls - only first has thought signature + r#"{"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call-paris","type":"function","function":{"name":"get_weather","arguments":""},"extra_content":{"google":{"thought_signature":""}}},{"index":1,"id":"call-london","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}"#, + // Arguments for first call + r#"{"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"location\":\"Paris\"}"}}]},"finish_reason":null}]}"#, + // Arguments for second call + r#"{"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"location\":\"London\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-gemini-parallel","object":"chat.completion.chunk","created":1711357598,"model":"gemini-3-pro","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("get_weather", json!({ + "type": "object", + "properties": { "location": { "type": "string" } }, + "required": ["location"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + let result = stream_parts(result).await; + let tool_call_events = of_type(&result, "tool-call"); + + assert_eq!(tool_call_events.len(), 2); + // First tool call should have thought signature + assert_match_object( + tool_call_events[0], + &json!({ + "type": "tool-call", + "toolCallId": "call-paris", + "toolName": "get_weather", + "providerMetadata": { "test-provider": { "thoughtSignature": "" } }, + }), + ); + // Second tool call should NOT have thought signature + assert_match_object( + tool_call_events[1], + &json!({ + "type": "tool-call", + "toolCallId": "call-london", + "toolName": "get_weather", + }), + ); + assert_eq!(tool_call_events[1].get("providerMetadata"), None); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream tool call deltas when tool call arguments are passed in the first chunk + #[tokio::test] + async fn should_stream_tool_call_deltas_when_tool_call_arguments_are_passed_in_the_first_chunk() + { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"va"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"lue"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Spark"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"le"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" Day"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}"#, + ]); + + let result = model(&fetch).do_stream(&test_tool_options()).await.unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-input-start" + }, + { + "delta": "{\"", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "va", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "lue", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "\":\"", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "Spark", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "le", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": " Day", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "delta": "\"}", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-end" + }, + { + "input": "{\"value\":\"Sparkle Day\"}", + "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_time": 0.798181818, + "completion_tokens": 439, + "prompt_time": 0.000211569, + "prompt_tokens": 18, + "queue_time": 0.061348671, + "total_time": 0.798393387, + "total_tokens": 457 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should not duplicate tool calls when there is an additional empty chunk after the tool call has been completed + #[tokio::test] + async fn should_not_duplicate_tool_calls_when_there_is_an_additional_empty_chunk() { + let fetch = stream_mock(&[ + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":226,"completion_tokens":0}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"id":"chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa","type":"function","index":0,"function":{"name":"searchGoogle"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":233,"completion_tokens":7}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"query\": \""}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":241,"completion_tokens":15}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"latest"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":242,"completion_tokens":16}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" news"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":243,"completion_tokens":17}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" on"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":244,"completion_tokens":18}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" ai\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":{"prompt_tokens":226,"total_tokens":245,"completion_tokens":19}}"#, + // empty arguments chunk after the tool call has already been finished: + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":""}}]},"logprobs":null,"finish_reason":"tool_calls","stop_reason":128008}],"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}"#, + r#"{"id":"chat-2267f7e2910a4254bac0650ba74cfc1c","object":"chat.completion.chunk","created":1733162241,"model":"meta/llama-3.1-8b-instruct:fp8","choices":[],"usage":{"prompt_tokens":226,"total_tokens":246,"completion_tokens":20}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("searchGoogle", json!({ + "type": "object", + "properties": { "query": { "type": "string" } }, + "required": ["query"], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chat-2267f7e2910a4254bac0650ba74cfc1c", + "modelId": "meta/llama-3.1-8b-instruct:fp8", + "timestamp": "2024-12-02T17:57:21.000Z", + "type": "response-metadata" + }, + { + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "toolName": "searchGoogle", + "type": "tool-input-start" + }, + { + "delta": "{\"query\": \"", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "delta": "latest", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "delta": " news", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "delta": " on", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "delta": " ai\"}", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "delta": "", + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-delta" + }, + { + "id": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "type": "tool-input-end" + }, + { + "input": "{\"query\": \"latest news on ai\"}", + "toolCallId": "chatcmpl-tool-b3b307239370432d9910d4b79b4dbbaa", + "toolName": "searchGoogle", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 226, + "total": 226 + }, + "outputTokens": { + "reasoning": 0, + "text": 20, + "total": 20 + }, + "raw": { + "completion_tokens": 20, + "prompt_tokens": 226, + "total_tokens": 246 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream tool call that is sent in one chunk + #[tokio::test] + async fn should_stream_tool_call_that_is_sent_in_one_chunk() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":"{\"value\":\"Sparkle Day\"}"}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}"#, + ]); + + let result = model(&fetch).do_stream(&test_tool_options()).await.unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-input-start" + }, + { + "delta": "{\"value\":\"Sparkle Day\"}", + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-delta" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-end" + }, + { + "input": "{\"value\":\"Sparkle Day\"}", + "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_time": 0.798181818, + "completion_tokens": 439, + "prompt_time": 0.000211569, + "prompt_tokens": 18, + "queue_time": 0.061348671, + "total_time": 0.798393387, + "total_tokens": 457 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should stream empty tool call that is sent in one chunk + #[tokio::test] + async fn should_stream_empty_tool_call_that_is_sent_in_one_chunk() { + let fetch = stream_mock(&[ + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1711357598,"model":"grok-3","system_fingerprint":"fp_3bc1b5746c","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_O17Uplv4lJvD6DVdIvFFeRMw","type":"function","function":{"name":"test-tool","arguments":""}}]},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798","object":"chat.completion.chunk","created":1729171479,"model":"grok-3","system_fingerprint":"fp_10c08bf97d","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"queue_time":0.061348671,"prompt_tokens":18,"prompt_time":0.000211569,"completion_tokens":439,"completion_time":0.798181818,"total_tokens":457,"total_time":0.798393387}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({ + "tools": [function_tool("test-tool", json!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#", + }))], + }))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "chatcmpl-e7f8e220-656c-4455-a132-dacfc1370798", + "modelId": "grok-3", + "timestamp": "2024-03-25T09:06:38.000Z", + "type": "response-metadata" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-input-start" + }, + { + "id": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "type": "tool-input-end" + }, + { + "input": "", + "toolCallId": "call_O17Uplv4lJvD6DVdIvFFeRMw", + "toolName": "test-tool", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 0, + "noCache": 18, + "total": 18 + }, + "outputTokens": { + "reasoning": 0, + "text": 439, + "total": 439 + }, + "raw": { + "completion_time": 0.798181818, + "completion_tokens": 439, + "prompt_time": 0.000211569, + "prompt_tokens": 18, + "queue_time": 0.061348671, + "total_time": 0.798393387, + "total_tokens": 457 + } + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should preserve structured error stream parts + #[tokio::test] + async fn should_preserve_structured_error_stream_parts() { + let fetch = stream_mock(&[ + r#"{"error": {"message": "Context length exceeded", "code": "CONTEXT_LENGTH_EXCEEDED"}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "error": { + "code": "CONTEXT_LENGTH_EXCEEDED", + "message": "Context length exceeded" + }, + "type": "error" + }, + { + "finishReason": { + "unified": "error" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": {}, + "outputTokens": {} + } + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should handle unparsable stream parts + #[tokio::test] + async fn should_handle_unparsable_stream_parts() { + let fetch = stream_mock(&["{unparsable}"]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let mut parts = stream_parts(result).await; + + let mut expected = json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "error": { + "name": "AI_JSONParseError", + "message": "JSON parsing failed: Text: {unparsable}.\nError message: SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)" + }, + "type": "error" + }, + { + "finishReason": { + "unified": "error" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": {}, + "outputTokens": {} + } + } + ]); + // The `SyntaxError` text after "Error message: " is the JS runtime's + // `JSON.parse` message; only the part before it is compared. + let runtime_specific = |parts: &mut Vec| { + let message = parts[1]["error"]["message"].take(); + let message = message.as_str().unwrap_or_default(); + message + .split_once("\nError message: ") + .map_or(message.to_string(), |(prefix, _)| prefix.to_string()) + }; + let message = runtime_specific(&mut parts); + let expected_message = runtime_specific(expected.as_array_mut().unwrap()); + assert_eq!(Value::Array(parts), expected); + assert_eq!(message, expected_message); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should pass the messages and the model + #[tokio::test] + async fn should_pass_the_messages_and_the_model() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "stream": true + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should pass video input as video_url in doStream + #[tokio::test] + async fn should_pass_video_input_as_video_url_in_do_stream() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + model(&fetch) + .do_stream(&stream_options(json!({ + "prompt": [{ + "role": "user", + "content": [{ + "type": "file", + "data": { "type": "url", "url": "https://example.com/video.mp4" }, + "mediaType": "video/mp4", + }], + }], + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ + "messages": [{ + "role": "user", + "content": [{ + "type": "video_url", + "video_url": { "url": "https://example.com/video.mp4" }, + }], + }], + "stream": true, + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + let provider = create_openai_compatible(OpenAICompatibleProviderSettings { + base_url: "https://my.api.com/v1".into(), + headers: Some(headers(&[ + ("Authorization", "Bearer test-api-key"), + ("Custom-Provider-Header", "provider-header-value"), + ])), + ..settings(&fetch) + }) + .unwrap(); + + provider + .chat("grok-3") + .do_stream(&stream_options(json!({ + "headers": { "Custom-Request-Header": "request-header-value" }, + }))) + .await + .unwrap(); + + assert_eq!( + request_headers(&fetch.seen()[0]), + json!({ + "authorization": "Bearer test-api-key", + "content-type": "application/json", + "custom-provider-header": "provider-header-value", + "custom-request-header": "request-header-value" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should include provider-specific options + #[tokio::test] + async fn should_include_provider_specific_options() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "test-provider": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "someCustomOption": "test-value", + "stream": true + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should not include provider-specific options for different provider + #[tokio::test] + async fn should_not_include_provider_specific_options_for_different_provider() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "notThisProviderName": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "stream": true + }) + ); + } + + mod camel_case_provider_options { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should accept camelCase provider options key for hyphenated provider name + #[tokio::test] + async fn should_accept_camel_case_provider_options_key_for_hyphenated_provider_name() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "testProvider": { "someCustomOption": "test-value" } }, + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ "someCustomOption": "test-value" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should prefer camelCase options over raw-name options + #[tokio::test] + async fn should_prefer_camel_case_options_over_raw_name_options() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { + "test-provider": { "someCustomOption": "raw-value" }, + "testProvider": { "someCustomOption": "camel-value" }, + }, + }))) + .await + .unwrap(); + + assert_match_object( + &request_body(&fetch), + &json!({ "someCustomOption": "camel-value" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should emit deprecated warning when raw provider options key is used + #[tokio::test] + async fn should_emit_deprecated_warning_when_raw_provider_options_key_is_used() { + let fetch = MockFetch::new(vec![prepare_stream_response(&["Hello"], "stop")]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "test-provider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + find(&parts, "stream-start")["warnings"], + json!([ + { + "message": "Use 'testProvider' instead.", + "setting": "providerOptions key 'test-provider'", + "type": "deprecated" + } + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should not emit deprecated warning when camelCase provider options key is used + #[tokio::test] + async fn should_not_emit_deprecated_warning_when_camel_case_provider_options_key_is_used() { + let fetch = MockFetch::new(vec![prepare_stream_response(&["Hello"], "stop")]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "testProvider": { "reasoningEffort": "high" } }, + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!(find(&parts, "stream-start")["warnings"], json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should use camelCase metadata key in finish event when camelCase options are used + #[tokio::test] + async fn should_use_camel_case_metadata_key_in_finish_event_when_camel_case_options_are_used() + { + let fetch = MockFetch::new(vec![prepare_stream_response(&["Hello"], "stop")]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "testProvider": {} }, + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + let finish_part = find(&parts, "finish"); + + assert!( + finish_part["providerMetadata"] + .get("testProvider") + .is_some() + ); + assert!( + finish_part["providerMetadata"] + .get("test-provider") + .is_none() + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should use raw metadata key in finish event when raw options are used + #[tokio::test] + async fn should_use_raw_metadata_key_in_finish_event_when_raw_options_are_used() { + let fetch = MockFetch::new(vec![prepare_stream_response(&["Hello"], "stop")]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "test-provider": {} }, + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert!( + find(&parts, "finish")["providerMetadata"] + .get("test-provider") + .is_some() + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should use raw metadata key in finish event when no provider options are passed + #[tokio::test] + async fn should_use_raw_metadata_key_in_finish_event_when_no_provider_options_are_passed() { + let fetch = MockFetch::new(vec![prepare_stream_response(&["Hello"], "stop")]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert!( + find(&parts, "finish")["providerMetadata"] + .get("test-provider") + .is_some() + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › camelCase provider options › should use camelCase metadata key for thought signatures in streamed tool calls + #[tokio::test] + async fn should_use_camel_case_metadata_key_for_thought_signatures_in_streamed_tool_calls() + { + let fetch = stream_mock(&[ + r#"{"id":"chat-id","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call-1","type":"function","function":{"name":"test_tool","arguments":"{\"a\":1}"},"extra_content":{"google":{"thought_signature":""}}}]},"finish_reason":null}]}"#, + r#"{"id":"chat-id","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}"#, + ]); + + let result = provider(&fetch) + .chat("grok-3") + .do_stream(&stream_options(json!({ + "providerOptions": { "testProvider": {} }, + }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_match_object( + find(&parts, "tool-call"), + &json!({ + "type": "tool-call", + "providerMetadata": { "testProvider": { "thoughtSignature": "" } }, + }), + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should send request body + #[tokio::test] + async fn should_send_request_body() { + let fetch = MockFetch::new(vec![prepare_stream_response(&[], "stop")]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(result.request).unwrap(), + json!({ + "body": { + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "grok-3", + "stream": true + } + }) + ); + } + + mod usage_details_in_streaming { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › usage details in streaming › should extract detailed token usage from stream finish + #[tokio::test] + async fn should_extract_detailed_token_usage_from_stream_finish() { + let fetch = stream_mock(&[ + r#"{"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}"#, + r#"{"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":20,"completion_tokens":30,"prompt_tokens_details":{"cached_tokens":5},"completion_tokens_details":{"reasoning_tokens":10,"accepted_prediction_tokens":15,"rejected_prediction_tokens":5}}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + find(&parts, "finish"), + &json!({ + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": { + "acceptedPredictionTokens": 15, + "rejectedPredictionTokens": 5 + } + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 5, + "noCache": 15, + "total": 20 + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30 + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "accepted_prediction_tokens": 15, + "reasoning_tokens": 10, + "rejected_prediction_tokens": 5 + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5 + } + } + } + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › usage details in streaming › should handle missing token details in stream + #[tokio::test] + async fn should_handle_missing_token_details_in_stream() { + let fetch = stream_mock(&[ + r#"{"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}"#, + r#"{"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":20,"completion_tokens":30}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + find(&parts, "finish")["providerMetadata"]["test-provider"], + json!({}) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › usage details in streaming › should handle partial token details in stream + #[tokio::test] + async fn should_handle_partial_token_details_in_stream() { + let fetch = stream_mock(&[ + r#"{"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}"#, + r#"{"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":20,"completion_tokens":30,"total_tokens":50,"prompt_tokens_details":{"cached_tokens":5},"completion_tokens_details":{"reasoning_tokens":10}}}"#, + ]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + find(&parts, "finish"), + &json!({ + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 5, + "noCache": 15, + "total": 20 + }, + "outputTokens": { + "reasoning": 10, + "text": 20, + "total": 30 + }, + "raw": { + "completion_tokens": 30, + "completion_tokens_details": { + "reasoning_tokens": 10 + }, + "prompt_tokens": 20, + "prompt_tokens_details": { + "cached_tokens": 5 + }, + "total_tokens": 50 + } + } + }) + ); + } + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › doStream › should extract response metadata after an empty placeholder chunk + #[tokio::test] + async fn should_extract_response_metadata_after_an_empty_placeholder_chunk() { + let fetch = MockFetch::new(vec![prepare_chunks_fixture_response( + "placeholder-first-chunk", + &[], + )]); + + let result = model(&fetch) + .do_stream(&stream_options(json!({}))) + .await + .unwrap(); + let events = stream_parts(result).await; + + assert_eq!( + find(&events, "response-metadata"), + &json!({ + "type": "response-metadata", + "id": "chatcmpl-123", + "modelId": "actual-model-name", + // `new Date(1700000000 * 1000)` + "timestamp": "2023-11-14T22:13:20.000Z", + }) + ); + } +} + +mod metadata_extraction { + use super::*; + + /// The model of both tests: `metadataExtractor: testMetadataExtractor`. + fn extractor_model(fetch: &Arc) -> OpenAICompatibleChatModel { + direct_model( + "gpt-5", + OpenAICompatibleProviderSettings { + metadata_extractor: Some(Arc::new(TestMetadataExtractor)), + ..settings(fetch) + }, + ) + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › metadata extraction › should process metadata from complete response + #[tokio::test] + async fn should_process_metadata_from_complete_response() { + let fetch = json_mock(json!({ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1711115037, + "model": "gpt-5", + "choices": [{ + "index": 0, + "message": { "role": "assistant" }, + "finish_reason": "stop", + }], + "test_field": "test_value", + })); + + let result = extractor_model(&fetch) + .do_generate(&call_options(json!({}))) + .await + .unwrap(); + + assert_eq!( + result_json(&result)["providerMetadata"], + json!({ "test-provider": { "value": "test_value" } }) + ); + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-5" + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › metadata extraction › should process metadata from streaming response + #[tokio::test] + async fn should_process_metadata_from_streaming_response() { + let fetch = stream_mock(&[ + r#"{"choices":[{"delta":{"content":"Hello"}}]}"#, + r#"{"choices":[{"finish_reason":"stop"}],"test_field":"test_value"}"#, + ]); + + let result = extractor_model(&fetch) + .do_stream(&call_options(json!({ "includeRawChunks": false }))) + .await + .unwrap(); + let parts = stream_parts(result).await; + + assert_eq!( + find(&parts, "finish")["providerMetadata"], + json!({ "test-provider": { "value": "test_value" } }) + ); + assert_eq!( + request_body(&fetch), + json!({ + "messages": [ + { + "content": "Hello", + "role": "user" + } + ], + "model": "gpt-5", + "stream": true + }) + ); + } +} + +mod raw_chunks { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › raw chunks › should include raw chunks when includeRawChunks is true + #[tokio::test] + async fn should_include_raw_chunks_when_include_raw_chunks_is_true() { + let fetch = stream_mock(&[ + r#"{"id":"chat-id","choices":[{"delta":{"content":"Hello"}}]}"#, + r#"{"choices":[{"delta":{},"finish_reason":"stop"}]}"#, + ]); + + let result = model(&fetch) + .do_stream(&call_options(json!({ "includeRawChunks": true }))) + .await + .unwrap(); + + assert_eq!( + Value::Array(stream_parts(result).await), + json!([ + { + "type": "stream-start", + "warnings": [] + }, + { + "rawValue": { + "choices": [ + { + "delta": { + "content": "Hello" + } + } + ], + "id": "chat-id" + }, + "type": "raw" + }, + { + "id": "chat-id", + "type": "response-metadata" + }, + { + "id": "txt-0", + "type": "text-start" + }, + { + "delta": "Hello", + "id": "txt-0", + "type": "text-delta" + }, + { + "rawValue": { + "choices": [ + { + "delta": {}, + "finish_reason": "stop" + } + ] + }, + "type": "raw" + }, + { + "id": "txt-0", + "type": "text-end" + }, + { + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": {} + }, + "type": "finish", + "usage": { + "inputTokens": {}, + "outputTokens": {} + } + } + ]) + ); + } +} + +mod transform_request_body { + use super::*; + + /// `prepareTransformJsonResponse`. + fn prepare_transform_json_response() -> Arc { + json_mock(json!({ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1711115037, + "model": "grok-3", + "choices": [{ + "index": 0, + "message": { "role": "assistant", "content": "Hello!" }, + "finish_reason": "stop", + }], + "usage": { "prompt_tokens": 4, "total_tokens": 34, "completion_tokens": 30 }, + })) + } + + /// `prepareTransformStreamResponse`. + fn prepare_transform_stream_response() -> Arc { + stream_mock(&[ + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1711115037,"model":"grok-3","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}"#, + r#"{"id":"chatcmpl-test","object":"chat.completion.chunk","created":1711115037,"model":"grok-3","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":"stop"}],"usage":{"prompt_tokens":4,"completion_tokens":2,"total_tokens":6}}"#, + ]) + } + + /// `vi.fn(body => ({ ...body, custom_field: 'added-by-transform' }))`: + /// the transform and the bodies it was called with. + fn transform_fn() -> ( + crate::openai_compatible::TransformRequestBody, + Arc>>, + ) { + let calls = Arc::new(Mutex::new(Vec::new())); + let seen = calls.clone(); + let transform: crate::openai_compatible::TransformRequestBody = + Arc::new(move |mut body: Value| { + seen.lock().unwrap().push(body.clone()); + body["custom_field"] = json!("added-by-transform"); + body + }); + (transform, calls) + } + + /// `new OpenAICompatibleChatLanguageModel('grok-3', { provider: + /// 'test-provider', url: ({ path }) => `https://my.api.com/v1${path}`, + /// headers: () => ({}), ... })`. + fn transform_model( + fetch: &Arc, + transform: Option, + ) -> OpenAICompatibleChatModel { + direct_model( + "grok-3", + OpenAICompatibleProviderSettings { + base_url: "https://my.api.com/v1".into(), + transform_request_body: transform, + ..settings(fetch) + }, + ) + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › transformRequestBody › should transform request body in doGenerate when transformRequestBody is provided + #[tokio::test] + async fn should_transform_request_body_in_do_generate_when_transform_request_body_is_provided() + { + let (transform, calls) = transform_fn(); + let fetch = prepare_transform_json_response(); + let model = transform_model(&fetch, Some(transform)); + + model.do_generate(&call_options(json!({}))).await.unwrap(); + + // Verify transform was called + let calls = calls.lock().unwrap(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0]["model"], "grok-3"); + assert_eq!( + calls[0]["messages"], + json!([{ "role": "user", "content": "Hello" }]) + ); + // Verify transformed body was sent + assert_match_object( + &request_body(&fetch), + &json!({ "custom_field": "added-by-transform" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › transformRequestBody › should transform request body in doStream when transformRequestBody is provided + #[tokio::test] + async fn should_transform_request_body_in_do_stream_when_transform_request_body_is_provided() { + let (transform, calls) = transform_fn(); + let fetch = prepare_transform_stream_response(); + let model = transform_model(&fetch, Some(transform)); + + let result = model.do_stream(&call_options(json!({}))).await.unwrap(); + // Consume the stream + stream_parts(result).await; + + // Verify transform was called + let calls = calls.lock().unwrap(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0]["model"], "grok-3"); + assert_eq!( + calls[0]["messages"], + json!([{ "role": "user", "content": "Hello" }]) + ); + assert_eq!(calls[0]["stream"], true); + // Verify transformed body was sent + assert_match_object( + &request_body(&fetch), + &json!({ "custom_field": "added-by-transform" }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-chat-language-model.test.ts › transformRequestBody › should work without transformRequestBody + #[tokio::test] + async fn should_work_without_transform_request_body() { + let fetch = prepare_transform_json_response(); + let model = transform_model(&fetch, None); + + model.do_generate(&call_options(json!({}))).await.unwrap(); + let request_body = request_body(&fetch); + + assert_match_object(&request_body, &json!({ "model": "grok-3" })); + assert_eq!(request_body.get("custom_field"), None); + } +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/convert_chat_usage.rs b/aimux-providers/src/openai_compatible/tests/chat/convert_chat_usage.rs new file mode 100644 index 000000000..e88425b95 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/convert_chat_usage.rs @@ -0,0 +1,77 @@ +use serde_json::{Value, json}; + +use crate::openai_compatible::chat::usage_from_raw; + +/// `convertOpenAICompatibleChatUsage`: the Rust form takes the raw `usage` +/// value (absent for `undefined`); the result is compared in its JSON form, +/// where an absent count is an `undefined` key. +fn convert_open_ai_compatible_chat_usage(usage: Option<&Value>) -> Value { + serde_json::to_value(usage_from_raw(usage)).expect("usage serializes") +} + +mod convert_open_ai_compatible_chat_usage { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-openai-compatible-chat-usage.test.ts › convertOpenAICompatibleChatUsage › returns null usage when usage is missing + #[test] + fn returns_null_usage_when_usage_is_missing() { + // createNullLanguageModelUsage(): every count undefined, no raw. + assert_eq!( + convert_open_ai_compatible_chat_usage(None), + json!({ "inputTokens": {}, "outputTokens": {} }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-openai-compatible-chat-usage.test.ts › convertOpenAICompatibleChatUsage › splits completion tokens into text and reasoning + #[test] + fn splits_completion_tokens_into_text_and_reasoning() { + let usage = json!({ + "prompt_tokens": 10, + "completion_tokens": 20, + "completion_tokens_details": { "reasoning_tokens": 5 }, + }); + assert_eq!( + convert_open_ai_compatible_chat_usage(Some(&usage)), + json!({ + "inputTokens": { + "total": 10, + "noCache": 10, + "cacheRead": 0, + }, + "outputTokens": { "total": 20, "text": 15, "reasoning": 5 }, + "raw": { + "prompt_tokens": 10, + "completion_tokens": 20, + "completion_tokens_details": { "reasoning_tokens": 5 }, + }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-openai-compatible-chat-usage.test.ts › convertOpenAICompatibleChatUsage › clamps text tokens at 0 when reasoning exceeds completion + #[test] + fn clamps_text_tokens_at_0_when_reasoning_exceeds_completion() { + // Provider-inconsistent usage (Baseten Kimi-K3, finish_reason 'length'): + // completion_tokens undercounts the actual generation, so + // completion_tokens_details.reasoning_tokens > completion_tokens. + let usage = json!({ + "prompt_tokens": 951, + "completion_tokens": 6000, + "total_tokens": 6952, + "prompt_tokens_details": { "cached_tokens": 60 }, + "completion_tokens_details": { "reasoning_tokens": 6001 }, + }); + assert_eq!( + convert_open_ai_compatible_chat_usage(Some(&usage)), + json!({ + "inputTokens": { + "total": 951, + "noCache": 891, + "cacheRead": 60, + }, + "outputTokens": { "total": 6000, "text": 0, "reasoning": 6001 }, + "raw": usage, + }) + ); + } +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/convert_to_chat_messages.rs b/aimux-providers/src/openai_compatible/tests/chat/convert_to_chat_messages.rs new file mode 100644 index 000000000..85419b9d2 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/convert_to_chat_messages.rs @@ -0,0 +1,2076 @@ +use serde_json::{Value, json}; + +use aimux_core::error::AiMuxError; +use aimux_core::language_model_message::{LanguageModelMessage, LanguageModelPrompt, UserPart}; +use aimux_core::shared::{FileBytes, FileData}; + +use crate::openai_compatible::convert::{MessageSpec, convert_messages}; + +/// A prompt in its AI SDK JSON form. +fn prompt(value: Value) -> LanguageModelPrompt { + serde_json::from_value(value).expect("prompt") +} + +/// Replace the data of file part `part` of user message `message` with raw +/// bytes: the JSON form only carries base64 strings, upstream passes a +/// `Uint8Array` there. +fn with_uint8_array( + mut prompt: LanguageModelPrompt, + message: usize, + part: usize, + bytes: &[u8], +) -> LanguageModelPrompt { + let LanguageModelMessage::User { content, .. } = &mut prompt[message] else { + panic!("message {message} is not a user message"); + }; + let UserPart::File(file) = &mut content[part] else { + panic!("part {part} is not a file part"); + }; + file.data = FileData::Data { + data: FileBytes::Binary(bytes.to_vec()), + }; + prompt +} + +/// `convertToOpenAICompatibleChatMessages(prompt, { providerOptionsKey, +/// supportsMultiPartToolContent })`. +fn convert_with( + prompt: &LanguageModelPrompt, + provider_options_key: &str, + supports_multi_part_tool_content: bool, +) -> Result { + convert_messages( + prompt, + &MessageSpec { + metadata_key: provider_options_key, + supports_multi_part_tool_content, + }, + ) + .map(Value::Array) +} + +/// `convertToOpenAICompatibleChatMessages(prompt)` with upstream's defaults +/// (`providerOptionsKey: 'google'`, `supportsMultiPartToolContent: false`). +fn convert(prompt: &LanguageModelPrompt) -> Value { + convert_with(prompt, "google", false).expect("conversion succeeds") +} + +/// `expect(() => convert(prompt)).toThrow("'' functionality not +/// supported")`: an `UnsupportedFunctionalityError` for that functionality. +fn assert_unsupported(prompt: &LanguageModelPrompt, functionality: &str) { + match convert_with(prompt, "google", false) { + Err(AiMuxError::UnsupportedFunctionality(actual)) => { + assert_eq!(actual, functionality); + } + other => panic!("expected UnsupportedFunctionality({functionality:?}), got {other:?}"), + } +} + +/// `toMatchObject`: objects match by subset, arrays element-wise with the +/// same length, other values by equality. +fn assert_match_object(actual: &Value, expected: &Value) { + match (actual, expected) { + (Value::Object(actual_map), Value::Object(expected_map)) => { + for (key, expected_value) in expected_map { + let actual_value = actual_map + .get(key) + .unwrap_or_else(|| panic!("missing key {key:?} in {actual}")); + assert_match_object(actual_value, expected_value); + } + } + (Value::Array(actual_items), Value::Array(expected_items)) => { + assert_eq!( + actual_items.len(), + expected_items.len(), + "array length differs: {actual} vs {expected}" + ); + for (actual_item, expected_item) in actual_items.iter().zip(expected_items) { + assert_match_object(actual_item, expected_item); + } + } + _ => assert_eq!(actual, expected), + } +} + +/// `JSON.stringify(value)` (the JSON maps keep insertion order). +fn stringify(value: &Value) -> String { + value.to_string() +} + +mod user_messages { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with only a text part to a string content + #[test] + fn should_convert_messages_with_only_a_text_part_to_a_string_content() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [{ "type": "text", "text": "Hello" }], + }, + ]))); + + assert_eq!(result, json!([{ "role": "user", "content": "Hello" }])); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with image parts + #[test] + fn should_convert_messages_with_image_parts() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Hello" }, + { + "type": "file", + "data": { "type": "data", "data": "AAECAw==" }, + "mediaType": "image/png", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Hello" }, + { + "type": "image_url", + "image_url": { "url": "data:image/png;base64,AAECAw==" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with image parts from Uint8Array + #[test] + fn should_convert_messages_with_image_parts_from_uint8_array() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Hi" }, + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "image/png", + }, + ], + }, + ])), + 0, + 1, + &[0, 1, 2, 3], + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Hi" }, + { + "type": "image_url", + "image_url": { "url": "data:image/png;base64,AAECAw==" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should handle URL-based images + #[test] + fn should_handle_url_based_images() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": "https://example.com/image.jpg" }, + "mediaType": "image/*", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { "url": "https://example.com/image.jpg" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert video data parts to video_url data URLs + #[test] + fn should_convert_video_data_parts_to_video_url_data_urls() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Describe this video" }, + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "video/mp4", + "providerOptions": { + "openaiCompatible": { + "fps": 1, + }, + }, + }, + ], + }, + ])), + 0, + 1, + &[0, 1, 2, 3], + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Describe this video" }, + { + "type": "video_url", + "video_url": { "url": "data:video/mp4;base64,AAECAw==" }, + "fps": 1, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert base64-encoded video data parts + #[test] + fn should_convert_base64_encoded_video_data_parts() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "AAECAw==" }, + "mediaType": "video/webm", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "video_url", + "video_url": { "url": "data:video/webm;base64,AAECAw==" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should pass through video URLs + #[test] + fn should_pass_through_video_urls() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": "https://example.com/video.mp4" }, + "mediaType": "video/*", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "video_url", + "video_url": { "url": "https://example.com/video.mp4" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with audio/wav parts + #[test] + fn should_convert_messages_with_audio_wav_parts() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Transcribe this audio" }, + { + "type": "file", + "data": { "type": "data", "data": "AAECAw==" }, + "mediaType": "audio/wav", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Transcribe this audio" }, + { + "type": "input_audio", + "input_audio": { "data": "AAECAw==", "format": "wav" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with audio/mp3 parts + #[test] + fn should_convert_messages_with_audio_mp3_parts() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "audio/mp3", + }, + ], + }, + ])), + 0, + 0, + &[0, 1, 2, 3], + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "input_audio", + "input_audio": { "data": "AAECAw==", "format": "mp3" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with audio/mpeg parts to mp3 format + #[test] + fn should_convert_messages_with_audio_mpeg_parts_to_mp3_format() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "audio/mpeg", + }, + ], + }, + ])), + 0, + 0, + &[0, 1, 2, 3], + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "input_audio", + "input_audio": { "data": "AAECAw==", "format": "mp3" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should throw error for audio parts with URLs + #[test] + fn should_throw_error_for_audio_parts_with_urls() { + assert_unsupported( + &prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": "https://example.com/audio.wav" }, + "mediaType": "audio/wav", + }, + ], + }, + ])), + "audio file parts with URLs", + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should throw error for unsupported audio format + #[test] + fn should_throw_error_for_unsupported_audio_format() { + assert_unsupported( + &with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "audio/ogg", + }, + ], + }, + ])), + 0, + 0, + &[0, 1, 2, 3], + ), + "audio media type audio/ogg", + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with PDF parts + #[test] + fn should_convert_messages_with_pdf_parts() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Summarize this PDF" }, + { + "type": "file", + "data": { "type": "data", "data": "AAECAw==" }, + "mediaType": "application/pdf", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Summarize this PDF" }, + { + "type": "file", + "file": { + "filename": "document.pdf", + "file_data": "data:application/pdf;base64,AAECAw==", + }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with PDF parts using provided filename + #[test] + fn should_convert_messages_with_pdf_parts_using_provided_filename() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "application/pdf", + "filename": "report.pdf", + }, + ], + }, + ])), + 0, + 0, + &[0, 1, 2, 3], + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "file": { + "filename": "report.pdf", + "file_data": "data:application/pdf;base64,AAECAw==", + }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should throw error for PDF parts with URLs + #[test] + fn should_throw_error_for_pdf_parts_with_urls() { + assert_unsupported( + &prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": "https://example.com/document.pdf" }, + "mediaType": "application/pdf", + }, + ], + }, + ])), + "PDF file parts with URLs", + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with base64-encoded text/markdown parts + #[test] + fn should_convert_messages_with_base64_encoded_text_markdown_parts() { + use base64::Engine; + + let markdown_text = "# Hello World\n\nThis is **markdown** content."; + let base64_data = base64::engine::general_purpose::STANDARD.encode(markdown_text); + + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Summarize this document" }, + { + "type": "file", + "data": { "type": "data", "data": base64_data }, + "mediaType": "text/markdown", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Summarize this document" }, + { + "type": "text", + "text": markdown_text, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert messages with text/plain parts from Uint8Array + #[test] + fn should_convert_messages_with_text_plain_parts_from_uint8_array() { + let result = convert(&with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "text/plain", + }, + ], + }, + ])), + 0, + 0, + "Plain text content".as_bytes(), + )); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Plain text content", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should decode base64 string data for text/* file parts + #[test] + fn should_decode_base64_string_data_for_text_file_parts() { + use base64::Engine; + + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { + "type": "data", + "data": base64::engine::general_purpose::STANDARD + .encode("Plain text content"), + }, + "mediaType": "text/plain", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Plain text content", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should convert text file URL to string + #[test] + fn should_convert_text_file_url_to_string() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": "https://example.com/readme.md" }, + "mediaType": "text/markdown", + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "https://example.com/readme.md", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should throw error for unsupported file types + #[test] + fn should_throw_error_for_unsupported_file_types() { + assert_unsupported( + &with_uint8_array( + prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "data", "data": "" }, + "mediaType": "application/zip", + }, + ], + }, + ])), + 0, + 0, + &[0, 1, 2, 3], + ), + "file part media type application/zip", + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › user messages › should throw error for file parts with provider references + #[test] + fn should_throw_error_for_file_parts_with_provider_references() { + assert_unsupported( + &prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { + "type": "reference", + "reference": { "openaiCompatible": "file-123" }, + }, + "mediaType": "image/png", + }, + ], + }, + ])), + "file parts with provider references", + ); + } +} + +mod tool_calls { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › tool calls › should stringify arguments to tool calls + #[test] + fn should_stringify_arguments_to_tool_calls() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "input": { "foo": "bar123" }, + "toolCallId": "quux", + "toolName": "thwomp", + }, + ], + }, + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "quux", + "toolName": "thwomp", + "output": { "type": "json", "value": { "oof": "321rab" } }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "type": "function", + "id": "quux", + "function": { + "name": "thwomp", + "arguments": stringify(&json!({ "foo": "bar123" })), + }, + }, + ], + }, + { + "role": "tool", + "content": stringify(&json!({ "oof": "321rab" })), + "tool_call_id": "quux", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › tool calls › should send empty string content for assistant messages with no tool calls + #[test] + fn should_send_empty_string_content_for_assistant_messages_with_no_tool_calls() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [{ "type": "text", "text": "" }], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "content": "", + "role": "assistant", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › tool calls › should handle text output type in tool results + #[test] + fn should_handle_text_output_type_in_tool_results() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "input": { "query": "weather" }, + "toolCallId": "call-1", + "toolName": "getWeather", + }, + ], + }, + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "call-1", + "toolName": "getWeather", + "output": { "type": "text", "value": "It is sunny today" }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "type": "function", + "id": "call-1", + "function": { + "name": "getWeather", + "arguments": stringify(&json!({ "query": "weather" })), + }, + }, + ], + }, + { + "role": "tool", + "content": "It is sunny today", + "tool_call_id": "call-1", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › tool calls › should stringify multi-part tool content by default + #[test] + fn should_stringify_multi_part_tool_content_by_default() { + let value = json!([ + { "type": "text", "text": "image result" }, + { + "type": "file", + "data": { "type": "data", "data": "iVBORw0KGgo=" }, + "mediaType": "image/png", + }, + ]); + + let result = convert(&prompt(json!([ + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "call-1", + "toolName": "useImage", + "output": { "type": "content", "value": value }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "tool", + "tool_call_id": "call-1", + "content": stringify(&value), + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › tool calls › should convert multi-part tool content when supported + #[test] + fn should_convert_multi_part_tool_content_when_supported() { + let result = convert_with( + &prompt(json!([ + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "call-1", + "toolName": "useImage", + "output": { + "type": "content", + "value": [ + { "type": "text", "text": "image result" }, + { + "type": "file", + "data": { "type": "data", "data": "iVBORw0KGgo=" }, + "mediaType": "image/png", + }, + { + "type": "file", + "data": { + "type": "url", + "url": "https://example.com/image.png", + }, + "mediaType": "image/png", + }, + ], + }, + }, + ], + }, + ])), + "google", + true, + ) + .expect("conversion succeeds"); + + assert_eq!( + result, + json!([ + { + "role": "tool", + "tool_call_id": "call-1", + "content": [ + { "type": "text", "text": "image result" }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,iVBORw0KGgo=", + }, + }, + { + "type": "image_url", + "image_url": { "url": "https://example.com/image.png" }, + }, + ], + }, + ]) + ); + } +} + +mod provider_specific_metadata_merging { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should merge system message metadata + #[test] + fn should_merge_system_message_metadata() { + let result = convert(&prompt(json!([ + { + "role": "system", + "content": "You are a helpful assistant.", + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "ephemeral" }, + }, + }, + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "system", + "content": "You are a helpful assistant.", + "cacheControl": { "type": "ephemeral" }, + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should merge user message content metadata + #[test] + fn should_merge_user_message_content_metadata() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello", + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "ephemeral" }, + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": "Hello", + "cacheControl": { "type": "ephemeral" }, + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should prioritize content-level metadata when merging + #[test] + fn should_prioritize_content_level_metadata_when_merging() { + let result = convert(&prompt(json!([ + { + "role": "user", + "providerOptions": { + "openaiCompatible": { + "messageLevel": true, + }, + }, + "content": [ + { + "type": "text", + "text": "Hello", + "providerOptions": { + "openaiCompatible": { + "contentLevel": true, + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": "Hello", + "contentLevel": true, + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle tool calls with metadata + #[test] + fn should_handle_tool_calls_with_metadata() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "call1", + "toolName": "calculator", + "input": { "x": 1, "y": 2 }, + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "ephemeral" }, + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call1", + "type": "function", + "function": { + "name": "calculator", + "arguments": stringify(&json!({ "x": 1, "y": 2 })), + }, + "cacheControl": { "type": "ephemeral" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle image content with metadata + #[test] + fn should_handle_image_content_with_metadata() { + let image_url = "https://example.com/image.jpg"; + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "data": { "type": "url", "url": image_url }, + "mediaType": "image/*", + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "ephemeral" }, + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": { "url": image_url }, + "cacheControl": { "type": "ephemeral" }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should omit non-openaiCompatible metadata + #[test] + fn should_omit_non_openai_compatible_metadata() { + let result = convert(&prompt(json!([ + { + "role": "system", + "content": "Hello", + "providerOptions": { + "someOtherProvider": { + "shouldBeIgnored": true, + }, + }, + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "system", + "content": "Hello", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle a user message with multiple content parts (text + image) + #[test] + fn should_handle_a_user_message_with_multiple_content_parts_text_image() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Hello from part 1", + "providerOptions": { + "openaiCompatible": { "sentiment": "positive" }, + "leftoverKey": { "foo": "some leftover data" }, + }, + }, + { + "type": "file", + "data": { "type": "data", "data": "AAECAw==" }, + "mediaType": "image/png", + "providerOptions": { + "openaiCompatible": { "alt_text": "A sample image" }, + }, + }, + ], + "providerOptions": { + "openaiCompatible": { "priority": "high" }, + }, + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "priority": "high", // hoisted from message-level providerOptions + "content": [ + { + "type": "text", + "text": "Hello from part 1", + "sentiment": "positive", // hoisted from part-level openaiCompatible + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,AAECAw==", + }, + "alt_text": "A sample image", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle a user message with multiple text parts (flattening disabled) + #[test] + fn should_handle_a_user_message_with_multiple_text_parts_flattening_disabled() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Part 1" }, + { "type": "text", "text": "Part 2" }, + ], + }, + ]))); + + // Because there are multiple text parts, the converter won't flatten them + assert_eq!( + result, + json!([ + { + "role": "user", + "content": [ + { "type": "text", "text": "Part 1" }, + { "type": "text", "text": "Part 2" }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle an assistant message with text plus multiple tool calls + #[test] + fn should_handle_an_assistant_message_with_text_plus_multiple_tool_calls() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { "type": "text", "text": "Checking that now..." }, + { + "type": "tool-call", + "toolCallId": "call1", + "toolName": "searchTool", + "input": { "query": "Weather" }, + "providerOptions": { + "openaiCompatible": { "function_call_reason": "user request" }, + }, + }, + { "type": "text", "text": "Almost there..." }, + { + "type": "tool-call", + "toolCallId": "call2", + "toolName": "mapsTool", + "input": { "location": "Paris" }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": "Checking that now...Almost there...", + "tool_calls": [ + { + "id": "call1", + "type": "function", + "function": { + "name": "searchTool", + "arguments": stringify(&json!({ "query": "Weather" })), + }, + "function_call_reason": "user request", + }, + { + "id": "call2", + "type": "function", + "function": { + "name": "mapsTool", + "arguments": stringify(&json!({ "location": "Paris" })), + }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle a single tool role message with multiple tool-result parts + #[test] + fn should_handle_a_single_tool_role_message_with_multiple_tool_result_parts() { + let result = convert(&prompt(json!([ + { + "role": "tool", + "providerOptions": { + // this just gets omitted as we prioritize content-level metadata + "openaiCompatible": { "responseTier": "detailed" }, + }, + "content": [ + { + "type": "tool-result", + "toolCallId": "call123", + "toolName": "calculator", + "output": { "type": "json", "value": { "stepOne": "data chunk 1" } }, + }, + { + "type": "tool-result", + "toolCallId": "call123", + "toolName": "calculator", + "providerOptions": { + "openaiCompatible": { "partial": true }, + }, + "output": { "type": "json", "value": { "stepTwo": "data chunk 2" } }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "tool", + "tool_call_id": "call123", + "content": stringify(&json!({ "stepOne": "data chunk 1" })), + }, + { + "role": "tool", + "tool_call_id": "call123", + "content": stringify(&json!({ "stepTwo": "data chunk 2" })), + "partial": true, + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle multiple content parts with multiple metadata layers + #[test] + fn should_handle_multiple_content_parts_with_multiple_metadata_layers() { + let result = convert(&prompt(json!([ + { + "role": "user", + "providerOptions": { + "openaiCompatible": { "messageLevel": "global-metadata" }, + "leftoverForMessage": { "x": 123 }, + }, + "content": [ + { + "type": "text", + "text": "Part A", + "providerOptions": { + "openaiCompatible": { "textPartLevel": "localized" }, + "leftoverForText": { "info": "text leftover" }, + }, + }, + { + "type": "file", + // Buffer.from([9, 8, 7, 6]).toString('base64') + "data": { "type": "data", "data": "CQgHBg==" }, + "mediaType": "image/png", + "providerOptions": { + "openaiCompatible": { "imagePartLevel": "image-data" }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "user", + "messageLevel": "global-metadata", + "content": [ + { + "type": "text", + "text": "Part A", + "textPartLevel": "localized", + }, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,CQgHBg==", + }, + "imagePartLevel": "image-data", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle different tool metadata vs. message-level metadata + #[test] + fn should_handle_different_tool_metadata_vs_message_level_metadata() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "providerOptions": { + "openaiCompatible": { "globalPriority": "high" }, + }, + "content": [ + { "type": "text", "text": "Initiating tool calls..." }, + { + "type": "tool-call", + "toolCallId": "callXYZ", + "toolName": "awesomeTool", + "input": { "param": "someValue" }, + "providerOptions": { + "openaiCompatible": { + "toolPriority": "critical", + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "globalPriority": "high", + "content": "Initiating tool calls...", + "tool_calls": [ + { + "id": "callXYZ", + "type": "function", + "function": { + "name": "awesomeTool", + "arguments": stringify(&json!({ "param": "someValue" })), + }, + "toolPriority": "critical", + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › provider-specific metadata merging › should handle metadata collisions and overwrites in tool calls + #[test] + fn should_handle_metadata_collisions_and_overwrites_in_tool_calls() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "default" }, + "sharedKey": "assistantLevel", + }, + }, + "content": [ + { + "type": "tool-call", + "toolCallId": "collisionToolCall", + "toolName": "collider", + "input": { "num": 42 }, + "providerOptions": { + "openaiCompatible": { + "cacheControl": { "type": "ephemeral" }, // overwrites top-level + "sharedKey": "toolLevel", + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "cacheControl": { "type": "default" }, + "sharedKey": "assistantLevel", + "content": null, + "tool_calls": [ + { + "id": "collisionToolCall", + "type": "function", + "function": { + "name": "collider", + "arguments": stringify(&json!({ "num": 42 })), + }, + "cacheControl": { "type": "ephemeral" }, + "sharedKey": "toolLevel", + }, + ], + }, + ]) + ); + } +} + +mod google_gemini_thought_signatures_open_ai_compatibility { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should serialize thought signature from a custom provider namespace + #[test] + fn should_serialize_thought_signature_from_a_custom_provider_namespace() { + let result = convert_with( + &prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": { "flight": "AA100" }, + "providerOptions": { + "myGateway": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + ])), + "myGateway", + false, + ) + .expect("conversion succeeds"); + + assert_match_object( + &result[0], + &json!({ + "tool_calls": [ + { + "extra_content": { + "google": { "thought_signature": "" }, + }, + }, + ], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should fall back to the google namespace + #[test] + fn should_fall_back_to_the_google_namespace() { + let result = convert_with( + &prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": { "flight": "AA100" }, + "providerOptions": { + "google": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + ])), + "myGateway", + false, + ) + .expect("conversion succeeds"); + + assert_match_object( + &result[0], + &json!({ + "tool_calls": [ + { + "extra_content": { + "google": { "thought_signature": "" }, + }, + }, + ], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should prefer the custom provider namespace over google + #[test] + fn should_prefer_the_custom_provider_namespace_over_google() { + let result = convert_with( + &prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": { "flight": "AA100" }, + "providerOptions": { + "myGateway": { + "thoughtSignature": "", + }, + "google": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + ])), + "myGateway", + false, + ) + .expect("conversion succeeds"); + + assert_match_object( + &result[0], + &json!({ + "tool_calls": [ + { + "extra_content": { + "google": { "thought_signature": "" }, + }, + }, + ], + }), + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should serialize thought signature to extra_content for single tool call + #[test] + fn should_serialize_thought_signature_to_extra_content_for_single_tool_call() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": { "flight": "AA100" }, + "providerOptions": { + "google": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "function-call-1", + "type": "function", + "function": { + "name": "check_flight", + "arguments": stringify(&json!({ "flight": "AA100" })), + }, + "extra_content": { + "google": { + "thought_signature": "", + }, + }, + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should handle sequential tool calls with separate signatures (Turn 1 Step 3 scenario) + #[test] + fn should_handle_sequential_tool_calls_with_separate_signatures() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Check flight status for AA100 and book a taxi 2 hours before if delayed.", + }, + ], + }, + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "input": { "flight": "AA100" }, + "providerOptions": { + "google": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "function-call-1", + "toolName": "check_flight", + "output": { + "type": "json", + "value": { "status": "delayed", "departure_time": "12 PM" }, + }, + }, + ], + }, + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-2", + "toolName": "book_taxi", + "input": { "time": "10 AM" }, + "providerOptions": { + "google": { + "thoughtSignature": "", + }, + }, + }, + ], + }, + { + "role": "tool", + "content": [ + { + "type": "tool-result", + "toolCallId": "function-call-2", + "toolName": "book_taxi", + "output": { "type": "json", "value": { "booking_status": "success" } }, + }, + ], + }, + ]))); + + // Verify both signatures are preserved + assert_eq!( + result[1], + json!({ + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "function-call-1", + "type": "function", + "function": { + "name": "check_flight", + "arguments": stringify(&json!({ "flight": "AA100" })), + }, + "extra_content": { + "google": { + "thought_signature": "", + }, + }, + }, + ], + }) + ); + + assert_eq!( + result[3], + json!({ + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "function-call-2", + "type": "function", + "function": { + "name": "book_taxi", + "arguments": stringify(&json!({ "time": "10 AM" })), + }, + "extra_content": { + "google": { + "thought_signature": "", + }, + }, + }, + ], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should handle parallel tool calls with signature only on first call + #[test] + fn should_handle_parallel_tool_calls_with_signature_only_on_first_call() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "function-call-paris", + "toolName": "get_current_temperature", + "input": { "location": "Paris" }, + "providerOptions": { + "google": { + "thoughtSignature": "", + }, + }, + }, + { + "type": "tool-call", + "toolCallId": "function-call-london", + "toolName": "get_current_temperature", + "input": { "location": "London" }, + // No signature on parallel function call + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "function-call-paris", + "type": "function", + "function": { + "name": "get_current_temperature", + "arguments": stringify(&json!({ "location": "Paris" })), + }, + "extra_content": { + "google": { + "thought_signature": "", + }, + }, + }, + { + "id": "function-call-london", + "type": "function", + "function": { + "name": "get_current_temperature", + "arguments": stringify(&json!({ "location": "London" })), + }, + // No extra_content for second parallel call + }, + ], + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › Google Gemini thought signatures (OpenAI compatibility) › should not include extra_content when no thought signature is present + #[test] + fn should_not_include_extra_content_when_no_thought_signature_is_present() { + let result = convert(&prompt(json!([ + { + "role": "assistant", + "content": [ + { + "type": "tool-call", + "toolCallId": "call-1", + "toolName": "some_tool", + "input": { "param": "value" }, + }, + ], + }, + ]))); + + assert_eq!( + result, + json!([ + { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "some_tool", + "arguments": stringify(&json!({ "param": "value" })), + }, + }, + ], + }, + ]) + ); + } +} + +mod top_level_only_media_type_resolution { + use super::*; + + const PNG_BASE64: &str = "iVBORw0KGgo="; + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › top-level-only media type resolution › passes full image/png through unchanged for inline data + #[test] + fn passes_full_image_png_through_unchanged_for_inline_data() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "mediaType": "image/png", + "data": { "type": "data", "data": PNG_BASE64 }, + }, + ], + }, + ]))); + + assert_eq!( + result[0]["content"][0], + json!({ + "type": "image_url", + "image_url": { "url": format!("data:image/png;base64,{PNG_BASE64}") }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › top-level-only media type resolution › detects image subtype from inline bytes for top-level "image" + #[test] + fn detects_image_subtype_from_inline_bytes_for_top_level_image() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "mediaType": "image", + "data": { "type": "data", "data": PNG_BASE64 }, + }, + ], + }, + ]))); + + assert_eq!( + result[0]["content"][0], + json!({ + "type": "image_url", + "image_url": { "url": format!("data:image/png;base64,{PNG_BASE64}") }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › top-level-only media type resolution › passes through URL source for top-level-only image (provider accepts raw URL) + #[test] + fn passes_through_url_source_for_top_level_only_image() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "mediaType": "image", + "data": { + "type": "url", + "url": "https://example.com/x.png", + }, + }, + ], + }, + ]))); + + assert_eq!( + result[0]["content"][0], + json!({ + "type": "image_url", + "image_url": { "url": "https://example.com/x.png" }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/convert-to-openai-compatible-chat-messages.test.ts › top-level-only media type resolution › normalizes image/* wildcard via detection + #[test] + fn normalizes_image_wildcard_via_detection() { + let result = convert(&prompt(json!([ + { + "role": "user", + "content": [ + { + "type": "file", + "mediaType": "image/*", + "data": { "type": "data", "data": PNG_BASE64 }, + }, + ], + }, + ]))); + + assert_eq!( + result[0]["content"][0], + json!({ + "type": "image_url", + "image_url": { "url": format!("data:image/png;base64,{PNG_BASE64}") }, + }) + ); + } +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/anthropic-fallback-tool-call.sse b/aimux-providers/src/openai_compatible/tests/chat/fixtures/anthropic-fallback-tool-call.sse new file mode 100644 index 000000000..539aa5d2a --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/anthropic-fallback-tool-call.sse @@ -0,0 +1,17 @@ +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"content":"Reading"},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"content":" it."},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"toolu_sanitized","type":"function","function":{"name":"read_file","arguments":""}}]},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":""}}]},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"pa"}}]},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"th\": \"a.txt\"}"}}]},"finish_reason":null}]} + +data: {"id":"msg_sanitized","object":"chat.completion.chunk","created":0,"model":"claude-haiku-4-5-20251001","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/placeholder-first-chunk.chunks.txt b/aimux-providers/src/openai_compatible/tests/chat/fixtures/placeholder-first-chunk.chunks.txt new file mode 100644 index 000000000..6297d4087 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/placeholder-first-chunk.chunks.txt @@ -0,0 +1,4 @@ +{"choices":[],"created":0,"id":"","model":"","object":""} +{"choices":[{"delta":{"content":"Hello","role":"assistant"},"finish_reason":null,"index":0}],"created":1700000000,"id":"chatcmpl-123","model":"actual-model-name","object":"chat.completion.chunk"} +{"choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":1700000000,"id":"chatcmpl-123","model":"actual-model-name","object":"chat.completion.chunk"} +{"choices":[],"created":1700000000,"id":"chatcmpl-123","model":"actual-model-name","object":"chat.completion.chunk","usage":{"prompt_tokens":10,"completion_tokens":1,"total_tokens":11}} diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.chunks.txt b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.chunks.txt new file mode 100644 index 000000000..b723c58d4 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.chunks.txt @@ -0,0 +1,344 @@ +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"First","role":"assistant"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" said"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Say"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772287,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" single"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" That's"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" straightforward"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" They"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" want"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" me"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" respond"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" with"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" just"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" one"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"As"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Gro"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"k"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I'm"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" supposed"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" helpful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" truthful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" not"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" verbose"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" So"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" keep"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" my"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" simple"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" directly"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" answer"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" request"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" instruction"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" say"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" single"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" so"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" my"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" exactly"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" one"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" shouldn't"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" add"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" any"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" explanations"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" greetings"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" or"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" extra"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" text"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"What"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" choose"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"?"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" didn't"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" specify"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" which"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" so"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" have"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" freedom"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" pick"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" any"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772288,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" To"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" make"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" it"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" fun"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" aligned"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" with"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" my"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" personality"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" could"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" choose"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" something"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" relevant"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" or"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" witty"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"I'm"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Gro"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"k"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" created"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" by"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" x"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"AI"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I'm"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" meant"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" maximally"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" truthful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" helpful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Maybe"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" could"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" choose"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" reflects"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" like"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Hello"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" or"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"G"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"rok"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" but"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" need"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" keep"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" it"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" simple"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Possible"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" words"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Hello"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" –"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Common"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" greeting"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Yes"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" –"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Affirm"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"ative"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"G"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"rok"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" –"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" My"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" name"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" which"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" might"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" meta"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772289,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" –"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Echo"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"ing"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" request"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Something"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" random"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" like"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Banana"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" humor"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Since"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" asked"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" single"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I'm"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" an"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" AI"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" ensure"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" it's"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" appropriate"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" not"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" misleading"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" system"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" prompt"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" says"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"You"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" are"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Gro"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"k"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" helpful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" maximally"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" truthful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" AI"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" built"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" by"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" x"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"AI"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" not"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" based"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" on"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" any"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" other"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" companies"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" their"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" models"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" So"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" stay"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" true"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"To"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" helpful"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I'll"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" just"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" pick"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772290,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" simple"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Let's"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" go"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" with"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"G"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"rok"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" make"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" it"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" personal"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Finally"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" my"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" only"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" one"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" nothing"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" else"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" No"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" punctuation"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" if"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" possible"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" just"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" word"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Gro"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"k"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"content":"G"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{"content":"rok"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"system_fingerprint":"fp_2a885414fb"} +{"id":"f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94","object":"chat.completion.chunk","created":1770772291,"model":"grok-3-mini","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":2,"total_tokens":354,"prompt_tokens_details":{"text_tokens":12,"audio_tokens":0,"image_tokens":0,"cached_tokens":11},"completion_tokens_details":{"reasoning_tokens":340,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0,"cost_in_usd_ticks":1721250},"system_fingerprint":"fp_2a885414fb"} \ No newline at end of file diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.json b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.json new file mode 100644 index 000000000..8dcf6c87f --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-text.json @@ -0,0 +1,38 @@ +{ + "id": "edea4703-19aa-6d74-fedb-dc1c213543e0", + "object": "chat.completion", + "created": 1770772090, + "model": "grok-3-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Grok", + "reasoning_content": "First, the user said: \"Say a single word.\" That's straightforward. They want me to respond with just one word.\n\nAs Grok, I'm supposed to be helpful, truthful, and maximally truthful, but in this case, it's a simple request. I don't need to overcomplicate it.\n\nMy response should be exactly one word, as per their instruction. If I say more than one word, I'm not following the request accurately.\n\nWhat word should I choose? The user didn't specify which word, so I have some freedom. I could pick a random word, or something relevant to the context.\n\nTo make it engaging and true to my identity, I could choose a word that reflects my personality. For example, something fun, helpful, or related to AI.\n\nPossible options:\n- \"Hello\" – Simple greeting.\n- \"Grok\" – My name, which is relevant.\n- \"Word\" – Echoing the request.\n- \"Yes\" – But that might not make sense alone.\n- Something random like \"Apple\" or \"Mountain\".\n\nSince I'm Grok, built by xAI, I should stay true to that. Maybe \"xAI\" or \"Truth\" to emphasize my truthful nature.\n\nThe simplest and most direct response is just to say any single word. To be maximally helpful, I should ensure it's appropriate.\n\nThe user might be testing me or playing a game, so I should respond in kind.\n\nFinally, decide on a word: Let's go with \"Grok\" – it's unique to me and reinforces my identity.\n\nSo, my response will be: Grok", + "refusal": null + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 12, + "completion_tokens": 2, + "total_tokens": 334, + "prompt_tokens_details": { + "text_tokens": 12, + "audio_tokens": 0, + "image_tokens": 0, + "cached_tokens": 2 + }, + "completion_tokens_details": { + "reasoning_tokens": 320, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "num_sources_used": 0, + "cost_in_usd_ticks": 1641500 + }, + "system_fingerprint": "fp_2a885414fb" +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.chunks.txt b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.chunks.txt new file mode 100644 index 000000000..ec2bb66a1 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.chunks.txt @@ -0,0 +1,230 @@ +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"First","role":"assistant"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" asking"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772293,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" about"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" weather"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" in"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" San"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Francisco"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" have"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" available"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" called"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"weather"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" retrieves"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" weather"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" given"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" location"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" requires"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" parameter"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"location"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" which"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" string"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" has"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" provided"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"San"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Francisco"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" as"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" location"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" so"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that's"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" clear"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" infer"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"able"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" call"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" this"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" get"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" weather"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" information"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" format"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" calling"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" specific"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" need"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" use"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" <"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"_call"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":">"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" tags"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" with"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" JSON"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" inside"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" like"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" <"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772294,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"_call"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":">{\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"action"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"weather"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"action"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"_input"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" {\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"location"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"San"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" Francisco"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"}}.\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"This"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" seems"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" be"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" direct"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" match"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" so"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" don't"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" need"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" ask"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" clarification"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" My"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" should"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" only"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" contain"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" call"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" if"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" that's"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" next"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" step"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" which"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" it"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"The"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" instructions"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" say"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Keep"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" your"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" clear"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":";"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" please"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" do"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" not"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" make"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" your"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" response"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" verbose"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"!\""}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" So"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" shouldn't"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" add"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" any"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" extra"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" text"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":";"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" just"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" call"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":".\n\n"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"Finally"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" after"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772295,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" calling"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" function"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" if"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" this"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" were"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" multi"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"-turn"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" conversation"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" I"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" might"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" need"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" respond"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" based"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" on"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" result"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" but"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" for"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" now"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" this"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" is"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" the"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" logical"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" next"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":" step"}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_79382389","function":{"name":"weather","arguments":"{\"location\":\"San Francisco\"}"},"index":0,"type":"function"}]}}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"system_fingerprint":"fp_2a885414fb"} +{"id":"7027d986-3c59-a37a-9a5f-50713e01c8a6","object":"chat.completion.chunk","created":1770772296,"model":"grok-3-mini","choices":[],"usage":{"prompt_tokens":307,"completion_tokens":26,"total_tokens":560,"prompt_tokens_details":{"text_tokens":307,"audio_tokens":0,"image_tokens":0,"cached_tokens":306},"completion_tokens_details":{"reasoning_tokens":227,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0,"cost_in_usd_ticks":1497500},"system_fingerprint":"fp_2a885414fb"} \ No newline at end of file diff --git a/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.json b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.json new file mode 100644 index 000000000..541f3ba1a --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/fixtures/xai-tool-call.json @@ -0,0 +1,48 @@ +{ + "id": "acfa24c3-b556-0f2c-731e-64fb836d544b", + "object": "chat.completion", + "created": 1770772214, + "model": "grok-3-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "", + "reasoning_content": "First, the user is asking about the weather in San Francisco. I have a function available called \"weather\" that retrieves the weather for a given location.\n\nThe function requires a parameter: \"location\", which is a string. The user has provided \"San Francisco\" as the location, so that's clear and inferable.\n\nI should call this function to get the weather information. The format for calling the function is specific: I need to use tags with a JSON object like {\"action\": \"function_name\", \"action_input\": {\"parameter\": \"argument\"}}.\n\nFor this, the action is \"weather\", and the action_input should be {\"location\": \"San Francisco\"}.\n\nAfter calling the function, I might need to handle the response in subsequent turns, but for now, this is the logical next step.\n\nMy response should only include the function call if I'm deciding to make one. I shouldn't add any extra text unless necessary.\n\nThe instructions say: \"Keep your response to user clear; please do not make your response verbose!\" So, I should not add any chit-chat; just the function call.\n\nFinally, structure it as: {\"action\": \"weather\", \"action_input\": {\"location\": \"San Francisco\"}}", + "tool_calls": [ + { + "id": "call_46427107", + "function": { + "name": "weather", + "arguments": "{\"location\":\"San Francisco\"}" + }, + "type": "function" + } + ], + "refusal": null + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 307, + "completion_tokens": 26, + "total_tokens": 588, + "prompt_tokens_details": { + "text_tokens": 307, + "audio_tokens": 0, + "image_tokens": 0, + "cached_tokens": 244 + }, + "completion_tokens_details": { + "reasoning_tokens": 255, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "num_sources_used": 0, + "cost_in_usd_ticks": 1777000 + }, + "system_fingerprint": "fp_2a885414fb" +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/mod.rs b/aimux-providers/src/openai_compatible/tests/chat/mod.rs new file mode 100644 index 000000000..e50bced16 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/mod.rs @@ -0,0 +1,5 @@ +mod chat_content_array; +mod chat_language_model; +mod convert_chat_usage; +mod convert_to_chat_messages; +mod prepare_tools; diff --git a/aimux-providers/src/openai_compatible/tests/chat/prepare_tools.rs b/aimux-providers/src/openai_compatible/tests/chat/prepare_tools.rs new file mode 100644 index 000000000..ddc522b8a --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/prepare_tools.rs @@ -0,0 +1,365 @@ +use serde_json::{Map, Value, json}; + +use aimux_core::options::CallOptions; + +use crate::openai_compatible::convert::{PreparedTools, prepare_function_tools}; + +/// `prepareTools({ tools, toolChoice })`: the Rust form reads both from the +/// call options, given here in their AI SDK JSON form. +fn prepare_tools(tools: Value, tool_choice: Value) -> PreparedTools { + let mut options = Map::new(); + options.insert("prompt".into(), json!([])); + if !tools.is_null() { + options.insert("tools".into(), tools); + } + if !tool_choice.is_null() { + options.insert("toolChoice".into(), tool_choice); + } + let options: CallOptions = + serde_json::from_value(Value::Object(options)).expect("call options"); + prepare_function_tools(&options) +} + +/// The result in its upstream shape; `undefined` fields are omitted. +fn result_json(result: &PreparedTools) -> Value { + let mut out = Map::new(); + if let Some(choice) = &result.tool_choice { + out.insert("toolChoice".into(), choice.clone()); + } + out.insert( + "toolWarnings".into(), + serde_json::to_value(&result.warnings).expect("warnings serialize"), + ); + if let Some(tools) = &result.tools { + out.insert("tools".into(), Value::Array(tools.clone())); + } + Value::Object(out) +} + +fn test_function_tool() -> Value { + json!([ + { + "type": "function", + "name": "testFunction", + "description": "Test", + "inputSchema": {}, + }, + ]) +} + +// The `prepareTools` describe block shares the file's name. +#[allow(clippy::module_inception)] +mod prepare_tools { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should return undefined tools and toolChoice when tools are null + #[test] + fn should_return_undefined_tools_and_tool_choice_when_tools_are_null() { + let result = prepare_tools(Value::Null, Value::Null); + + assert_eq!(result_json(&result), json!({ "toolWarnings": [] })); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should return undefined tools and toolChoice when tools are empty + #[test] + fn should_return_undefined_tools_and_tool_choice_when_tools_are_empty() { + let result = prepare_tools(json!([]), Value::Null); + + assert_eq!(result_json(&result), json!({ "toolWarnings": [] })); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should correctly prepare function tools + #[test] + fn should_correctly_prepare_function_tools() { + let result = prepare_tools( + json!([ + { + "type": "function", + "name": "testFunction", + "description": "A test function", + "inputSchema": { "type": "object", "properties": {} }, + }, + ]), + Value::Null, + ); + + assert_eq!( + result_json(&result), + json!({ + "toolWarnings": [], + "tools": [ + { + "function": { + "description": "A test function", + "name": "testFunction", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + "type": "function", + }, + ], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should add warnings for unsupported provider-defined tools + #[test] + fn should_add_warnings_for_unsupported_provider_defined_tools() { + let result = prepare_tools( + json!([ + { + "type": "provider", + "id": "some.unsupported_tool", + "name": "unsupported_tool", + "args": {}, + }, + ]), + Value::Null, + ); + + assert_eq!(result.tools, Some(vec![])); + assert_eq!(result.tool_choice, None); + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([ + { + "feature": "provider-defined tool some.unsupported_tool", + "type": "unsupported", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should handle tool choice "auto" + #[test] + fn should_handle_tool_choice_auto() { + let result = prepare_tools(test_function_tool(), json!({ "type": "auto" })); + + assert_eq!(result.tool_choice, Some(json!("auto"))); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should handle tool choice "required" + #[test] + fn should_handle_tool_choice_required() { + let result = prepare_tools(test_function_tool(), json!({ "type": "required" })); + + assert_eq!(result.tool_choice, Some(json!("required"))); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should handle tool choice "none" + #[test] + fn should_handle_tool_choice_none() { + let result = prepare_tools(test_function_tool(), json!({ "type": "none" })); + + assert_eq!(result.tool_choice, Some(json!("none"))); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › should handle tool choice "tool" + #[test] + fn should_handle_tool_choice_tool() { + let result = prepare_tools( + test_function_tool(), + json!({ "type": "tool", "toolName": "testFunction" }), + ); + + assert_eq!( + result.tool_choice, + Some(json!({ + "type": "function", + "function": { "name": "testFunction" }, + })) + ); + } + + mod strict_mode { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › strict mode › should pass through strict mode when strict is true + #[test] + fn should_pass_through_strict_mode_when_strict_is_true() { + let result = prepare_tools( + json!([ + { + "type": "function", + "name": "testFunction", + "description": "A test function", + "inputSchema": { "type": "object", "properties": {} }, + "strict": true, + }, + ]), + Value::Null, + ); + + assert_eq!( + result_json(&result), + json!({ + "toolWarnings": [], + "tools": [ + { + "function": { + "description": "A test function", + "name": "testFunction", + "parameters": { + "properties": {}, + "type": "object", + }, + "strict": true, + }, + "type": "function", + }, + ], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › strict mode › should pass through strict mode when strict is false + #[test] + fn should_pass_through_strict_mode_when_strict_is_false() { + let result = prepare_tools( + json!([ + { + "type": "function", + "name": "testFunction", + "description": "A test function", + "inputSchema": { "type": "object", "properties": {} }, + "strict": false, + }, + ]), + Value::Null, + ); + + assert_eq!( + result_json(&result), + json!({ + "toolWarnings": [], + "tools": [ + { + "function": { + "description": "A test function", + "name": "testFunction", + "parameters": { + "properties": {}, + "type": "object", + }, + "strict": false, + }, + "type": "function", + }, + ], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › strict mode › should not include strict mode when strict is undefined + #[test] + fn should_not_include_strict_mode_when_strict_is_undefined() { + let result = prepare_tools( + json!([ + { + "type": "function", + "name": "testFunction", + "description": "A test function", + "inputSchema": { "type": "object", "properties": {} }, + }, + ]), + Value::Null, + ); + + assert_eq!( + result_json(&result), + json!({ + "toolWarnings": [], + "tools": [ + { + "function": { + "description": "A test function", + "name": "testFunction", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + "type": "function", + }, + ], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/chat/openai-compatible-prepare-tools.test.ts › prepareTools › strict mode › should pass through strict mode for multiple tools with different strict settings + #[test] + fn should_pass_through_strict_mode_for_multiple_tools_with_different_strict() { + let result = prepare_tools( + json!([ + { + "type": "function", + "name": "strictTool", + "description": "A strict tool", + "inputSchema": { "type": "object", "properties": {} }, + "strict": true, + }, + { + "type": "function", + "name": "nonStrictTool", + "description": "A non-strict tool", + "inputSchema": { "type": "object", "properties": {} }, + "strict": false, + }, + { + "type": "function", + "name": "defaultTool", + "description": "A tool without strict setting", + "inputSchema": { "type": "object", "properties": {} }, + }, + ]), + Value::Null, + ); + + assert_eq!( + result_json(&result), + json!({ + "toolWarnings": [], + "tools": [ + { + "function": { + "description": "A strict tool", + "name": "strictTool", + "parameters": { + "properties": {}, + "type": "object", + }, + "strict": true, + }, + "type": "function", + }, + { + "function": { + "description": "A non-strict tool", + "name": "nonStrictTool", + "parameters": { + "properties": {}, + "type": "object", + }, + "strict": false, + }, + "type": "function", + }, + { + "function": { + "description": "A tool without strict setting", + "name": "defaultTool", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + "type": "function", + }, + ], + }) + ); + } + } +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_text_fixture.json b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_text_fixture.json new file mode 100644 index 000000000..7c0c514e2 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_text_fixture.json @@ -0,0 +1,104 @@ +{ + "content": [ + { + "text": "Grok", + "type": "text" + }, + { + "text": "First, the user said: \"Say a single word.\" That's straightforward. They want me to respond with just one word.\n\nAs Grok, I'm supposed to be helpful, truthful, and maximally truthful, but in this case, it's a simple request. I don't need to overcomplicate it.\n\nMy response should be exactly one word, as per their instruction. If I say more than one word, I'm not following the request accurately.\n\nWhat word should I choose? The user didn't specify which word, so I have some freedom. I could pick a random word, or something relevant to the context.\n\nTo make it engaging and true to my identity, I could choose a word that reflects my personality. For example, something fun, helpful, or related to AI.\n\nPossible options:\n- \"Hello\" – Simple greeting.\n- \"Grok\" – My name, which is relevant.\n- \"Word\" – Echoing the request.\n- \"Yes\" – But that might not make sense alone.\n- Something random like \"Apple\" or \"Mountain\".\n\nSince I'm Grok, built by xAI, I should stay true to that. Maybe \"xAI\" or \"Truth\" to emphasize my truthful nature.\n\nThe simplest and most direct response is just to say any single word. To be maximally helpful, I should ensure it's appropriate.\n\nThe user might be testing me or playing a game, so I should respond in kind.\n\nFinally, decide on a word: Let's go with \"Grok\" – it's unique to me and reinforces my identity.\n\nSo, my response will be: Grok", + "type": "reasoning" + } + ], + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": { + "acceptedPredictionTokens": 0, + "rejectedPredictionTokens": 0 + } + }, + "request": { + "body": "{\"model\":\"grok-3\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}" + }, + "response": { + "body": { + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "Grok", + "reasoning_content": "First, the user said: \"Say a single word.\" That's straightforward. They want me to respond with just one word.\n\nAs Grok, I'm supposed to be helpful, truthful, and maximally truthful, but in this case, it's a simple request. I don't need to overcomplicate it.\n\nMy response should be exactly one word, as per their instruction. If I say more than one word, I'm not following the request accurately.\n\nWhat word should I choose? The user didn't specify which word, so I have some freedom. I could pick a random word, or something relevant to the context.\n\nTo make it engaging and true to my identity, I could choose a word that reflects my personality. For example, something fun, helpful, or related to AI.\n\nPossible options:\n- \"Hello\" – Simple greeting.\n- \"Grok\" – My name, which is relevant.\n- \"Word\" – Echoing the request.\n- \"Yes\" – But that might not make sense alone.\n- Something random like \"Apple\" or \"Mountain\".\n\nSince I'm Grok, built by xAI, I should stay true to that. Maybe \"xAI\" or \"Truth\" to emphasize my truthful nature.\n\nThe simplest and most direct response is just to say any single word. To be maximally helpful, I should ensure it's appropriate.\n\nThe user might be testing me or playing a game, so I should respond in kind.\n\nFinally, decide on a word: Let's go with \"Grok\" – it's unique to me and reinforces my identity.\n\nSo, my response will be: Grok", + "refusal": null, + "role": "assistant" + } + } + ], + "created": 1770772090, + "id": "edea4703-19aa-6d74-fedb-dc1c213543e0", + "model": "grok-3-mini", + "object": "chat.completion", + "system_fingerprint": "fp_2a885414fb", + "usage": { + "completion_tokens": 2, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 320, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1641500, + "num_sources_used": 0, + "prompt_tokens": 12, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 2, + "image_tokens": 0, + "text_tokens": 12 + }, + "total_tokens": 334 + } + }, + "headers": { + "content-length": "2053", + "content-type": "application/json" + }, + "id": "edea4703-19aa-6d74-fedb-dc1c213543e0", + "modelId": "grok-3-mini", + "timestamp": "2026-02-11T01:08:10.000Z" + }, + "usage": { + "inputTokens": { + "cacheRead": 2, + "noCache": 10, + "total": 12 + }, + "outputTokens": { + "reasoning": 320, + "text": 0, + "total": 2 + }, + "raw": { + "completion_tokens": 2, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 320, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1641500, + "num_sources_used": 0, + "prompt_tokens": 12, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 2, + "image_tokens": 0, + "text_tokens": 12 + }, + "total_tokens": 334 + } + }, + "warnings": [] +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_tool_call_fixture.json b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_tool_call_fixture.json new file mode 100644 index 000000000..8ef5c2ecc --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_generate_tool_call_fixture.json @@ -0,0 +1,116 @@ +{ + "content": [ + { + "text": "First, the user is asking about the weather in San Francisco. I have a function available called \"weather\" that retrieves the weather for a given location.\n\nThe function requires a parameter: \"location\", which is a string. The user has provided \"San Francisco\" as the location, so that's clear and inferable.\n\nI should call this function to get the weather information. The format for calling the function is specific: I need to use tags with a JSON object like {\"action\": \"function_name\", \"action_input\": {\"parameter\": \"argument\"}}.\n\nFor this, the action is \"weather\", and the action_input should be {\"location\": \"San Francisco\"}.\n\nAfter calling the function, I might need to handle the response in subsequent turns, but for now, this is the logical next step.\n\nMy response should only include the function call if I'm deciding to make one. I shouldn't add any extra text unless necessary.\n\nThe instructions say: \"Keep your response to user clear; please do not make your response verbose!\" So, I should not add any chit-chat; just the function call.\n\nFinally, structure it as: {\"action\": \"weather\", \"action_input\": {\"location\": \"San Francisco\"}}", + "type": "reasoning" + }, + { + "input": "{\"location\":\"San Francisco\"}", + "toolCallId": "call_46427107", + "toolName": "weather", + "type": "tool-call" + } + ], + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": { + "acceptedPredictionTokens": 0, + "rejectedPredictionTokens": 0 + } + }, + "request": { + "body": "{\"model\":\"grok-3\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}" + }, + "response": { + "body": { + "choices": [ + { + "finish_reason": "tool_calls", + "index": 0, + "message": { + "content": "", + "reasoning_content": "First, the user is asking about the weather in San Francisco. I have a function available called \"weather\" that retrieves the weather for a given location.\n\nThe function requires a parameter: \"location\", which is a string. The user has provided \"San Francisco\" as the location, so that's clear and inferable.\n\nI should call this function to get the weather information. The format for calling the function is specific: I need to use tags with a JSON object like {\"action\": \"function_name\", \"action_input\": {\"parameter\": \"argument\"}}.\n\nFor this, the action is \"weather\", and the action_input should be {\"location\": \"San Francisco\"}.\n\nAfter calling the function, I might need to handle the response in subsequent turns, but for now, this is the logical next step.\n\nMy response should only include the function call if I'm deciding to make one. I shouldn't add any extra text unless necessary.\n\nThe instructions say: \"Keep your response to user clear; please do not make your response verbose!\" So, I should not add any chit-chat; just the function call.\n\nFinally, structure it as: {\"action\": \"weather\", \"action_input\": {\"location\": \"San Francisco\"}}", + "refusal": null, + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"location\":\"San Francisco\"}", + "name": "weather" + }, + "id": "call_46427107", + "type": "function" + } + ] + } + } + ], + "created": 1770772214, + "id": "acfa24c3-b556-0f2c-731e-64fb836d544b", + "model": "grok-3-mini", + "object": "chat.completion", + "system_fingerprint": "fp_2a885414fb", + "usage": { + "completion_tokens": 26, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 255, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1777000, + "num_sources_used": 0, + "prompt_tokens": 307, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 244, + "image_tokens": 0, + "text_tokens": 307 + }, + "total_tokens": 588 + } + }, + "headers": { + "content-length": "2013", + "content-type": "application/json" + }, + "id": "acfa24c3-b556-0f2c-731e-64fb836d544b", + "modelId": "grok-3-mini", + "timestamp": "2026-02-11T01:10:14.000Z" + }, + "usage": { + "inputTokens": { + "cacheRead": 244, + "noCache": 63, + "total": 307 + }, + "outputTokens": { + "reasoning": 255, + "text": 0, + "total": 26 + }, + "raw": { + "completion_tokens": 26, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 255, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1777000, + "num_sources_used": 0, + "prompt_tokens": 307, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 244, + "image_tokens": 0, + "text_tokens": 307 + }, + "total_tokens": 588 + } + }, + "warnings": [] +} diff --git a/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_text_fixture.json b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_text_fixture.json new file mode 100644 index 000000000..67c3d8786 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_text_fixture.json @@ -0,0 +1,1782 @@ +[ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "f0f0f217-c24d-1fee-5fe3-28fa1d3c8c94", + "modelId": "grok-3-mini", + "timestamp": "2026-02-11T01:11:27.000Z", + "type": "response-metadata" + }, + { + "id": "reasoning-0", + "type": "reasoning-start" + }, + { + "delta": "First", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " said", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Say", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " single", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " That's", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " straightforward", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " They", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " want", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " me", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " respond", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " with", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " just", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " one", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "As", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Gro", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "k", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I'm", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " supposed", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " helpful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " truthful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " not", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " verbose", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " So", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " keep", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " my", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " simple", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " directly", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " answer", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " request", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " instruction", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " say", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " single", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " so", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " my", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " exactly", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " one", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " shouldn't", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " add", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " any", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " explanations", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " greetings", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " or", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " extra", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " text", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "What", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " choose", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "?", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " didn't", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " specify", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " which", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " so", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " have", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " freedom", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " pick", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " any", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " To", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " make", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " it", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " fun", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " aligned", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " with", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " my", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " personality", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " could", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " choose", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " something", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " relevant", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " or", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " witty", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "I'm", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Gro", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "k", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " created", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " by", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " x", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "AI", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I'm", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " meant", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " maximally", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " truthful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " helpful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Maybe", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " could", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " choose", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " reflects", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " like", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Hello", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " or", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "G", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "rok", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " but", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " need", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " keep", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " it", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " simple", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Possible", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " words", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Hello", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " –", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Common", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " greeting", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Yes", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " –", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Affirm", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "ative", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "G", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "rok", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " –", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " My", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " name", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " which", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " might", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " meta", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " –", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Echo", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "ing", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " request", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Something", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " random", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " like", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Banana", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " humor", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Since", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " asked", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " single", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I'm", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " an", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " AI", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " ensure", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " it's", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " appropriate", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " not", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " misleading", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " system", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " prompt", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " says", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "You", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " are", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Gro", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "k", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " helpful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " maximally", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " truthful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " AI", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " built", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " by", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " x", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "AI", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " not", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " based", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " on", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " any", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " other", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " companies", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " their", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " models", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " So", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " stay", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " true", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "To", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " helpful", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I'll", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " just", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " pick", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " simple", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Let's", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " go", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " with", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "G", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "rok", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " make", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " it", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " personal", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Finally", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " my", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " only", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " one", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " nothing", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " else", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " No", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " punctuation", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " if", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " possible", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " just", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " word", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Gro", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "k", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "id": "reasoning-0", + "type": "reasoning-end" + }, + { + "id": "txt-0", + "type": "text-start" + }, + { + "delta": "G", + "id": "txt-0", + "type": "text-delta" + }, + { + "delta": "rok", + "id": "txt-0", + "type": "text-delta" + }, + { + "id": "txt-0", + "type": "text-end" + }, + { + "finishReason": { + "raw": "stop", + "unified": "stop" + }, + "providerMetadata": { + "test-provider": { + "acceptedPredictionTokens": 0, + "rejectedPredictionTokens": 0 + } + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 11, + "noCache": 1, + "total": 12 + }, + "outputTokens": { + "reasoning": 340, + "text": 0, + "total": 2 + }, + "raw": { + "completion_tokens": 2, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 340, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1721250, + "num_sources_used": 0, + "prompt_tokens": 12, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 11, + "image_tokens": 0, + "text_tokens": 12 + }, + "total_tokens": 354 + } + } + } +] diff --git a/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_tool_call_fixture.json b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_tool_call_fixture.json new file mode 100644 index 000000000..e7f86a725 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/chat/snapshots/do_stream_tool_call_fixture.json @@ -0,0 +1,1219 @@ +[ + { + "type": "stream-start", + "warnings": [] + }, + { + "id": "7027d986-3c59-a37a-9a5f-50713e01c8a6", + "modelId": "grok-3-mini", + "timestamp": "2026-02-11T01:11:33.000Z", + "type": "response-metadata" + }, + { + "id": "reasoning-0", + "type": "reasoning-start" + }, + { + "delta": "First", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " asking", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " about", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " weather", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " in", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " San", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Francisco", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " have", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " available", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " called", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "weather", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " retrieves", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " weather", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " given", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " location", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " requires", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " parameter", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "location", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " which", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " string", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " has", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " provided", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "San", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Francisco", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " as", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " location", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " so", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that's", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " clear", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " and", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " infer", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "able", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " call", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " this", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " get", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " weather", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " information", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " format", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " calling", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " specific", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " need", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " use", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " <", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "_call", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ">", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " tags", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " with", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " JSON", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " inside", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " like", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " <", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "_call", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ">{\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "action", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "weather", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "action", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "_input", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " {\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "location", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "San", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " Francisco", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "}}.\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "This", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " seems", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " be", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " direct", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " match", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " so", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " don't", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " need", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " ask", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " clarification", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " My", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " should", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " only", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " contain", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " call", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " if", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " that's", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " next", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " step", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " which", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " it", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "The", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " instructions", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " say", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ":", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " \"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Keep", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " your", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " user", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " clear", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ";", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " please", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " do", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " not", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " make", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " your", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " response", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " verbose", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "!\"", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " So", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " shouldn't", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " add", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " any", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " extra", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " text", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ";", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " just", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " call", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".\n\n", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "Finally", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " after", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " calling", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " function", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " if", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " this", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " were", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " a", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " multi", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": "-turn", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " conversation", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " I", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " might", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " need", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " to", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " respond", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " based", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " on", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " result", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " but", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " for", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " now", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ",", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " this", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " is", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " the", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " logical", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " next", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": " step", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "delta": ".", + "id": "reasoning-0", + "type": "reasoning-delta" + }, + { + "id": "reasoning-0", + "type": "reasoning-end" + }, + { + "id": "call_79382389", + "toolName": "weather", + "type": "tool-input-start" + }, + { + "delta": "{\"location\":\"San Francisco\"}", + "id": "call_79382389", + "type": "tool-input-delta" + }, + { + "id": "call_79382389", + "type": "tool-input-end" + }, + { + "input": "{\"location\":\"San Francisco\"}", + "toolCallId": "call_79382389", + "toolName": "weather", + "type": "tool-call" + }, + { + "finishReason": { + "raw": "tool_calls", + "unified": "tool-calls" + }, + "providerMetadata": { + "test-provider": { + "acceptedPredictionTokens": 0, + "rejectedPredictionTokens": 0 + } + }, + "type": "finish", + "usage": { + "inputTokens": { + "cacheRead": 306, + "noCache": 1, + "total": 307 + }, + "outputTokens": { + "reasoning": 227, + "text": 0, + "total": 26 + }, + "raw": { + "completion_tokens": 26, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 227, + "rejected_prediction_tokens": 0 + }, + "cost_in_usd_ticks": 1497500, + "num_sources_used": 0, + "prompt_tokens": 307, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 306, + "image_tokens": 0, + "text_tokens": 307 + }, + "total_tokens": 560 + } + } + } +] diff --git a/aimux-providers/src/openai_compatible/tests/completion/completion_language_model.rs b/aimux-providers/src/openai_compatible/tests/completion/completion_language_model.rs new file mode 100644 index 000000000..e6f5d3780 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/completion/completion_language_model.rs @@ -0,0 +1,922 @@ +//! Port of `@ai-sdk/openai-compatible` 3.0.62 +//! `src/completion/openai-compatible-completion-language-model.test.ts`. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use futures::StreamExt; +use serde_json::{Value, json}; + +use aimux_core::AiMuxError; +use aimux_core::language_model::LanguageModel; +use aimux_core::language_model_message::LanguageModelMessage; +use aimux_core::options::CallOptions; +use aimux_core::result::StreamResult; +use aimux_core::stream_part::StreamPart; + +use crate::openai_compatible::completion::{CompletionConfig, OpenAICompatibleCompletionModel}; +use crate::openai_compatible::config::{BaseUrl, ChatSettings, CompatModelConfig}; +use crate::openai_compatible::{ + OpenAICompatibleProvider, OpenAICompatibleProviderSettings, create_openai_compatible, +}; +use crate::shared::{Credential, ProviderHeaders}; +use crate::test_support::mock_fetch::{Canned, MockFetch, Seen}; + +/// The user agent aimux pins (upstream mocks `0.0.0-test`); compared apart +/// from the other request headers, which upstream's test server lists +/// without it. +const USER_AGENT: &str = "ai-sdk-openai-compatible/3.0.62 ai-sdk-provider-utils/5.0.53"; + +const PROMPT_TEXT: &str = "user:\nHello\n\nassistant:\n"; + +fn test_prompt() -> Vec { + vec![LanguageModelMessage::user_text("Hello")] +} + +fn call_options() -> CallOptions { + CallOptions::new(test_prompt()) +} + +fn stream_options() -> CallOptions { + let mut options = call_options(); + options.include_raw_chunks = Some(false); + options +} + +fn with_provider_options(mut options: CallOptions, provider_options: Value) -> CallOptions { + options.provider_options = Some(serde_json::from_value(provider_options).unwrap()); + options +} + +fn header_map(entries: &[(&str, &str)]) -> HashMap { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())) + .collect() +} + +/// `createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: +/// 'test-provider', headers })` on the mock transport. +fn create_provider(mock: &Arc, headers: &[(&str, &str)]) -> OpenAICompatibleProvider { + create_openai_compatible(OpenAICompatibleProviderSettings { + base_url: "https://my.api.com/v1/".into(), + name: "test-provider".into(), + headers: Some( + headers + .iter() + .map(|(name, value)| ((*name).to_string(), Some((*value).to_string()))) + .collect(), + ), + fetch: Some(mock.transport()), + ..Default::default() + }) + .unwrap() +} + +/// The module-level `provider` of the upstream file. +fn provider(mock: &Arc) -> OpenAICompatibleProvider { + create_provider(mock, &[("Authorization", "Bearer test-api-key")]) +} + +/// The module-level `model` of the upstream file. +fn model(mock: &Arc) -> OpenAICompatibleCompletionModel { + provider(mock).completion("gpt-3.5-turbo-instruct") +} + +/// `new OpenAICompatibleCompletionLanguageModel(modelId, { provider, url: () +/// => '', headers: () => ({}) })`. +fn model_with_provider(model_id: &str, provider: &str) -> OpenAICompatibleCompletionModel { + OpenAICompatibleCompletionModel::from_config( + model_id.to_string(), + CompatModelConfig { + provider: provider.to_string(), + base_url: BaseUrl::Fixed(String::new()), + query_params: None, + headers: ProviderHeaders::bearer(Credential::None, Vec::new(), None), + fetch_headers: None, + fetch: None, + transform_request_body: None, + chat: ChatSettings::default(), + }, + ) +} + +/// `model['providerOptionsName']`: the name the compatible config derives +/// from the provider string. +fn provider_options_name(model: &OpenAICompatibleCompletionModel) -> String { + match &model.config { + CompletionConfig::Compatible(config) => config.provider_options_name().to_string(), + CompletionConfig::Native(_) => unreachable!("a compatible model"), + } +} + +/// A `json-value` response of the test server: `content-type` and +/// `content-length` are its default headers. +fn json_response(body: &Value, headers: &[(&str, &str)]) -> Canned { + let body = serde_json::to_vec(body).unwrap(); + let mut all = vec![ + ("content-type".to_string(), "application/json".to_string()), + ("content-length".to_string(), body.len().to_string()), + ]; + all.extend( + headers + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())), + ); + Canned { + status: 200, + headers: all, + body, + } +} + +/// A `stream-chunks` response of the test server with its default headers. +fn stream_response(chunks: &[&str], headers: &[(&str, &str)]) -> Canned { + let mut all = vec![ + ("content-type".to_string(), "text/event-stream".to_string()), + ("cache-control".to_string(), "no-cache".to_string()), + ("connection".to_string(), "keep-alive".to_string()), + ]; + all.extend( + headers + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())), + ); + Canned { + status: 200, + headers: all, + body: chunks.concat().into_bytes(), + } +} + +/// The request headers without `user-agent`, which is checked against the +/// pinned value. +fn request_headers(seen: &Seen) -> BTreeMap { + let mut headers = seen.headers.clone(); + assert_eq!( + headers.remove("user-agent").as_deref(), + Some(USER_AGENT), + "pinned user agent" + ); + headers +} + +fn expected_headers(entries: &[(&str, &str)]) -> BTreeMap { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())) + .collect() +} + +/// One stream part as upstream's snapshot shows it. An error part of an +/// `{"error": ...}` chunk carries that chunk's `error` value upstream; aimux +/// wraps it in an API call error whose `data` is that value. +fn part_json(part: StreamPart) -> Value { + match part { + StreamPart::Error { + error: AiMuxError::ApiCall(error), + } => json!({ "type": "error", "error": error.data }), + other => serde_json::to_value(other).unwrap(), + } +} + +async fn stream_parts(result: StreamResult) -> Vec { + result + .stream + .map(|part| part_json(part.expect("stream item"))) + .collect() + .await +} + +mod config { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › config › should extract base name from provider string + #[test] + fn should_extract_base_name_from_provider_string() { + let model = model_with_provider("gpt-5", "anthropic.beta"); + + assert_eq!(provider_options_name(&model), "anthropic"); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › config › should handle provider without dot notation + #[test] + fn should_handle_provider_without_dot_notation() { + let model = model_with_provider("gpt-5", "openai"); + + assert_eq!(provider_options_name(&model), "openai"); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › config › should return empty for empty provider + #[test] + fn should_return_empty_for_empty_provider() { + let model = model_with_provider("gpt-5", ""); + + assert_eq!(provider_options_name(&model), ""); + } +} + +mod do_generate { + use super::*; + + /// The arguments of upstream's `prepareJsonResponse`. + struct JsonResponse { + content: &'static str, + usage: Value, + finish_reason: &'static str, + id: &'static str, + created: i64, + model: &'static str, + headers: &'static [(&'static str, &'static str)], + } + + impl Default for JsonResponse { + fn default() -> Self { + Self { + content: "", + usage: json!({ "prompt_tokens": 4, "total_tokens": 34, "completion_tokens": 30 }), + finish_reason: "stop", + id: "cmpl-96cAM1v77r4jXa4qb2NSmRREV5oWB", + created: 1_711_363_706, + model: "gpt-3.5-turbo-instruct", + headers: &[], + } + } + } + + fn prepare_json_response(response: JsonResponse) -> Arc { + MockFetch::new(vec![json_response( + &json!({ + "id": response.id, + "object": "text_completion", + "created": response.created, + "model": response.model, + "choices": [{ + "text": response.content, + "index": 0, + "finish_reason": response.finish_reason, + }], + "usage": response.usage, + }), + response.headers, + )]) + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should extract text response + #[tokio::test] + async fn should_extract_text_response() { + let mock = prepare_json_response(JsonResponse { + content: "Hello, World!", + ..Default::default() + }); + + let result = model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.content).unwrap(), + json!([{ "text": "Hello, World!", "type": "text" }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should extract usage + #[tokio::test] + async fn should_extract_usage() { + let mock = prepare_json_response(JsonResponse { + usage: json!({ "prompt_tokens": 20, "total_tokens": 25, "completion_tokens": 5 }), + ..Default::default() + }); + + let result = model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ + "inputTokens": { "noCache": 20, "total": 20 }, + "outputTokens": { "text": 5, "total": 5 }, + "raw": { "completion_tokens": 5, "prompt_tokens": 20, "total_tokens": 25 }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should send request body + #[tokio::test] + async fn should_send_request_body() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.request).unwrap(), + json!({ + "body": { + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should send additional response information + #[tokio::test] + async fn should_send_additional_response_information() { + let mock = prepare_json_response(JsonResponse { + id: "test-id", + created: 123, + model: "test-model", + ..Default::default() + }); + + let result = model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.response).unwrap(), + json!({ + "body": { + "choices": [{ "finish_reason": "stop", "index": 0, "text": "" }], + "created": 123, + "id": "test-id", + "model": "test-model", + "object": "text_completion", + "usage": { "completion_tokens": 30, "prompt_tokens": 4, "total_tokens": 34 }, + }, + "headers": { + "content-length": "204", + "content-type": "application/json", + }, + "id": "test-id", + "modelId": "test-model", + "timestamp": "1970-01-01T00:02:03.000Z", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should extract finish reason + #[tokio::test] + async fn should_extract_finish_reason() { + let mock = prepare_json_response(JsonResponse { + finish_reason: "stop", + ..Default::default() + }); + + let result = provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&call_options()) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.finish_reason).unwrap(), + json!({ "raw": "stop", "unified": "stop" }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should support unknown finish reason + #[tokio::test] + async fn should_support_unknown_finish_reason() { + let mock = prepare_json_response(JsonResponse { + finish_reason: "eos", + ..Default::default() + }); + + let result = provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&call_options()) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.finish_reason).unwrap(), + json!({ "raw": "eos", "unified": "other" }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should expose the raw response headers + #[tokio::test] + async fn should_expose_the_raw_response_headers() { + let mock = prepare_json_response(JsonResponse { + headers: &[("test-header", "test-value")], + ..Default::default() + }); + + let result = model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + result.response.unwrap().headers.unwrap(), + header_map(&[ + // default headers: + ("content-length", "250"), + ("content-type", "application/json"), + // custom header + ("test-header", "test-value"), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should pass the model and the prompt + #[tokio::test] + async fn should_pass_the_model_and_the_prompt() { + let mock = prepare_json_response(JsonResponse::default()); + + model(&mock).do_generate(&call_options()).await.unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let mock = prepare_json_response(JsonResponse::default()); + + let provider = create_provider( + &mock, + &[ + ("Authorization", "Bearer test-api-key"), + ("Custom-Provider-Header", "provider-header-value"), + ], + ); + + let mut options = call_options(); + options.headers = Some(header_map(&[( + "Custom-Request-Header", + "request-header-value", + )])); + provider + .completion("gpt-3.5-turbo-instruct") + .do_generate(&options) + .await + .unwrap(); + + assert_eq!( + request_headers(&mock.seen()[0]), + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("content-type", "application/json"), + ("custom-provider-header", "provider-header-value"), + ("custom-request-header", "request-header-value"), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should include provider-specific options + #[tokio::test] + async fn should_include_provider_specific_options() { + let mock = prepare_json_response(JsonResponse::default()); + + provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ "test-provider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "someCustomOption": "test-value", + "stop": ["\nuser:"], + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › should not include provider-specific options for different provider + #[tokio::test] + async fn should_not_include_provider_specific_options_for_different_provider() { + let mock = prepare_json_response(JsonResponse::default()); + + provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ "notThisProviderName": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + }) + ); + } + + mod camel_case_provider_options { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › camelCase provider options › should accept camelCase provider options key for hyphenated provider name + #[tokio::test] + async fn should_accept_camel_case_provider_options_key_for_hyphenated_provider_name() { + let mock = prepare_json_response(JsonResponse::default()); + + provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ "testProvider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body()["someCustomOption"], + json!("test-value") + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › camelCase provider options › should prefer camelCase options over raw-name options + #[tokio::test] + async fn should_prefer_camel_case_options_over_raw_name_options() { + let mock = prepare_json_response(JsonResponse::default()); + + provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ + "test-provider": { "someCustomOption": "raw-value" }, + "testProvider": { "someCustomOption": "camel-value" }, + }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body()["someCustomOption"], + json!("camel-value") + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › camelCase provider options › should emit deprecated warning when raw provider options key is used + #[tokio::test] + async fn should_emit_deprecated_warning_when_raw_provider_options_key_is_used() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ "test-provider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([{ + "message": "Use 'testProvider' instead.", + "setting": "providerOptions key 'test-provider'", + "type": "deprecated", + }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doGenerate › camelCase provider options › should not emit deprecated warning when camelCase provider options key is used + #[tokio::test] + async fn should_not_emit_deprecated_warning_when_camel_case_provider_options_key_is_used() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = provider(&mock) + .completion("gpt-3.5-turbo-instruct") + .do_generate(&with_provider_options( + call_options(), + json!({ "testProvider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!(serde_json::to_value(&result.warnings).unwrap(), json!([])); + } + } +} + +mod do_stream { + use super::*; + + fn prepare_empty_stream_response(headers: &[(&str, &str)]) -> Arc { + MockFetch::new(vec![stream_response( + &[ + "data: {\"id\":\"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H\",\"object\":\"text_completion\",\"created\":1711363310,\"model\":\"gpt-3.5-turbo-instruct\",\"choices\":[{\"text\":\"\",\"index\":0,\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\n", + "data: {\"id\":\"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H\",\"object\":\"text_completion\",\"created\":1711363310,\"model\":\"gpt-3.5-turbo-instruct\",\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":0,\"total_tokens\":10},\"choices\":[]}\n\n", + "data: [DONE]\n\n", + ], + headers, + )]) + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should stream text deltas + #[tokio::test] + async fn should_stream_text_deltas() { + let mock = MockFetch::new(vec![stream_response( + &[ + "data: {\"id\":\"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT\",\"object\":\"text_completion\",\"created\":1711363440,\"model\":\"gpt-3.5-turbo-instruct\",\"choices\":[{\"text\":\"Hello\",\"index\":0,\"logprobs\":null,\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT\",\"object\":\"text_completion\",\"created\":1711363440,\"model\":\"gpt-3.5-turbo-instruct\",\"choices\":[{\"text\":\",\",\"index\":0,\"logprobs\":null,\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT\",\"object\":\"text_completion\",\"created\":1711363440,\"model\":\"gpt-3.5-turbo-instruct\",\"choices\":[{\"text\":\" World!\",\"index\":0,\"logprobs\":null,\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H\",\"object\":\"text_completion\",\"created\":1711363310,\"model\":\"gpt-3.5-turbo-instruct\",\"choices\":[{\"text\":\"\",\"index\":0,\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\n", + "data: {\"id\":\"cmpl-96c3yLQE1TtZCd6n6OILVmzev8M8H\",\"object\":\"text_completion\",\"created\":1711363310,\"model\":\"gpt-3.5-turbo-instruct\",\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":362,\"total_tokens\":372},\"choices\":[]}\n\n", + "data: [DONE]\n\n", + ], + &[], + )]); + + let result = model(&mock).do_stream(&stream_options()).await.unwrap(); + + assert_eq!( + stream_parts(result).await, + vec![ + json!({ "type": "stream-start", "warnings": [] }), + json!({ + "id": "cmpl-96c64EdfhOw8pjFFgVpLuT8k2MtdT", + "modelId": "gpt-3.5-turbo-instruct", + "timestamp": "2024-03-25T10:44:00.000Z", + "type": "response-metadata", + }), + json!({ "id": "0", "type": "text-start" }), + json!({ "delta": "Hello", "id": "0", "type": "text-delta" }), + json!({ "delta": ",", "id": "0", "type": "text-delta" }), + json!({ "delta": " World!", "id": "0", "type": "text-delta" }), + json!({ "delta": "", "id": "0", "type": "text-delta" }), + json!({ "id": "0", "type": "text-end" }), + json!({ + "finishReason": { "raw": "stop", "unified": "stop" }, + "type": "finish", + "usage": { + "inputTokens": { "noCache": 10, "total": 10 }, + "outputTokens": { "text": 362, "total": 362 }, + "raw": { "completion_tokens": 362, "prompt_tokens": 10, "total_tokens": 372 }, + }, + }), + ] + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should handle error stream parts + #[tokio::test] + async fn should_handle_error_stream_parts() { + let mock = MockFetch::new(vec![stream_response( + &[ + "data: {\"error\":{\"message\":\"The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.\",\"type\":\"server_error\",\"param\":null,\"code\":null}}\n\n", + "data: [DONE]\n\n", + ], + &[], + )]); + + let result = model(&mock).do_stream(&stream_options()).await.unwrap(); + + assert_eq!( + stream_parts(result).await, + vec![ + json!({ "type": "stream-start", "warnings": [] }), + json!({ + "error": { + "code": null, + "message": "The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error.", + "param": null, + "type": "server_error", + }, + "type": "error", + }), + json!({ + "finishReason": { "unified": "error" }, + "type": "finish", + "usage": { "inputTokens": {}, "outputTokens": {} }, + }), + ] + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should handle unparsable stream parts + #[tokio::test] + async fn should_handle_unparsable_stream_parts() { + let mock = MockFetch::new(vec![stream_response( + &["data: {unparsable}\n\n", "data: [DONE]\n\n"], + &[], + )]); + + let result = model(&mock).do_stream(&stream_options()).await.unwrap(); + + let mut parts = stream_parts(result).await; + // Upstream's error is `AI_JSONParseError: JSON parsing failed: Text: + // {unparsable}.\nError message: SyntaxError: ...`; the text after + // `Error message:` is the JavaScript parser's own wording. + let error = parts[1]["error"].take(); + assert_eq!(error["name"], json!("AI_JSONParseError"), "{error}"); + assert!( + error["message"].as_str().is_some_and(|message| message + .starts_with("JSON parsing failed: Text: {unparsable}.\nError message: ")), + "{error}" + ); + assert_eq!( + parts, + vec![ + json!({ "type": "stream-start", "warnings": [] }), + json!({ "error": null, "type": "error" }), + json!({ + "finishReason": { "unified": "error" }, + "type": "finish", + "usage": { "inputTokens": {}, "outputTokens": {} }, + }), + ] + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should send request body + #[tokio::test] + async fn should_send_request_body() { + let mock = prepare_empty_stream_response(&[]); + + let result = model(&mock).do_stream(&stream_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.request).unwrap(), + json!({ + "body": { + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + "stream": true, + }, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should expose the raw response headers + #[tokio::test] + async fn should_expose_the_raw_response_headers() { + let mock = prepare_empty_stream_response(&[("test-header", "test-value")]); + + let result = model(&mock).do_stream(&stream_options()).await.unwrap(); + + assert_eq!( + result.response.unwrap().headers.unwrap(), + header_map(&[ + // default headers: + ("content-type", "text/event-stream"), + ("cache-control", "no-cache"), + ("connection", "keep-alive"), + // custom header + ("test-header", "test-value"), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should pass the model and the prompt + #[tokio::test] + async fn should_pass_the_model_and_the_prompt() { + let mock = prepare_empty_stream_response(&[]); + + model(&mock).do_stream(&stream_options()).await.unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + "stream": true, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let mock = prepare_empty_stream_response(&[]); + + let provider = create_provider( + &mock, + &[ + ("Authorization", "Bearer test-api-key"), + ("Custom-Provider-Header", "provider-header-value"), + ], + ); + + let mut options = stream_options(); + options.headers = Some(header_map(&[( + "Custom-Request-Header", + "request-header-value", + )])); + provider + .completion("gpt-3.5-turbo-instruct") + .do_stream(&options) + .await + .unwrap(); + + assert_eq!( + request_headers(&mock.seen()[0]), + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("content-type", "application/json"), + ("custom-provider-header", "provider-header-value"), + ("custom-request-header", "request-header-value"), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should include provider-specific options + #[tokio::test] + async fn should_include_provider_specific_options() { + let mock = prepare_empty_stream_response(&[]); + + model(&mock) + .do_stream(&with_provider_options( + stream_options(), + json!({ "test-provider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "someCustomOption": "test-value", + "stop": ["\nuser:"], + "stream": true, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › should not include provider-specific options for different provider + #[tokio::test] + async fn should_not_include_provider_specific_options_for_different_provider() { + let mock = prepare_empty_stream_response(&[]); + + model(&mock) + .do_stream(&with_provider_options( + stream_options(), + json!({ "notThisProviderName": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "gpt-3.5-turbo-instruct", + "prompt": PROMPT_TEXT, + "stop": ["\nuser:"], + "stream": true, + }) + ); + } + + mod camel_case_provider_options { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › camelCase provider options › should accept camelCase provider options key for hyphenated provider name + #[tokio::test] + async fn should_accept_camel_case_provider_options_key_for_hyphenated_provider_name() { + let mock = prepare_empty_stream_response(&[]); + + model(&mock) + .do_stream(&with_provider_options( + stream_options(), + json!({ "testProvider": { "someCustomOption": "test-value" } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body()["someCustomOption"], + json!("test-value") + ); + } + + /// upstream: @ai-sdk/openai-compatible src/completion/openai-compatible-completion-language-model.test.ts › doStream › camelCase provider options › should prefer camelCase options over raw-name options + #[tokio::test] + async fn should_prefer_camel_case_options_over_raw_name_options() { + let mock = prepare_empty_stream_response(&[]); + + model(&mock) + .do_stream(&with_provider_options( + stream_options(), + json!({ + "test-provider": { "someCustomOption": "raw-value" }, + "testProvider": { "someCustomOption": "camel-value" }, + }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body()["someCustomOption"], + json!("camel-value") + ); + } + } +} diff --git a/aimux-providers/src/openai_compatible/tests/completion/mod.rs b/aimux-providers/src/openai_compatible/tests/completion/mod.rs new file mode 100644 index 000000000..0ae7656d9 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/completion/mod.rs @@ -0,0 +1 @@ +mod completion_language_model; diff --git a/aimux-providers/src/openai_compatible/tests/embedding/embedding_model.rs b/aimux-providers/src/openai_compatible/tests/embedding/embedding_model.rs new file mode 100644 index 000000000..e7e521d34 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/embedding/embedding_model.rs @@ -0,0 +1,362 @@ +//! Port of `@ai-sdk/openai-compatible` 3.0.62 +//! `src/embedding/openai-compatible-embedding-model.test.ts`. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use serde_json::{Value, json}; + +use aimux_core::embedding_model::{EmbeddingCallOptions, EmbeddingModel}; + +use crate::openai_compatible::{ + OpenAICompatibleEmbeddingModel, OpenAICompatibleProvider, OpenAICompatibleProviderSettings, + create_openai_compatible, +}; +use crate::test_support::mock_fetch::{Canned, MockFetch, Seen}; + +/// The user agent aimux pins (upstream mocks `0.0.0-test`); compared apart +/// from the other request headers, which upstream's test server lists +/// without it. +const USER_AGENT: &str = "ai-sdk-openai-compatible/3.0.62 ai-sdk-provider-utils/5.0.53"; + +fn dummy_embeddings() -> Vec> { + vec![vec![0.1, 0.2, 0.3, 0.4, 0.5], vec![0.6, 0.7, 0.8, 0.9, 1.0]] +} + +const TEST_VALUES: [&str; 2] = ["sunny day at the beach", "rainy day in the city"]; + +fn test_values() -> Vec { + TEST_VALUES.iter().map(ToString::to_string).collect() +} + +/// `{ values: testValues }`. +fn embed_options() -> EmbeddingCallOptions { + EmbeddingCallOptions { + values: test_values(), + ..EmbeddingCallOptions::new("") + } +} + +fn with_provider_options( + mut options: EmbeddingCallOptions, + provider_options: Value, +) -> EmbeddingCallOptions { + options.provider_options = Some(serde_json::from_value(provider_options).unwrap()); + options +} + +fn header_map(entries: &[(&str, &str)]) -> HashMap { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())) + .collect() +} + +/// `createOpenAICompatible({ baseURL: 'https://my.api.com/v1/', name: +/// 'test-provider', headers })` on the mock transport. +fn create_provider(mock: &Arc, headers: &[(&str, &str)]) -> OpenAICompatibleProvider { + create_openai_compatible(OpenAICompatibleProviderSettings { + base_url: "https://my.api.com/v1/".into(), + name: "test-provider".into(), + headers: Some( + headers + .iter() + .map(|(name, value)| ((*name).to_string(), Some((*value).to_string()))) + .collect(), + ), + fetch: Some(mock.transport()), + ..Default::default() + }) + .unwrap() +} + +/// The module-level `provider` of the upstream file. +fn provider(mock: &Arc) -> OpenAICompatibleProvider { + create_provider(mock, &[("Authorization", "Bearer test-api-key")]) +} + +/// The module-level `model` of the upstream file. +fn model(mock: &Arc) -> OpenAICompatibleEmbeddingModel { + provider(mock).embedding("text-embedding-3-large") +} + +/// The request headers without `user-agent`, which is checked against the +/// pinned value. +fn request_headers(seen: &Seen) -> BTreeMap { + let mut headers = seen.headers.clone(); + assert_eq!( + headers.remove("user-agent").as_deref(), + Some(USER_AGENT), + "pinned user agent" + ); + headers +} + +mod do_embed { + use super::*; + + /// The arguments of upstream's `prepareJsonResponse`. + struct JsonResponse { + embeddings: Vec>, + usage: Value, + headers: &'static [(&'static str, &'static str)], + } + + impl Default for JsonResponse { + fn default() -> Self { + Self { + embeddings: dummy_embeddings(), + usage: json!({ "prompt_tokens": 8, "total_tokens": 8 }), + headers: &[], + } + } + } + + /// The `json-value` response of the test server: the body as + /// `JSON.stringify` writes it, with its `content-type` and + /// `content-length` headers. + fn prepare_json_response(response: JsonResponse) -> Arc { + let data: Vec = response + .embeddings + .iter() + .enumerate() + .map(|(index, embedding)| { + // `Display` writes `1.0` as `1`, as `JSON.stringify` does. + let numbers: Vec = embedding.iter().map(ToString::to_string).collect(); + format!( + "{{\"object\":\"embedding\",\"index\":{index},\"embedding\":[{}]}}", + numbers.join(",") + ) + }) + .collect(); + let body = format!( + "{{\"object\":\"list\",\"data\":[{}],\"model\":\"text-embedding-3-large\",\"usage\":{}}}", + data.join(","), + serde_json::to_string(&response.usage).unwrap() + ); + let mut headers = vec![ + ("content-type".to_string(), "application/json".to_string()), + ("content-length".to_string(), body.len().to_string()), + ]; + headers.extend( + response + .headers + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())), + ); + MockFetch::new(vec![Canned { + status: 200, + headers, + body: body.into_bytes(), + }]) + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should extract embedding + #[tokio::test] + async fn should_extract_embedding() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = model(&mock).do_embed(&embed_options()).await.unwrap(); + + assert_eq!(result.embeddings, dummy_embeddings()); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should expose the raw response headers + #[tokio::test] + async fn should_expose_the_raw_response_headers() { + let mock = prepare_json_response(JsonResponse { + headers: &[("test-header", "test-value")], + ..Default::default() + }); + + let result = model(&mock).do_embed(&embed_options()).await.unwrap(); + + assert_eq!( + result.response.unwrap().headers.unwrap(), + header_map(&[ + // default headers: + ("content-length", "236"), + ("content-type", "application/json"), + // custom header + ("test-header", "test-value"), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should extract usage + #[tokio::test] + async fn should_extract_usage() { + let mock = prepare_json_response(JsonResponse { + usage: json!({ "prompt_tokens": 20, "total_tokens": 20 }), + ..Default::default() + }); + + let result = model(&mock).do_embed(&embed_options()).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ "tokens": 20 }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should pass the model and the values + #[tokio::test] + async fn should_pass_the_model_and_the_values() { + let mock = prepare_json_response(JsonResponse::default()); + + model(&mock).do_embed(&embed_options()).await.unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "text-embedding-3-large", + "input": TEST_VALUES, + "encoding_format": "float", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should pass the dimensions setting + #[tokio::test] + async fn should_pass_the_dimensions_setting() { + let mock = prepare_json_response(JsonResponse::default()); + + provider(&mock) + .embedding("text-embedding-3-large") + .do_embed(&with_provider_options( + embed_options(), + json!({ "openaiCompatible": { "dimensions": 64 } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "text-embedding-3-large", + "input": TEST_VALUES, + "encoding_format": "float", + "dimensions": 64, + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should pass settings with deprecated openai-compatible key and emit warning + #[tokio::test] + async fn should_pass_settings_with_deprecated_openai_compatible_key_and_emit_warning() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = provider(&mock) + .embedding("text-embedding-3-large") + .do_embed(&with_provider_options( + embed_options(), + json!({ "openai-compatible": { "dimensions": 64 } }), + )) + .await + .unwrap(); + + assert_eq!( + mock.seen()[0].json_body(), + json!({ + "model": "text-embedding-3-large", + "input": TEST_VALUES, + "encoding_format": "float", + "dimensions": 64, + }) + ); + + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([{ + "message": "Use 'openaiCompatible' instead.", + "setting": "providerOptions key 'openai-compatible'", + "type": "deprecated", + }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should emit deprecated warning when raw provider name key is used + #[tokio::test] + async fn should_emit_deprecated_warning_when_raw_provider_name_key_is_used() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = provider(&mock) + .embedding("text-embedding-3-large") + .do_embed(&with_provider_options( + embed_options(), + json!({ "test-provider": { "dimensions": 64 } }), + )) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([{ + "message": "Use 'testProvider' instead.", + "setting": "providerOptions key 'test-provider'", + "type": "deprecated", + }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should not emit deprecated warning when camelCase provider name key is used + #[tokio::test] + async fn should_not_emit_deprecated_warning_when_camel_case_provider_name_key_is_used() { + let mock = prepare_json_response(JsonResponse::default()); + + let result = provider(&mock) + .embedding("text-embedding-3-large") + .do_embed(&with_provider_options( + embed_options(), + json!({ "testProvider": { "dimensions": 64 } }), + )) + .await + .unwrap(); + + assert_eq!(serde_json::to_value(&result.warnings).unwrap(), json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/embedding/openai-compatible-embedding-model.test.ts › doEmbed › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let mock = prepare_json_response(JsonResponse::default()); + + let provider = create_provider( + &mock, + &[ + ("Authorization", "Bearer test-api-key"), + ("Custom-Provider-Header", "provider-header-value"), + ], + ); + + let mut options = embed_options(); + options.headers = Some(header_map(&[( + "Custom-Request-Header", + "request-header-value", + )])); + provider + .embedding("text-embedding-3-large") + .do_embed(&options) + .await + .unwrap(); + + assert_eq!( + request_headers(&mock.seen()[0]), + BTreeMap::from([ + ( + "authorization".to_string(), + "Bearer test-api-key".to_string() + ), + ("content-type".to_string(), "application/json".to_string()), + ( + "custom-provider-header".to_string(), + "provider-header-value".to_string() + ), + ( + "custom-request-header".to_string(), + "request-header-value".to_string() + ), + ]) + ); + } +} diff --git a/aimux-providers/src/openai_compatible/tests/embedding/mod.rs b/aimux-providers/src/openai_compatible/tests/embedding/mod.rs new file mode 100644 index 000000000..55753b1ba --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/embedding/mod.rs @@ -0,0 +1 @@ +mod embedding_model; diff --git a/aimux-providers/src/openai_compatible/tests/image/image_model.rs b/aimux-providers/src/openai_compatible/tests/image/image_model.rs new file mode 100644 index 000000000..8f5e2cab1 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/image/image_model.rs @@ -0,0 +1,845 @@ +//! Port of `@ai-sdk/openai-compatible` 3.0.62 +//! `src/image/openai-compatible-image-model.test.ts`. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use serde_json::{Value, json}; + +use aimux_core::AiMuxError; +use aimux_core::Timestamp; +use aimux_core::image_model::{ + ImageCallOptions, ImageFile, ImageFileData, ImageModel, ImageOutputs, +}; +use aimux_core::shared::{AspectRatio, Size}; +use aimux_provider_utils::{HeaderMapOpt, ProviderErrorParts}; + +use crate::openai_compatible::OpenAICompatibleImageModel; +use crate::openai_compatible::config::{BaseUrl, ChatSettings, CompatModelConfig, ErrorStructure}; +use crate::shared::{Credential, ProviderHeaders}; +use crate::test_support::mock_fetch::{Canned, RouteFetch, Seen}; + +const PROMPT: &str = "A photorealistic astronaut riding a horse"; + +const GENERATIONS_URL: &str = "https://api.example.com/dall-e-3/images/generations"; +const RECRAFT_URL: &str = "https://external.api.recraft.ai/v1/images/generations"; +const EDITS_URL: &str = "https://api.example.com/dall-e-3/images/edits"; + +/// The provider-utils part of the user agent the openai-compatible package +/// pins (upstream's helper layer adds it; its test server lists headers +/// without it). +const USER_AGENT: &str = "ai-sdk-provider-utils/5.0.53"; + +fn json_response(body: Value) -> Canned { + Canned::json(&body) +} + +/// Upstream's `createTestServer` routes: the generation, recraft and edit +/// endpoints with their default responses; `generations` / `edits` replace +/// a route's response. +fn create_test_server(generations: Option, edits: Option) -> Arc { + let generations = generations.unwrap_or_else(|| { + json_response(json!({ + "data": [{ "b64_json": "test1234" }, { "b64_json": "test5678" }], + })) + }); + let recraft = json_response(json!({ "data": [{ "b64_json": "recraft-test-image" }] })); + let edits = edits.unwrap_or_else(|| { + json_response(json!({ "data": [{ "b64_json": "edited-image-base64" }] })) + }); + RouteFetch::new(move |seen: &Seen| match seen.url.as_str() { + GENERATIONS_URL => generations.clone(), + RECRAFT_URL => recraft.clone(), + EDITS_URL => edits.clone(), + _ => Canned { + status: 404, + headers: Vec::new(), + body: Vec::new(), + }, + }) +} + +fn server() -> Arc { + create_test_server(None, None) +} + +fn header_opt_map(entries: &[(&str, &str)]) -> HeaderMapOpt { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), Some((*value).to_string()))) + .collect() +} + +/// `new OpenAICompatibleImageModel(modelId, { provider, headers, url, +/// errorStructure })` on the test server's transport. The base URL is what +/// upstream's `url({ modelId, path })` puts before `path`. +fn image_model( + server: &Arc, + model_id: &str, + provider: &str, + base_url: &str, + headers: &[(&str, &str)], + error_structure: Option, +) -> OpenAICompatibleImageModel { + let mut chat = ChatSettings::default(); + if let Some(error_structure) = error_structure { + chat.error_structure = error_structure; + } + OpenAICompatibleImageModel::from_config( + model_id.to_string(), + CompatModelConfig { + provider: provider.to_string(), + base_url: BaseUrl::Fixed(base_url.to_string()), + query_params: None, + headers: ProviderHeaders::bearer( + Credential::None, + Vec::new(), + Some(header_opt_map(headers)), + ), + fetch_headers: None, + fetch: Some(crate::shared::package_fetch( + Some(server.transport()), + crate::shared::aisdk_pins::OPENAI_COMPATIBLE, + )), + transform_request_body: None, + chat, + }, + ) +} + +/// Upstream's `createBasicModel({ headers, errorStructure })`. +fn create_basic_model( + server: &Arc, + headers: Option<&[(&str, &str)]>, + error_structure: Option, +) -> OpenAICompatibleImageModel { + image_model( + server, + "dall-e-3", + "openai-compatible", + "https://api.example.com/dall-e-3", + headers.unwrap_or(&[("Authorization", "Bearer test-key")]), + error_structure, + ) +} + +/// Upstream's `createDefaultGenerateParams()`. +fn create_default_generate_params() -> ImageCallOptions { + ImageCallOptions { + size: Some(Size::new(1024, 1024)), + headers: Some(HashMap::new()), + ..ImageCallOptions::new(PROMPT) + } +} + +fn with_provider_options( + mut options: ImageCallOptions, + provider_options: Value, +) -> ImageCallOptions { + options.provider_options = serde_json::from_value(provider_options).unwrap(); + options +} + +fn png_file(bytes: &[u8]) -> ImageFile { + ImageFile::File { + media_type: "image/png".to_string(), + data: ImageFileData::Binary(bytes.to_vec()), + } +} + +fn base64_images(outputs: &ImageOutputs) -> Vec { + match outputs { + ImageOutputs::Base64(images) => images.clone(), + ImageOutputs::Binary(_) => panic!("expected base64 images"), + } +} + +/// The request headers without `user-agent`, which is checked against the +/// pinned value. +fn request_headers(seen: &Seen) -> BTreeMap { + let mut headers = seen.headers.clone(); + assert_eq!( + headers.remove("user-agent").as_deref(), + Some(USER_AGENT), + "pinned user agent" + ); + headers +} + +/// The API call error's `message`, `statusCode` and `url` +/// (`rejects.toMatchObject`). +fn api_error_parts(error: &AiMuxError) -> (String, Option, String) { + match error { + AiMuxError::ApiCall(error) => (error.message.clone(), error.status_code, error.url.clone()), + other => panic!("expected an API call error, got {other:?}"), + } +} + +mod open_ai_compatible_image_model { + use super::*; + + mod constructor { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › constructor › should expose correct provider and model information + #[test] + fn should_expose_correct_provider_and_model_information() { + let model = create_basic_model(&server(), None, None); + + assert_eq!(model.provider(), "openai-compatible"); + assert_eq!(model.model_id(), "dall-e-3"); + assert_eq!(model.specification_version(), "v4"); + assert_eq!(model.max_images_per_call(), Some(10)); + assert_eq!(model.supports_file_inputs(), None); + assert_eq!(model.supports_mask_inputs(), None); + } + } + + mod do_generate { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should pass the correct parameters + #[tokio::test] + async fn should_pass_the_correct_parameters() { + let server = server(); + let model = create_basic_model(&server, None, None); + + let mut options = with_provider_options( + create_default_generate_params(), + json!({ "openaiCompatible": { "quality": "hd" } }), + ); + options.n = 2; + model.do_generate(&options).await.unwrap(); + + assert_eq!( + server.seen()[0].json_body(), + json!({ + "model": "dall-e-3", + "prompt": PROMPT, + "n": 2, + "size": "1024x1024", + "quality": "hd", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should use provider name from config for providerOptions key + #[tokio::test] + async fn should_use_provider_name_from_config_for_provider_options_key() { + let server = server(); + let recraft_model = image_model( + &server, + "recraft-v3", + "recraft.image", + "https://external.api.recraft.ai/v1", + &[("Authorization", "Bearer test-key")], + None, + ); + + let mut options = with_provider_options( + create_default_generate_params(), + json!({ "recraft": { "style": "vector_illustration" } }), + ); + options.prompt = Some("A beautiful sunset".to_string()); + recraft_model.do_generate(&options).await.unwrap(); + + assert_eq!( + server.seen()[0].json_body(), + json!({ + "model": "recraft-v3", + "prompt": "A beautiful sunset", + "n": 1, + "size": "1024x1024", + "style": "vector_illustration", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should pass typed image output options + #[tokio::test] + async fn should_pass_typed_image_output_options() { + let server = server(); + let model = create_basic_model(&server, None, None); + + model + .do_generate(&with_provider_options( + create_default_generate_params(), + json!({ + "openaiCompatible": { + "size": "auto", + "quality": "high", + "output_format": "jpeg", + "output_compression": 80, + "background": "opaque", + }, + }), + )) + .await + .unwrap(); + + assert_eq!( + server.seen()[0].json_body(), + json!({ + "model": "dall-e-3", + "prompt": PROMPT, + "n": 1, + "size": "auto", + "quality": "high", + "output_format": "jpeg", + "output_compression": 80, + "background": "opaque", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should emit deprecated warning when raw provider options key is used for hyphenated provider + #[tokio::test] + async fn should_emit_deprecated_warning_for_raw_key_of_hyphenated_provider() { + let server = server(); + let model = image_model( + &server, + "dall-e-3", + "black-forest-labs.image", + "https://api.example.com/dall-e-3", + &[("Authorization", "Bearer test-key")], + None, + ); + + let result = model + .do_generate(&with_provider_options( + create_default_generate_params(), + json!({ "black-forest-labs": { "quality": "hd" } }), + )) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([{ + "message": "Use 'blackForestLabs' instead.", + "setting": "providerOptions key 'black-forest-labs'", + "type": "deprecated", + }]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should not emit deprecated warning when camelCase provider options key is used + #[tokio::test] + async fn should_not_emit_deprecated_warning_when_camel_case_provider_options_key_is_used() { + let server = server(); + let model = image_model( + &server, + "dall-e-3", + "black-forest-labs.image", + "https://api.example.com/dall-e-3", + &[("Authorization", "Bearer test-key")], + None, + ); + + let result = model + .do_generate(&with_provider_options( + create_default_generate_params(), + json!({ "blackForestLabs": { "quality": "hd" } }), + )) + .await + .unwrap(); + + assert_eq!(serde_json::to_value(&result.warnings).unwrap(), json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should add warnings for unsupported settings + #[tokio::test] + async fn should_add_warnings_for_unsupported_settings() { + let server = server(); + let model = create_basic_model(&server, None, None); + + let mut options = create_default_generate_params(); + options.aspect_ratio = Some(AspectRatio::new(16, 9)); + options.seed = Some(123); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.warnings).unwrap(), + json!([ + { + "details": "This model does not support aspect ratio. Use `size` instead.", + "feature": "aspectRatio", + "type": "unsupported", + }, + { + "feature": "seed", + "type": "unsupported", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should pass headers + #[tokio::test] + async fn should_pass_headers() { + let server = server(); + let model_with_headers = create_basic_model( + &server, + Some(&[("Custom-Provider-Header", "provider-header-value")]), + None, + ); + + let mut options = create_default_generate_params(); + options.headers = Some(HashMap::from([( + "Custom-Request-Header".to_string(), + "request-header-value".to_string(), + )])); + model_with_headers.do_generate(&options).await.unwrap(); + + assert_eq!( + request_headers(&server.seen()[0]), + BTreeMap::from([ + ("content-type".to_string(), "application/json".to_string()), + ( + "custom-provider-header".to_string(), + "provider-header-value".to_string() + ), + ( + "custom-request-header".to_string(), + "request-header-value".to_string() + ), + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should handle API errors with custom error structure + #[tokio::test] + async fn should_handle_api_errors_with_custom_error_structure() { + let server = create_test_server( + Some(Canned { + status: 400, + headers: Vec::new(), + body: serde_json::to_vec(&json!({ + "status": "error", + "details": { + "errorMessage": "Custom provider error format", + "errorCode": 1234, + }, + })) + .unwrap(), + }), + None, + ); + + // `errorSchema` + `errorToMessage`: `Error ${errorCode}: ${errorMessage}`. + let error_structure: ErrorStructure = Arc::new(|data: &Value| ProviderErrorParts { + message: format!( + "Error {}: {}", + data["details"]["errorCode"], + data["details"]["errorMessage"].as_str().unwrap_or_default() + ), + provider_code: None, + }); + let model = create_basic_model(&server, None, Some(error_structure)); + + let error = model + .do_generate(&create_default_generate_params()) + .await + .unwrap_err(); + + assert_eq!( + api_error_parts(&error), + ( + "Error 1234: Custom provider error format".to_string(), + Some(400), + GENERATIONS_URL.to_string(), + ) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should handle API errors with default error structure + #[tokio::test] + async fn should_handle_api_errors_with_default_error_structure() { + let server = create_test_server( + Some(Canned { + status: 400, + headers: Vec::new(), + body: serde_json::to_vec(&json!({ + "error": { + "message": "Invalid prompt content", + "type": "invalid_request_error", + "param": null, + "code": null, + }, + })) + .unwrap(), + }), + None, + ); + + let model = create_basic_model(&server, None, None); + + let error = model + .do_generate(&create_default_generate_params()) + .await + .unwrap_err(); + + assert_eq!( + api_error_parts(&error), + ( + "Invalid prompt content".to_string(), + Some(400), + GENERATIONS_URL.to_string(), + ) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should return the raw b64_json content + #[tokio::test] + async fn should_return_the_raw_b64_json_content() { + let model = create_basic_model(&server(), None, None); + let mut options = create_default_generate_params(); + options.n = 2; + let result = model.do_generate(&options).await.unwrap(); + + let images = base64_images(&result.images); + assert_eq!(images.len(), 2); + assert_eq!(images[0], "test1234"); + assert_eq!(images[1], "test5678"); + } + + mod usage { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › usage › should map the usage object reported by the provider + #[tokio::test] + async fn should_map_the_usage_object_reported_by_the_provider() { + let server = create_test_server( + Some(json_response(json!({ + "data": [{ "b64_json": "test1234" }], + "usage": { + "input_tokens": 12, + "output_tokens": 4, + "total_tokens": 16, + }, + }))), + None, + ); + + let model = create_basic_model(&server, None, None); + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ "inputTokens": 12, "outputTokens": 4, "totalTokens": 16 }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › usage › should return undefined usage when the provider omits it + #[tokio::test] + async fn should_return_undefined_usage_when_the_provider_omits_it() { + let model = create_basic_model(&server(), None, None); + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + + assert!(result.usage.is_none()); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › usage › should map null usage fields to undefined + #[tokio::test] + async fn should_map_null_usage_fields_to_undefined() { + let server = create_test_server( + Some(json_response(json!({ + "data": [{ "b64_json": "test1234" }], + "usage": { + "input_tokens": 7, + "output_tokens": null, + "total_tokens": null, + }, + }))), + None, + ); + + let model = create_basic_model(&server, None, None); + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ "inputTokens": 7 }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › usage › should ignore unknown usage fields + #[tokio::test] + async fn should_ignore_unknown_usage_fields() { + let server = create_test_server( + Some(json_response(json!({ + "data": [{ "b64_json": "test1234" }], + "usage": { + "input_tokens": 3, + "output_tokens": 1, + "total_tokens": 4, + "input_tokens_details": { "image_tokens": 2, "text_tokens": 1 }, + }, + }))), + None, + ); + + let model = create_basic_model(&server, None, None); + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ "inputTokens": 3, "outputTokens": 1, "totalTokens": 4 }) + ); + } + } + + mod response_metadata { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › response metadata › should include timestamp, headers and modelId in response + #[tokio::test] + async fn should_include_timestamp_headers_and_model_id_in_response() { + // Upstream injects `currentDate: () => testDate`; aimux has no + // date seam, so the timestamp is checked to be the call's time. + let model = create_basic_model(&server(), None, None); + + let before = Timestamp::now(); + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + let after = Timestamp::now(); + + let timestamp = result.response.timestamp.expect("timestamp"); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(result.response.model_id.as_deref(), Some("dall-e-3")); + assert!(result.response.headers.is_some()); + } + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should use real date when no custom date provider is specified + #[tokio::test] + async fn should_use_real_date_when_no_custom_date_provider_is_specified() { + let before_date = Timestamp::now(); + + let model = image_model( + &server(), + "dall-e-3", + "openai-compatible", + "https://api.example.com/dall-e-3", + &[("Authorization", "Bearer test-key")], + None, + ); + + let result = model + .do_generate(&create_default_generate_params()) + .await + .unwrap(); + + let after_date = Timestamp::now(); + + let timestamp = result.response.timestamp.expect("timestamp"); + assert!(timestamp.unix_millis() >= before_date.unix_millis()); + assert!(timestamp.unix_millis() <= after_date.unix_millis()); + assert_eq!(result.response.model_id.as_deref(), Some("dall-e-3")); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should pass the user setting in the request + #[tokio::test] + async fn should_pass_the_user_setting_in_the_request() { + let server = server(); + let model = create_basic_model(&server, None, None); + + model + .do_generate(&with_provider_options( + create_default_generate_params(), + json!({ "openaiCompatible": { "user": "test-user-id" } }), + )) + .await + .unwrap(); + + assert_eq!( + server.seen()[0].json_body(), + json!({ + "model": "dall-e-3", + "prompt": PROMPT, + "n": 1, + "size": "1024x1024", + "user": "test-user-id", + }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › doGenerate › should not include user field in request when not set via provider options + #[tokio::test] + async fn should_not_include_user_field_in_request_when_not_set_via_provider_options() { + let server = server(); + let model = create_basic_model(&server, None, None); + + model + .do_generate(&with_provider_options( + create_default_generate_params(), + json!({ "openaiCompatible": {} }), + )) + .await + .unwrap(); + + let request_body = server.seen()[0].json_body(); + assert_eq!( + request_body, + json!({ + "model": "dall-e-3", + "prompt": PROMPT, + "n": 1, + "size": "1024x1024", + }) + ); + assert!(request_body.get("user").is_none()); + } + } + + mod image_editing { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should send edit request with files + #[tokio::test] + async fn should_send_edit_request_with_files() { + let edit_server = server(); + let model = create_basic_model(&edit_server, None, None); + + // PNG magic bytes + let image_data = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Turn the cat into a dog".to_string()); + options.files = Some(vec![png_file(&image_data)]); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!(base64_images(&result.images), ["edited-image-base64"]); + assert_eq!(edit_server.seen()[0].url, EDITS_URL); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should send edit request with files and mask + #[tokio::test] + async fn should_send_edit_request_with_files_and_mask() { + let edit_server = server(); + let model = create_basic_model(&edit_server, None, None); + + let image_data = [137, 80, 78, 71]; + let mask_data = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Add a flamingo to the pool".to_string()); + options.files = Some(vec![png_file(&image_data)]); + options.mask = Some(png_file(&mask_data)); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!(base64_images(&result.images), ["edited-image-base64"]); + assert_eq!(edit_server.seen()[0].url, EDITS_URL); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should send edit request with Uint8Array data + #[tokio::test] + async fn should_send_edit_request_with_uint8_array_data() { + let model = create_basic_model(&server(), None, None); + + let image_uint8_array = [104, 101, 108, 108, 111]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Edit this image".to_string()); + options.files = Some(vec![png_file(&image_uint8_array)]); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!(base64_images(&result.images), ["edited-image-base64"]); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should send edit request with multiple images + #[tokio::test] + async fn should_send_edit_request_with_multiple_images() { + let model = create_basic_model(&server(), None, None); + + let image1 = [137, 80, 78, 71]; + let image2 = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Combine these images".to_string()); + options.files = Some(vec![png_file(&image1), png_file(&image2)]); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!(base64_images(&result.images), ["edited-image-base64"]); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should include response metadata for edit requests + #[tokio::test] + async fn should_include_response_metadata_for_edit_requests() { + // Upstream injects `currentDate: () => testDate`; aimux has no + // date seam, so the timestamp is checked to be the call's time. + let model = create_basic_model(&server(), None, None); + + let image_data = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Edit this image".to_string()); + options.files = Some(vec![png_file(&image_data)]); + let before = Timestamp::now(); + let result = model.do_generate(&options).await.unwrap(); + let after = Timestamp::now(); + + let timestamp = result.response.timestamp.expect("timestamp"); + assert!(before <= timestamp && timestamp <= after); + assert_eq!(result.response.model_id.as_deref(), Some("dall-e-3")); + assert!(result.response.headers.is_some()); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should map usage for edit requests + #[tokio::test] + async fn should_map_usage_for_edit_requests() { + let edit_server = create_test_server( + None, + Some(json_response(json!({ + "data": [{ "b64_json": "edited-image-base64" }], + "usage": { + "input_tokens": 20, + "output_tokens": 5, + "total_tokens": 25, + }, + }))), + ); + + let model = create_basic_model(&edit_server, None, None); + let image_data = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Edit this image".to_string()); + options.files = Some(vec![png_file(&image_data)]); + let result = model.do_generate(&options).await.unwrap(); + + assert_eq!( + serde_json::to_value(&result.usage).unwrap(), + json!({ "inputTokens": 20, "outputTokens": 5, "totalTokens": 25 }) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/image/openai-compatible-image-model.test.ts › OpenAICompatibleImageModel › Image Editing › should return undefined usage for edit requests when omitted + #[tokio::test] + async fn should_return_undefined_usage_for_edit_requests_when_omitted() { + let model = create_basic_model(&server(), None, None); + let image_data = [137, 80, 78, 71]; + + let mut options = create_default_generate_params(); + options.prompt = Some("Edit this image".to_string()); + options.files = Some(vec![png_file(&image_data)]); + let result = model.do_generate(&options).await.unwrap(); + + assert!(result.usage.is_none()); + } + } +} diff --git a/aimux-providers/src/openai_compatible/tests/image/mod.rs b/aimux-providers/src/openai_compatible/tests/image/mod.rs new file mode 100644 index 000000000..4fb462e4c --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/image/mod.rs @@ -0,0 +1 @@ +mod image_model; diff --git a/aimux-providers/src/openai_compatible/tests/mod.rs b/aimux-providers/src/openai_compatible/tests/mod.rs new file mode 100644 index 000000000..2b4c89c6c --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/mod.rs @@ -0,0 +1,6 @@ +mod chat; +mod completion; +mod embedding; +mod image; +mod provider; +mod utils; diff --git a/aimux-providers/src/openai_compatible/tests/provider.rs b/aimux-providers/src/openai_compatible/tests/provider.rs new file mode 100644 index 000000000..29205aebf --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/provider.rs @@ -0,0 +1,413 @@ +//! Port of `@ai-sdk/openai-compatible` 3.0.62 +//! `src/openai-compatible-provider.test.ts`. +//! +//! Upstream mocks the model classes and reads the config each constructor +//! was called with. Here the config is the one the returned model holds +//! (`model.config`). `provider(modelId)` and `provider.languageModel(modelId)` +//! are `call` / `Provider::language_model`, which return an +//! `Arc` whose config cannot be read; for them the setting +//! is observed in the request the model sends. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use serde_json::Value; + +use aimux_core::language_model::LanguageModel; +use aimux_core::language_model_message::LanguageModelMessage; +use aimux_core::options::CallOptions; +use aimux_core::types::ProviderMetadata; + +use crate::openai_compatible::completion::{CompletionConfig, OpenAICompatibleCompletionModel}; +use crate::openai_compatible::config::CompatModelConfig; +use crate::openai_compatible::{ + MetadataExtractor, OpenAICompatibleProvider, OpenAICompatibleProviderSettings, + StreamMetadataExtractor, create_openai_compatible, +}; +use crate::test_support::mock_fetch::{Canned, MockFetch}; + +/// The user-agent part the provider headers carry: the pinned +/// `ai-sdk-openai-compatible/` (upstream mocks `0.0.0-test`). +const USER_AGENT: &str = "ai-sdk-openai-compatible/3.0.62"; + +fn header_opt_map(entries: &[(&str, &str)]) -> aimux_provider_utils::HeaderMapOpt { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), Some((*value).to_string()))) + .collect() +} + +fn expected_headers(entries: &[(&str, &str)]) -> BTreeMap { + entries + .iter() + .map(|(name, value)| ((*name).to_string(), (*value).to_string())) + .collect() +} + +/// `config.headers()`, with the user agent checked against the pinned value +/// and left out. +async fn config_headers(config: &CompatModelConfig) -> BTreeMap { + let mut headers: BTreeMap = config + .headers + .resolve() + .await + .unwrap() + .into_iter() + .filter_map(|(name, value)| value.map(|value| (name, value))) + .collect(); + assert_eq!( + headers.remove("user-agent").as_deref(), + Some(USER_AGENT), + "pinned user agent" + ); + headers +} + +/// `config.url({ path })`. +fn config_url(config: &CompatModelConfig, path: &str) -> String { + config.url(path).unwrap().0 +} + +fn completion_config(model: &OpenAICompatibleCompletionModel) -> &CompatModelConfig { + match &model.config { + CompletionConfig::Compatible(config) => config, + CompletionConfig::Native(_) => unreachable!("a compatible model"), + } +} + +/// A server-sent-events chat response; the request body is what the probes +/// read, so the stream itself is never consumed. +fn chat_stream_response() -> Canned { + Canned { + status: 200, + headers: vec![("content-type".into(), "text/event-stream".into())], + body: b"data: [DONE]\n\n".to_vec(), + } +} + +/// The body a `doStream` call of `model` sends with `options`. +async fn sent_stream_body( + model: &dyn LanguageModel, + mock: &Arc, + options: &CallOptions, +) -> Value { + model.do_stream(options).await.unwrap(); + mock.seen().last().expect("a request").json_body() +} + +fn prompt_options() -> CallOptions { + CallOptions::new(vec![LanguageModelMessage::user_text("Hello")]) +} + +/// The `includeUsage` a language model got, read from the +/// `stream_options` it sends (`undefined` and `false` send none). +async fn sends_include_usage(model: &dyn LanguageModel, mock: &Arc) -> Option { + let body = sent_stream_body(model, mock, &prompt_options()).await; + body.get("stream_options") + .map(|stream_options| stream_options["include_usage"].as_bool().unwrap()) +} + +mod open_ai_compatible_provider { + use super::*; + + mod create_open_ai_compatible { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › createOpenAICompatible › should create provider with correct configuration + #[tokio::test] + async fn should_create_provider_with_correct_configuration() { + let options = OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + api_key: Some("test-api-key".into()), + headers: Some(header_opt_map(&[("custom-header", "value")])), + query_params: Some(HashMap::from([( + "Custom-Param".to_string(), + "value".to_string(), + )])), + ..Default::default() + }; + + let provider = create_openai_compatible(options).unwrap(); + // `provider('model-id')` is `call`, which returns `chat('model-id')`. + assert_eq!(provider.call("model-id").provider(), "test-provider.chat"); + let model = provider.chat("model-id"); + + let config = &model.config; + let headers = config_headers(config).await; + + assert_eq!( + headers, + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("custom-header", "value"), + ]) + ); + assert_eq!(config.provider, "test-provider.chat"); + assert_eq!( + config_url(config, "/v1/chat"), + "https://api.example.com/v1/chat?Custom-Param=value" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › createOpenAICompatible › should create headers without authorization when no apiKey provided + #[tokio::test] + async fn should_create_headers_without_authorization_when_no_api_key_provided() { + let options = OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + headers: Some(header_opt_map(&[("custom-header", "value")])), + ..Default::default() + }; + + let provider = create_openai_compatible(options).unwrap(); + // `provider('model-id')` is `call`, which returns `chat('model-id')`. + assert_eq!(provider.call("model-id").provider(), "test-provider.chat"); + let model = provider.chat("model-id"); + + let headers = config_headers(&model.config).await; + + assert_eq!(headers, expected_headers(&[("custom-header", "value")])); + } + } + + mod model_creation_methods { + use super::*; + + fn default_options() -> OpenAICompatibleProviderSettings { + OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + api_key: Some("test-api-key".into()), + headers: Some(header_opt_map(&[("custom-header", "value")])), + query_params: Some(HashMap::from([( + "Custom-Param".to_string(), + "value".to_string(), + )])), + ..Default::default() + } + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › model creation methods › should create chat model with correct configuration + #[tokio::test] + async fn should_create_chat_model_with_correct_configuration() { + let provider = create_openai_compatible(default_options()).unwrap(); + + let model = provider.chat("chat-model"); + + let config = &model.config; + let headers = config_headers(config).await; + + assert_eq!( + headers, + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("custom-header", "value"), + ]) + ); + assert_eq!(config.provider, "test-provider.chat"); + assert_eq!( + config_url(config, "/v1/chat"), + "https://api.example.com/v1/chat?Custom-Param=value" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › model creation methods › should create completion model with correct configuration + #[tokio::test] + async fn should_create_completion_model_with_correct_configuration() { + let provider = create_openai_compatible(default_options()).unwrap(); + + let model = provider.completion("completion-model"); + + let config = completion_config(&model); + let headers = config_headers(config).await; + + assert_eq!( + headers, + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("custom-header", "value"), + ]) + ); + assert_eq!(config.provider, "test-provider.completion"); + assert_eq!( + config_url(config, "/v1/completions"), + "https://api.example.com/v1/completions?Custom-Param=value" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › model creation methods › should create embedding model with correct configuration + #[tokio::test] + async fn should_create_embedding_model_with_correct_configuration() { + let provider = create_openai_compatible(default_options()).unwrap(); + + let model = provider.embedding("embedding-model"); + + let config = &model.config; + let headers = config_headers(config).await; + + assert_eq!( + headers, + expected_headers(&[ + ("authorization", "Bearer test-api-key"), + ("custom-header", "value"), + ]) + ); + assert_eq!(config.provider, "test-provider.embedding"); + assert_eq!( + config_url(config, "/v1/embeddings"), + "https://api.example.com/v1/embeddings?Custom-Param=value" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › model creation methods › should use languageModel as default when called as function + #[test] + fn should_use_language_model_as_default_when_called_as_function() { + let provider = create_openai_compatible(default_options()).unwrap(); + + let model = provider.call("model-id"); + + assert_eq!(model.model_id(), "model-id"); + assert_eq!(model.provider(), "test-provider.chat"); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › model creation methods › should create URL without query parameters when queryParams is not specified + #[test] + fn should_create_url_without_query_parameters_when_query_params_is_not_specified() { + let options = OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + api_key: Some("test-api-key".into()), + ..Default::default() + }; + + let provider = create_openai_compatible(options).unwrap(); + // `provider('model-id')` is `call`, which returns `chat('model-id')`. + assert_eq!(provider.call("model-id").provider(), "test-provider.chat"); + let model = provider.chat("model-id"); + + assert_eq!( + config_url(&model.config, "/v1/chat"), + "https://api.example.com/v1/chat" + ); + } + } + + mod include_usage_setting { + use super::*; + + fn options( + mock: &Arc, + include_usage: Option, + ) -> OpenAICompatibleProviderSettings { + OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + include_usage, + fetch: Some(mock.transport()), + ..Default::default() + } + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › includeUsage setting › should pass includeUsage: true to all model types when specified in provider settings + #[tokio::test] + async fn should_pass_include_usage_true_to_all_model_types_when_specified_in_provider_settings() + { + let mock = MockFetch::new(vec![chat_stream_response()]); + let provider = create_openai_compatible(options(&mock, Some(true))).unwrap(); + + let chat = provider.chat("chat-model"); + assert!(chat.config.chat.include_usage); + + let completion = provider.completion("completion-model"); + assert!(completion_config(&completion).chat.include_usage); + + let model = provider.call("model-id"); + assert_eq!(sends_include_usage(model.as_ref(), &mock).await, Some(true)); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › includeUsage setting › should pass includeUsage: false to all model types when specified in provider settings + #[tokio::test] + async fn should_pass_include_usage_false_to_all_model_types_when_specified_in_provider_settings() + { + let mock = MockFetch::new(vec![chat_stream_response()]); + let provider = create_openai_compatible(options(&mock, Some(false))).unwrap(); + + let chat = provider.chat("chat-model"); + assert!(!chat.config.chat.include_usage); + + let completion = provider.completion("completion-model"); + assert!(!completion_config(&completion).chat.include_usage); + + let model = provider.call("model-id"); + // `false` and `undefined` both send no `stream_options`. + assert_eq!(sends_include_usage(model.as_ref(), &mock).await, None); + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › includeUsage setting › should pass includeUsage: undefined to all model types when not specified in provider settings + #[tokio::test] + async fn should_pass_include_usage_undefined_to_all_model_types_when_not_specified() { + let mock = MockFetch::new(vec![chat_stream_response()]); + let provider = create_openai_compatible(options(&mock, None)).unwrap(); + + // The model config holds a `bool`: upstream's `undefined` is its + // `false` default. + let chat = provider.chat("chat-model"); + assert!(!chat.config.chat.include_usage); + + let completion = provider.completion("completion-model"); + assert!(!completion_config(&completion).chat.include_usage); + + let model = provider.call("model-id"); + assert_eq!(sends_include_usage(model.as_ref(), &mock).await, None); + } + } + + mod metadata_extractor_setting { + use super::*; + + struct MockExtractor; + + struct MockStreamExtractor; + + impl MetadataExtractor for MockExtractor { + fn extract_metadata(&self, _parsed_body: &Value) -> Option { + None + } + + fn create_stream_extractor(&self) -> Box { + Box::new(MockStreamExtractor) + } + } + + impl StreamMetadataExtractor for MockStreamExtractor { + fn process_chunk(&mut self, _parsed_chunk: &Value) {} + + fn build_metadata(&self) -> Option { + None + } + } + + /// upstream: @ai-sdk/openai-compatible src/openai-compatible-provider.test.ts › OpenAICompatibleProvider › metadataExtractor setting › should pass metadataExtractor to chat model + #[test] + fn should_pass_metadata_extractor_to_chat_model() { + let mock_extractor: Arc = Arc::new(MockExtractor); + + let provider: OpenAICompatibleProvider = + create_openai_compatible(OpenAICompatibleProviderSettings { + base_url: "https://api.example.com".into(), + name: "test-provider".into(), + metadata_extractor: Some(mock_extractor.clone()), + ..Default::default() + }) + .unwrap(); + + let chat = provider.chat("chat-model"); + + assert!(Arc::ptr_eq( + chat.config.chat.metadata_extractor.as_ref().unwrap(), + &mock_extractor + )); + } + } +} diff --git a/aimux-providers/src/openai_compatible/tests/utils/mod.rs b/aimux-providers/src/openai_compatible/tests/utils/mod.rs new file mode 100644 index 000000000..a8f67761c --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/utils/mod.rs @@ -0,0 +1 @@ +mod to_camel_case; diff --git a/aimux-providers/src/openai_compatible/tests/utils/to_camel_case.rs b/aimux-providers/src/openai_compatible/tests/utils/to_camel_case.rs new file mode 100644 index 000000000..9f1f703d2 --- /dev/null +++ b/aimux-providers/src/openai_compatible/tests/utils/to_camel_case.rs @@ -0,0 +1,216 @@ +use serde_json::{Value, json}; + +use aimux_core::options::CallOptions; +use aimux_core::types::Warning; + +use crate::openai_compatible::config::ChatSettings; +use crate::openai_compatible::convert::{ + ChatBodySpec, RequestBodyResult, build_request_body, to_camel_case, +}; + +/// `resolveProviderOptionsKey` and `warnIfDeprecatedProviderOptionsKey` have +/// no standalone Rust form: both run inside `build_request_body` (the +/// metadata key it reports, and the deprecation warning it raises). An empty +/// prompt with the baseline chat settings raises no other warning. +fn build(raw_name: &str, provider_options: Option) -> RequestBodyResult { + let options = CallOptions { + provider_options: provider_options + .map(|value| serde_json::from_value(value).expect("provider options")), + ..CallOptions::default() + }; + build_request_body( + "model", + &options, + false, + &ChatBodySpec { + provider_options_name: raw_name, + chat: &ChatSettings::default(), + }, + ) + .expect("request body") +} + +fn resolve_provider_options_key(raw_name: &str, provider_options: Option) -> String { + build(raw_name, provider_options).metadata_key +} + +fn warn_if_deprecated_provider_options_key( + raw_name: &str, + provider_options: Option, +) -> Vec { + build(raw_name, provider_options).warnings +} + +fn warnings_json(warnings: &[Warning]) -> Value { + serde_json::to_value(warnings).expect("warnings serialize") +} + +// The `toCamelCase` describe block shares the file's name. +#[allow(clippy::module_inception)] +mod to_camel_case { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should convert hyphenated names to camelCase + #[test] + fn should_convert_hyphenated_names_to_camel_case() { + assert_eq!(to_camel_case("provider-name"), "providerName"); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should convert underscored names to camelCase + #[test] + fn should_convert_underscored_names_to_camel_case() { + assert_eq!(to_camel_case("provider_name"), "providerName"); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should handle multiple separators + #[test] + fn should_handle_multiple_separators() { + assert_eq!(to_camel_case("my-provider-name"), "myProviderName"); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should return the same string when already camelCase + #[test] + fn should_return_the_same_string_when_already_camel_case() { + assert_eq!(to_camel_case("providerName"), "providerName"); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should return the same string when no separators + #[test] + fn should_return_the_same_string_when_no_separators() { + assert_eq!(to_camel_case("openai"), "openai"); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › toCamelCase › should handle empty string + #[test] + fn should_handle_empty_string() { + assert_eq!(to_camel_case(""), ""); + } +} + +mod resolve_provider_options_key { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return camelCase key when camelCase options are present + #[test] + fn should_return_camel_case_key_when_camel_case_options_are_present() { + assert_eq!( + resolve_provider_options_key( + "provider-name", + Some(json!({ "providerName": { "someOption": "value" } })), + ), + "providerName" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return raw key when only raw options are present + #[test] + fn should_return_raw_key_when_only_raw_options_are_present() { + assert_eq!( + resolve_provider_options_key( + "provider-name", + Some(json!({ "provider-name": { "someOption": "value" } })), + ), + "provider-name" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return camelCase key when both are present + #[test] + fn should_return_camel_case_key_when_both_are_present() { + assert_eq!( + resolve_provider_options_key( + "provider-name", + Some(json!({ + "provider-name": { "a": 1 }, + "providerName": { "b": 2 }, + })), + ), + "providerName" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return raw key when no options are present + #[test] + fn should_return_raw_key_when_no_options_are_present() { + assert_eq!( + resolve_provider_options_key("provider-name", Some(json!({}))), + "provider-name" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return raw key when providerOptions is undefined + #[test] + fn should_return_raw_key_when_provider_options_is_undefined() { + assert_eq!( + resolve_provider_options_key("provider-name", None), + "provider-name" + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › resolveProviderOptionsKey › should return raw key when name has no separators + #[test] + fn should_return_raw_key_when_name_has_no_separators() { + assert_eq!( + resolve_provider_options_key("openai", Some(json!({ "openai": { "a": 1 } }))), + "openai" + ); + } +} + +mod warn_if_deprecated_provider_options_key { + use super::*; + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › warnIfDeprecatedProviderOptionsKey › should push a deprecated warning when raw key is used and differs from camelCase + #[test] + fn should_push_a_deprecated_warning_when_raw_key_is_used_and_differs() { + let warnings = warn_if_deprecated_provider_options_key( + "black-forest-labs", + Some(json!({ "black-forest-labs": { "style": "hd" } })), + ); + assert_eq!( + warnings_json(&warnings), + json!([ + { + "type": "deprecated", + "setting": "providerOptions key 'black-forest-labs'", + "message": "Use 'blackForestLabs' instead.", + }, + ]) + ); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › warnIfDeprecatedProviderOptionsKey › should not push a warning when only camelCase key is used + #[test] + fn should_not_push_a_warning_when_only_camel_case_key_is_used() { + let warnings = warn_if_deprecated_provider_options_key( + "black-forest-labs", + Some(json!({ "blackForestLabs": { "style": "hd" } })), + ); + assert_eq!(warnings_json(&warnings), json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › warnIfDeprecatedProviderOptionsKey › should not push a warning when raw name is already camelCase + #[test] + fn should_not_push_a_warning_when_raw_name_is_already_camel_case() { + let warnings = warn_if_deprecated_provider_options_key( + "openai", + Some(json!({ "openai": { "user": "test" } })), + ); + assert_eq!(warnings_json(&warnings), json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › warnIfDeprecatedProviderOptionsKey › should not push a warning when raw key is not present in providerOptions + #[test] + fn should_not_push_a_warning_when_raw_key_is_not_present_in_provider_options() { + let warnings = + warn_if_deprecated_provider_options_key("black-forest-labs", Some(json!({}))); + assert_eq!(warnings_json(&warnings), json!([])); + } + + /// upstream: @ai-sdk/openai-compatible src/utils/to-camel-case.test.ts › warnIfDeprecatedProviderOptionsKey › should not push a warning when providerOptions is undefined + #[test] + fn should_not_push_a_warning_when_provider_options_is_undefined() { + let warnings = warn_if_deprecated_provider_options_key("black-forest-labs", None); + assert_eq!(warnings_json(&warnings), json!([])); + } +} diff --git a/aimux-providers/tests/openai_compatible_factory_test.rs b/aimux-providers/tests/openai_compatible_factory_test.rs index 077bd1fd9..44da42f92 100644 --- a/aimux-providers/tests/openai_compatible_factory_test.rs +++ b/aimux-providers/tests/openai_compatible_factory_test.rs @@ -562,6 +562,7 @@ async fn chat_basic() { replay_groq_chat("chat-basic").await; } +/// partial: `should include provider-specific options`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. #[tokio::test] async fn chat_provider_options_namespace() { // `groq.foo` passes through at the top level; `openaiCompatible.user` is a @@ -584,6 +585,8 @@ async fn unknown_fields_of_the_generic_namespace_are_not_forwarded() { ); } +/// partial: `should pass headers`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. +/// partial: `should create provider with correct configuration`; full port in src/openai_compatible/tests/provider.rs. #[tokio::test] async fn chat_query_params_and_headers() { let fixture = Fixture::load("chat-query-params-and-headers"); @@ -620,6 +623,7 @@ async fn chat_query_params_and_headers() { ); } +/// partial: `should create headers without authorization when no apiKey provided`; full port in src/openai_compatible/tests/provider.rs. #[tokio::test] async fn chat_without_an_api_key_sends_no_authorization() { let fixture = Fixture::load("chat-no-api-key"); @@ -744,6 +748,9 @@ async fn chat_stream_basic() { ); } +/// partial: `should extract embedding`; full port in src/openai_compatible/tests/embedding/embedding_model.rs. +/// partial: `should extract usage`; full port in src/openai_compatible/tests/embedding/embedding_model.rs. +/// partial: `should pass the model and the values`; full port in src/openai_compatible/tests/embedding/embedding_model.rs. #[tokio::test] async fn embedding_basic() { let fixture = Fixture::load("embedding-basic"); diff --git a/aimux-providers/tests/openai_compatible_upstream_e2e_test.rs b/aimux-providers/tests/openai_compatible_upstream_e2e_test.rs index 0265b6f33..2a3ccaa0a 100644 --- a/aimux-providers/tests/openai_compatible_upstream_e2e_test.rs +++ b/aimux-providers/tests/openai_compatible_upstream_e2e_test.rs @@ -71,8 +71,8 @@ fn stream_request() -> Value { json!({"model":"test-model","messages":[{"role":"user","content":"Hello"}],"stream":true}) } -/// TS: "should use camelCase metadata key when camelCase provider options are used" (openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts) -/// TS: "should extract detailed token usage when available" (openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts) +/// partial: `should use camelCase metadata key when camelCase provider options are used`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. +/// partial: `should extract detailed token usage when available`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. #[tokio::test] async fn chat_generate_metadata_and_usage() { let usage = json!({"prompt_tokens":20,"completion_tokens":30,"total_tokens":50, @@ -127,7 +127,7 @@ async fn chat_generate_metadata_and_usage() { assert!(result.warnings.is_empty()); } -/// TS: "should stream reasoning content before text deltas" (openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts) +/// partial: `should stream reasoning content before text deltas`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. #[tokio::test] async fn chat_stream_reasoning_before_text() { let delta = |value: Value| { @@ -200,7 +200,7 @@ async fn chat_stream_reasoning_before_text() { ); } -/// TS: "should parse thought signature from extra_content and include in providerMetadata" (openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts) +/// partial: `should parse thought signature from extra_content and include in providerMetadata`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. #[tokio::test] async fn chat_tool_call_preserves_arguments_and_signature() { let fetch = MockFetch::new(vec![Canned::json(&chat_response( @@ -244,7 +244,7 @@ async fn chat_tool_call_preserves_arguments_and_signature() { ); } -/// TS: "should preserve structured error stream parts" (openai-compatible/src/chat/openai-compatible-chat-language-model.test.ts) +/// partial: `should preserve structured error stream parts`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. #[tokio::test] async fn chat_stream_preserves_structured_error() { let fetch = MockFetch::new(vec![stream_response(&[json!({"error":{ @@ -364,8 +364,8 @@ async fn completion_stream_error_infers_no_status() { assert!(!error.is_retryable); } -/// TS: "should extract embedding" (openai-compatible/src/embedding/openai-compatible-embedding-model.test.ts) -/// TS: "should pass the model and the values" (openai-compatible/src/embedding/openai-compatible-embedding-model.test.ts) +/// partial: `should extract embedding`; full port in src/openai_compatible/tests/embedding/embedding_model.rs. +/// partial: `should pass the model and the values`; full port in src/openai_compatible/tests/embedding/embedding_model.rs. #[tokio::test] async fn embedding_request_and_result() { let body = json!({"object":"list","data":[ @@ -406,8 +406,8 @@ fn image_options() -> ImageCallOptions { opts } -/// TS: "should return the raw b64_json content" (openai-compatible/src/image/openai-compatible-image-model.test.ts) -/// TS: "should map the usage object reported by the provider" (openai-compatible/src/image/openai-compatible-image-model.test.ts) +/// partial: `should return the raw b64_json content`; full port in src/openai_compatible/tests/image/image_model.rs. +/// partial: `should map the usage object reported by the provider`; full port in src/openai_compatible/tests/image/image_model.rs. #[tokio::test] async fn image_generate_raw_content_and_usage() { let fetch = MockFetch::new(vec![Canned::json(&json!({"data":[ @@ -442,7 +442,7 @@ async fn image_generate_raw_content_and_usage() { assert!(result.warnings.is_empty()); } -/// TS: "should handle API errors with default error structure" (openai-compatible/src/image/openai-compatible-image-model.test.ts) +/// partial: `should handle API errors with default error structure`; full port in src/openai_compatible/tests/image/image_model.rs. #[tokio::test] async fn image_generate_api_error() { let mut canned = Canned::json(&json!({"error":{"message":"Invalid prompt content", @@ -520,6 +520,9 @@ fn multi_part_provider( .unwrap() } +/// partial: `should pass multi-part tool content when supported`; full port in src/openai_compatible/tests/chat/chat_language_model.rs. +/// partial: `should convert multi-part tool content when supported`; full port in src/openai_compatible/tests/chat/convert_to_chat_messages.rs. +/// partial: `should stringify multi-part tool content by default`; full port in src/openai_compatible/tests/chat/convert_to_chat_messages.rs. /// Upstream: @ai-sdk/openai-compatible 3.0.62 /// src/chat/convert-to-openai-compatible-chat-messages.ts:303-306 sends a /// `content` tool output as an array of content parts diff --git a/scripts/check_provider_boundaries.sh b/scripts/check_provider_boundaries.sh index 54d3fb7fc..72b71d382 100755 --- a/scripts/check_provider_boundaries.sh +++ b/scripts/check_provider_boundaries.sh @@ -153,13 +153,13 @@ violation 'groq / deepseek literal in the shared OpenAI or compatible package' " # vendor's endpoint, base URL, headers or usage accounting is its own data # behind a trait or hook of the shared layer. Their code (doc links aside) # names only the shared layers (including `stream`), the preset registry and -# `runtime_decision`. +# `runtime_decision`; the in-crate tests also name `test_support`. rule4c_hits="$( grep -rnE 'crate::[a-z_]+' aimux-providers/src/shared \ aimux-providers/src/openai_compatible --include='*.rs' | awk '{ code = $0; sub(/^[^:]*:[0-9]+:/, "", code) } code ~ /^[[:space:]]*\/\// { next } - { gsub(/crate::(shared|stream|openai|openai_compatible|preset|runtime_decision)([^a-z_]|$)/, "", code) } + { gsub(/crate::(shared|stream|openai|openai_compatible|preset|runtime_decision|test_support)([^a-z_]|$)/, "", code) } code ~ /crate::[a-z_]/' || true )" violation 'shared or openai_compatible names a vendor package (implement its trait in the vendor module)' "$rule4c_hits"