use super::super::tests::{recorded, recorded_json};
use super::*;
use crate::error::{ErrorKind, ProviderError};
use crate::providers::doubleword::QWEN3_EMBEDDING_8B;
use crate::providers::mistral::embedding::{CODESTRAL_EMBED, MISTRAL_EMBED};
use crate::providers::openai::embedding::TEXT_EMBEDDING_ADA_002;
use crate::providers::openai::wire::{
AZURE, DOUBLEWORD, Dialect, LLAMACPP, MISTRAL, OPENAI, OpenAIConfig, TOGETHER,
};
use crate::test_utils::{RecordingHttpClient, json_body};
fn documents() -> Vec<String> {
vec![
"Rust values memory safety and predictable performance.".to_owned(),
"Streaming responses arrive incrementally instead of all at once.".to_owned(),
"Embeddings turn text into numeric vectors for similarity search.".to_owned(),
]
}
#[tokio::test]
async fn a_recorded_embedding_reply_zips_onto_the_requests_inputs() {
let reply = recorded(
"then",
"embedding_matrix/normalized_response_is_complete.yaml",
);
let wire = OpenAIConfig::new("sk-test").embedding("text-embedding-3-small", None);
let bound = crate::driver::Model::new(wire, RecordingHttpClient::new(reply));
let response = bound
.call(documents())
.await
.expect("the recorded reply decodes");
let inputs: Vec<&str> = response
.embeddings
.iter()
.map(|embedding| embedding.document.as_str())
.collect();
assert_eq!(
inputs,
documents().iter().map(String::as_str).collect::<Vec<_>>(),
"vectors must stay paired with the input they belong to, in order"
);
assert!(
response
.embeddings
.iter()
.all(|embedding| embedding.vec.len() == 1536),
"widths: {:?}",
response
.embeddings
.iter()
.map(|embedding| embedding.vec.len())
.collect::<Vec<_>>()
);
assert_eq!(response.provider, "openai");
assert_eq!(response.model.as_deref(), Some("text-embedding-3-small"));
assert!(response.usage.input_tokens.is_some());
}
#[tokio::test]
async fn a_short_embedding_reply_fails_the_call() {
let reply = r#"{"object":"list","model":"m","data":[{"object":"embedding","index":0,"embedding":[0.5]}],"usage":{"prompt_tokens":1,"total_tokens":1}}"#;
let bound = crate::driver::Model::new(
OpenAIConfig::new("sk-test").embedding("text-embedding-3-small", None),
RecordingHttpClient::new(reply),
);
let error = bound
.call(documents())
.await
.expect_err("three inputs and one vector cannot pair up");
assert!(
error.to_string().contains('1') && error.to_string().contains('3'),
"the error names both counts: {error}"
);
}
#[test]
fn a_requested_width_matches_the_recorded_request() {
let encoded = OpenAIConfig::new("sk-test")
.embedding("text-embedding-3-small", Some(512))
.encode(documents(), Mode::Unary)
.expect("the request encodes");
let body = json_body(&encoded.request);
assert_eq!(
body,
recorded_json("when", "embedding_matrix/dimensions_request.yaml")
);
}
#[test]
fn the_dialect_decides_the_width_field() {
fn width_field(dialect: &Dialect, model: &str) -> Option<String> {
let encoded = OpenAIConfig::with_key(dialect, "k")
.embedding(model, Some(256))
.encode(documents(), Mode::Unary)
.expect("the request encodes");
let body = json_body(&encoded.request);
["dimensions", "output_dimension"]
.into_iter()
.find(|field| body.get(*field).is_some())
.map(str::to_owned)
}
assert_eq!(
width_field(&OPENAI, "text-embedding-3-small"),
Some("dimensions".to_owned())
);
assert_eq!(
width_field(&MISTRAL, "codestral-embed"),
Some("output_dimension".to_owned())
);
assert_eq!(
width_field(&LLAMACPP, "nomic-embed"),
None,
"`llama-server` ignores a width field, so sending one would leave \
`ndims()` describing vectors it never returned"
);
assert_eq!(width_field(&OPENAI, TEXT_EMBEDDING_ADA_002), None);
}
#[test]
fn azure_sends_no_model_field() {
let encoded = OpenAIConfig::with_key(&AZURE, "k")
.with_base_url("https://example.openai.azure.com")
.with_api_version("2024-10-21")
.embedding("my-deployment", None)
.encode(documents(), Mode::Unary)
.expect("the request encodes");
let request = &encoded.request;
assert_eq!(
request.uri().to_string(),
"https://example.openai.azure.com/openai/deployments/my-deployment/embeddings?api-version=2024-10-21"
);
let body = json_body(request);
assert!(body.get("model").is_none(), "{body}");
}
#[tokio::test]
async fn a_usage_less_reply_fails_a_dialect_that_requires_usage() {
let reply = r#"{"object":"list","model":"m","data":[{"object":"embedding","index":0,"embedding":[0.5]}]}"#;
let error = crate::driver::Model::new(
OpenAIConfig::new("sk-test").embedding("text-embedding-3-small", None),
RecordingHttpClient::new(reply),
)
.call(vec!["one".to_owned()])
.await
.expect_err("OpenAI always reports usage");
assert_eq!(error.kind(), ErrorKind::Response, "{error}");
assert!(
error
.to_string()
.contains("openai embedding response omitted required usage"),
"{error}"
);
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&TOGETHER, "k")
.embedding("togethercomputer/m2-bert-80M-8k-retrieval", None),
RecordingHttpClient::new(reply),
)
.call(vec!["one".to_owned()])
.await
.expect("Together may omit usage");
assert_eq!(response.embeddings.len(), 1);
}
#[tokio::test]
async fn a_recorded_model_listing_decodes() {
let reply = recorded("then", "models/list_models_smoke.yaml");
let models = crate::driver::Model::new(
OpenAIConfig::new("sk-test").models(),
RecordingHttpClient::new(reply),
)
.list()
.await
.expect("the recorded catalogue decodes");
assert!(!models.is_empty(), "the catalogue is not empty");
assert!(
models.iter().all(|model| !model.id.is_empty()),
"every entry names a model"
);
}
#[test]
fn a_transcription_request_is_multipart() {
let request = crate::transcription::TranscriptionRequest {
data: b"RIFF".to_vec(),
filename: "clip.wav".to_owned(),
language: Some("en".to_owned()),
prompt: None,
temperature: None,
additional_params: Some(serde_json::json!({"response_format": "verbose_json"})),
};
let encoded = OpenAIConfig::new("sk-test")
.transcription("whisper-1")
.encode(request, Mode::Unary)
.expect("the request encodes");
let http_request = &encoded.request;
let Body::Multipart(form) = http_request.body() else {
panic!("a transcription body is multipart");
};
let names: Vec<&str> = form.parts().iter().map(|part| part.name()).collect();
assert_eq!(
names,
vec!["model", "file", "language", "response_format"],
"field order is the order these endpoints were always sent in"
);
assert_eq!(
http_request.uri().to_string(),
"https://api.openai.com/v1/audio/transcriptions"
);
}
#[cfg(feature = "image")]
#[tokio::test]
async fn the_xai_image_body_and_reply_differ_from_openais() {
use crate::providers::xai::DIALECT as XAI;
let request = || crate::image_generation::ImageGenerationRequest {
prompt: "a cat".to_owned(),
width: 1024,
height: 1024,
additional_params: None,
};
let encoded = OpenAIConfig::with_key(&XAI, "xai-key")
.image_generation("grok-imagine-image-pro")
.encode(request(), Mode::Unary)
.expect("the request encodes");
let http_request = &encoded.request;
assert_eq!(
http_request.uri().to_string(),
"https://api.x.ai/v1/images/generations"
);
let body = json_body(http_request);
assert_eq!(body["response_format"], "b64_json");
assert_eq!(body["aspect_ratio"], "1:1");
assert!(body.get("size").is_none(), "xAI takes no `size`: {body}");
let openai = OpenAIConfig::new("sk")
.image_generation("gpt-image-1")
.encode(request(), Mode::Unary)
.expect("encodes");
let openai_body = json_body(&openai.request);
assert_eq!(openai_body["size"], "1024x1024");
assert!(openai_body.get("aspect_ratio").is_none());
let reply = r#"{"data":[{"b64_json":"aGk="}]}"#;
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&XAI, "k").image_generation("grok-imagine-image-pro"),
RecordingHttpClient::new(reply),
)
.call(request())
.await
.expect("a reply without `created` still decodes");
assert_eq!(response.image, b"hi");
assert_eq!(response.provider, "xai");
}
#[cfg(feature = "audio")]
#[test]
fn the_xai_speech_body_differs_from_openais() {
use crate::providers::xai::DIALECT as XAI;
let request = |voice: &str| crate::audio_generation::AudioGenerationRequest {
text: "hello".to_owned(),
voice: voice.to_owned(),
speed: 1.0,
additional_params: None,
};
let encoded = OpenAIConfig::with_key(&XAI, "k")
.audio_generation("tts-1")
.encode(request("nova"), Mode::Unary)
.expect("encodes");
let http_request = &encoded.request;
assert_eq!(http_request.uri().to_string(), "https://api.x.ai/v1/tts");
let body = json_body(&encoded.request);
assert_eq!(body["voice_id"], "nova");
assert_eq!(body["text"], "hello");
assert_eq!(body["language"], "en");
assert!(body.get("model").is_none(), "xAI's tts takes no model");
let defaulted = OpenAIConfig::with_key(&XAI, "k")
.audio_generation("tts-1")
.encode(request(""), Mode::Unary)
.expect("encodes");
assert_eq!(json_body(&defaulted.request)["voice_id"], "eve");
let openai = OpenAIConfig::new("sk")
.audio_generation("tts-1")
.encode(request("nova"), Mode::Unary)
.expect("encodes");
let body = json_body(&openai.request);
assert_eq!(body["voice"], "nova");
assert_eq!(body["input"], "hello");
assert_eq!(body["model"], "tts-1");
}
#[cfg(feature = "audio")]
#[test]
fn azure_speech_carries_its_own_api_version() {
let provider = OpenAIConfig::with_key(&AZURE, "k")
.with_base_url("https://example.openai.azure.com")
.with_api_version("2024-10-21")
.with_audio_api_version("2025-04-01-preview");
let encoded = provider
.audio_generation("my-tts")
.encode(
crate::audio_generation::AudioGenerationRequest {
text: "hi".to_owned(),
voice: "alloy".to_owned(),
speed: 1.0,
additional_params: None,
},
Mode::Unary,
)
.expect("encodes");
let request = &encoded.request;
assert_eq!(
request.uri().to_string(),
"https://example.openai.azure.com/openai/deployments/my-tts/audio/speech?api-version=2025-04-01-preview"
);
let embeddings = OpenAIConfig::with_key(&AZURE, "k")
.with_base_url("https://example.openai.azure.com")
.with_api_version("2024-10-21")
.embedding("my-embed", None)
.encode(vec!["a".to_owned()], Mode::Unary)
.expect("encodes");
let request = &embeddings.request;
assert!(
request
.uri()
.to_string()
.ends_with("?api-version=2024-10-21"),
"{}",
request.uri()
);
}
#[tokio::test]
async fn a_recorded_rerank_reply_folds_its_ranking() {
let reply = r#"{"model":"bge-reranker-v2-m3","object":"list","usage":{"prompt_tokens":37,"total_tokens":37},"results":[{"index":2,"relevance_score":0.98},{"index":0,"relevance_score":0.41},{"index":1,"relevance_score":0.02}]}"#;
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&LLAMACPP, "").rerank("bge-reranker-v2-m3"),
RecordingHttpClient::new(reply),
)
.call(crate::operation::RerankRequest {
query: "which is about cats?".to_owned(),
documents: vec![
"dogs bark".to_owned(),
"the sky is blue".to_owned(),
"cats purr".to_owned(),
],
})
.await
.expect("the reply decodes");
let ranked: Vec<(usize, f64)> = response
.results
.iter()
.map(|result| (result.index, result.relevance_score))
.collect();
assert_eq!(ranked, vec![(2, 0.98), (0, 0.41), (1, 0.02)]);
assert_eq!(response.provider, "llamacpp");
assert_eq!(response.model.as_deref(), Some("bge-reranker-v2-m3"));
assert_eq!(response.usage.input_tokens, Some(37));
assert_eq!(response.usage.total_tokens, Some(37));
assert!(
response
.results
.iter()
.all(|result| result.document.is_none())
);
}
#[test]
fn a_rerank_request_is_the_jina_shape() {
let encoded = OpenAIConfig::with_key(&LLAMACPP, "")
.rerank("bge-reranker-v2-m3")
.with_top_n(2)
.encode(
crate::operation::RerankRequest {
query: "q".to_owned(),
documents: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
},
Mode::Unary,
)
.expect("encodes");
let request = &encoded.request;
assert_eq!(request.uri().to_string(), "http://localhost:8080/v1/rerank");
let body = json_body(&encoded.request);
assert_eq!(
body,
serde_json::json!({
"query": "q",
"documents": ["a", "b", "c"],
"model": "bge-reranker-v2-m3",
"top_n": 2,
})
);
assert_eq!(
OpenAIConfig::with_key(&LLAMACPP, "")
.rerank("r")
.describe()
.capabilities
.max_documents,
1024
);
}
#[cfg(feature = "image")]
#[tokio::test]
async fn the_hyperbolic_image_body_and_reply_differ_from_openais() {
use crate::providers::openai::wire::HYPERBOLIC;
let request = || crate::image_generation::ImageGenerationRequest {
prompt: "a cat".to_owned(),
width: 1024,
height: 768,
additional_params: None,
};
let encoded = OpenAIConfig::with_key(&HYPERBOLIC, "hb")
.image_generation("SDXL1.0-base")
.encode(request(), Mode::Unary)
.expect("the request encodes");
let http_request = &encoded.request;
assert_eq!(
http_request.uri().to_string(),
"https://api.hyperbolic.xyz/v1/image/generation"
);
let body = json_body(&encoded.request);
assert_eq!(body["model_name"], "SDXL1.0-base");
assert_eq!(body["width"], 1024);
assert_eq!(body["height"], 768);
assert!(
body.get("model").is_none(),
"the key is `model_name`: {body}"
);
assert!(body.get("size").is_none(), "the size is two fields: {body}");
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&HYPERBOLIC, "hb").image_generation("SDXL1.0-base"),
RecordingHttpClient::new(r#"{"images":[{"image":"aGk="}]}"#),
)
.call(request())
.await
.expect("Hyperbolic's reply shape decodes");
assert_eq!(response.image, b"hi");
assert_eq!(response.provider, "hyperbolic");
}
#[cfg(feature = "image")]
#[test]
fn the_huggingface_image_body_is_the_routers_own_shape() {
use crate::providers::openai::wire::HUGGINGFACE;
let encoded = OpenAIConfig::with_key(&HUGGINGFACE, "hf")
.image_generation("stabilityai/stable-diffusion-3-medium-diffusers")
.encode(
crate::image_generation::ImageGenerationRequest {
prompt: "a cat".to_owned(),
width: 1024,
height: 768,
additional_params: None,
},
Mode::Unary,
)
.expect("the request encodes");
let http_request = &encoded.request;
assert_eq!(
http_request.uri().to_string(),
"https://router.huggingface.co/stabilityai/stable-diffusion-3-medium-diffusers"
);
let body = json_body(&encoded.request);
assert_eq!(body["inputs"], "a cat");
assert_eq!(body["parameters"]["width"], 1024);
assert_eq!(body["parameters"]["height"], 768);
assert!(
body.get("prompt").is_none(),
"the prompt is `inputs`: {body}"
);
assert!(body.get("model").is_none(), "the model is the path: {body}");
assert!(body.get("size").is_none(), "the size is nested: {body}");
assert!(
body.get("width").is_none(),
"the size is under `parameters`: {body}"
);
}
#[cfg(feature = "image")]
#[tokio::test]
async fn the_huggingface_image_reply_is_the_image_bytes() {
use crate::providers::openai::wire::HUGGINGFACE;
let png = b"\x89PNG\r\n\x1a\n\xff\xd8not-json";
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&HUGGINGFACE, "hf").image_generation("black-forest-labs/FLUX.1-dev"),
RecordingHttpClient::new(&png[..]),
)
.call(crate::image_generation::ImageGenerationRequest {
prompt: "a cat".to_owned(),
width: 1024,
height: 768,
additional_params: None,
})
.await
.expect("raw image bytes decode");
assert_eq!(response.image, png);
assert_eq!(response.provider, "huggingface");
assert!(response.raw.is_null(), "{}", response.raw);
}
#[cfg(feature = "audio")]
#[tokio::test]
async fn the_hyperbolic_speech_body_and_reply_differ_from_openais() {
use crate::providers::openai::wire::HYPERBOLIC;
let request = || crate::audio_generation::AudioGenerationRequest {
text: "hello".to_owned(),
voice: "EN-US".to_owned(),
speed: 1.0,
additional_params: None,
};
let encoded = OpenAIConfig::with_key(&HYPERBOLIC, "hb")
.audio_generation("EN")
.encode(request(), Mode::Unary)
.expect("the request encodes");
let http_request = &encoded.request;
assert_eq!(
http_request.uri().to_string(),
"https://api.hyperbolic.xyz/v1/audio/generation"
);
let body = json_body(&encoded.request);
assert_eq!(body["language"], "EN");
assert_eq!(body["speaker"], "EN-US");
assert_eq!(body["text"], "hello");
assert_eq!(body["speed"], 1.0);
assert!(body.get("model").is_none(), "{body}");
assert!(body.get("voice").is_none(), "{body}");
let response = crate::driver::Model::new(
OpenAIConfig::with_key(&HYPERBOLIC, "hb").audio_generation("EN"),
RecordingHttpClient::new(r#"{"audio":"aGk="}"#),
)
.call(request())
.await
.expect("Hyperbolic's base64 envelope decodes");
assert_eq!(response.audio, b"hi");
assert_eq!(response.provider, "hyperbolic");
let openai = crate::driver::Model::new(
OpenAIConfig::new("sk").audio_generation("tts-1"),
RecordingHttpClient::new(&b"ID3\x04raw-mp3"[..]),
)
.call(request())
.await
.expect("raw bytes decode");
assert_eq!(openai.audio, b"ID3\x04raw-mp3");
}
#[test]
fn a_dialects_width_table_supplies_the_default_and_suppresses_the_field() {
let unasked = OpenAIConfig::with_key(&DOUBLEWORD, "k").embedding(QWEN3_EMBEDDING_8B, None);
assert_eq!(
unasked.describe().capabilities.ndims,
4_096,
"the dialect's table is the only place this model's width is written down"
);
for wire in [
&unasked,
&OpenAIConfig::with_key(&DOUBLEWORD, "k").embedding(QWEN3_EMBEDDING_8B, Some(4_096)),
] {
let encoded = wire
.encode(documents(), Mode::Unary)
.expect("the native width encodes");
assert!(
json_body(&encoded.request).get("dimensions").is_none(),
"{}",
json_body(&encoded.request)
);
}
let encoded = OpenAIConfig::with_key(&DOUBLEWORD, "k")
.embedding(QWEN3_EMBEDDING_8B, Some(512))
.encode(documents(), Mode::Unary)
.expect("a width inside the documented range encodes");
assert_eq!(
json_body(&encoded.request)["dimensions"],
serde_json::json!(512)
);
let mistral = OpenAIConfig::with_key(&MISTRAL, "k").embedding(MISTRAL_EMBED, None);
assert_eq!(mistral.describe().capabilities.ndims, 1_024);
let encoded = mistral
.encode(documents(), Mode::Unary)
.expect("the native width encodes");
assert!(
json_body(&encoded.request)
.get("output_dimension")
.is_none(),
"{}",
json_body(&encoded.request)
);
}
#[test]
fn an_unhonourable_width_is_refused_before_the_request_is_built() {
fn refusal(dialect: &Dialect, model: &str, ndims: usize) -> String {
let error: ProviderError = OpenAIConfig::with_key(dialect, "k")
.embedding(model, Some(ndims))
.encode(documents(), Mode::Unary)
.expect_err("a width the dialect cannot honour must not reach the wire")
.into();
assert_eq!(error.kind(), ErrorKind::Request, "{error}");
error.to_string()
}
assert_eq!(
refusal(&DOUBLEWORD, QWEN3_EMBEDDING_8B, 0),
"RequestError: doubleword embeddings require `dimensions` to be greater than zero"
);
for over_or_under in [8_192, 31] {
assert_eq!(
refusal(&DOUBLEWORD, QWEN3_EMBEDDING_8B, over_or_under),
"RequestError: doubleword embeddings require `dimensions` to be between 32 and 4096"
);
}
assert_eq!(
refusal(&MISTRAL, CODESTRAL_EMBED, 3_073),
"RequestError: mistral embeddings require `output_dimension` to be at most 3072 for \
Codestral Embed"
);
assert_eq!(
refusal(&MISTRAL, MISTRAL_EMBED, 512),
"RequestError: mistral embeddings do not support the `output_dimension` parameter",
"a fixed-width model has no width to request"
);
let unknown =
OpenAIConfig::with_key(&DOUBLEWORD, "k").embedding("Qwen/Qwen4-Unreleased", Some(8_192));
assert_eq!(unknown.describe().capabilities.ndims, 8_192);
let encoded = unknown
.encode(documents(), Mode::Unary)
.expect("an undocumented model's width is not rig's to refuse");
assert_eq!(
json_body(&encoded.request)["dimensions"],
serde_json::json!(8_192)
);
}