use serde_json::{Value, json};
use super::*;
fn request<'a>(model: &'a str, provider_options: &'a Value) -> Request<'a> {
Request {
model,
language: None,
prompt: None,
temperature: None,
format: None,
speaker_names: None,
speaker_references: None,
provider_options,
}
}
fn ruby_wav() -> Attachment {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/ruby.wav");
Attachment::from_bytes(std::fs::read(path).unwrap(), "ruby.wav", None)
}
#[test]
fn gemini_passes_the_requested_format_and_temperature_through() {
let options = json!({});
let r = Request {
format: Some("application/json"),
temperature: Some(0.2),
..request("gemini-2.5-flash", &options)
};
let payload = gemini_payload(&ruby_wav(), &r).unwrap();
assert_eq!(
payload["generationConfig"],
json!({ "responseMimeType": "application/json", "temperature": 0.2 })
);
}
#[test]
fn gemini_refuses_an_attachment_that_is_not_audio() {
let notes = Attachment::from_bytes(b"not audio".to_vec(), "notes.txt", None);
let options = json!({});
let error = gemini_payload(¬es, &request("gemini-2.5-flash", &options)).unwrap_err();
assert!(
matches!(error, Error::UnsupportedAttachment(_)),
"{error:?}"
);
}
fn parse(body: &Value) -> Result<Transcription> {
parse_gemini(body, 200, "gemini-2.5-flash")
}
fn blocked_usage() -> Value {
json!({ "promptTokenCount": 133, "totalTokenCount": 133,
"promptTokensDetails": [{ "modality": "TEXT", "tokenCount": 14 },
{ "modality": "AUDIO", "tokenCount": 119 }] })
}
fn content_filter_message(result: Result<Transcription>) -> (String, Option<ErrorResponse>) {
match result {
Err(Error::ContentFilter(message, response)) => (message, response),
other => panic!("expected ContentFilterError, got {other:?}"),
}
}
#[test]
fn gemini_leaves_the_text_nil_when_the_response_is_not_a_json_object() {
assert_eq!(parse(&json!("not a hash")).unwrap().text, None);
}
#[test]
fn gemini_leaves_the_text_nil_when_the_candidate_carries_no_text_parts() {
let inline = json!({ "candidates": [{ "content": { "parts": [{ "inlineData": {} }] } }] });
assert_eq!(parse(&inline).unwrap().text, None);
let empty = json!({ "candidates": [{ "content": {} }] });
assert_eq!(parse(&empty).unwrap().text, None);
}
#[test]
fn gemini_leaves_the_token_counts_nil_when_the_response_carries_no_usage() {
let data = json!({ "candidates": [{ "content": { "parts": [{ "text": "hi" }] } }] });
let t = parse(&data).unwrap();
assert_eq!(t.tokens().input, None);
assert_eq!(t.tokens().output, None);
}
#[test]
fn gemini_raises_with_the_block_reason_when_gemini_blocks_the_audio_prompt() {
let body = json!({ "promptFeedback": { "blockReason": "SAFETY" }, "usageMetadata": blocked_usage(),
"modelVersion": "gemini-3.8-flash", "responseId": "b2WsavzrBt3ZxN8PmJyl6Aw" });
let (message, response) = content_filter_message(parse(&body));
assert_eq!(message, "Gemini blocked the transcription: SAFETY");
let response = response.expect("the error carries the response");
assert_eq!(serde_json::from_str::<Value>(&response.body).unwrap(), body);
}
#[test]
fn gemini_raises_with_the_finish_reason_when_gemini_blocks_the_transcript() {
let body = json!({
"candidates": [{
"finishReason": "SAFETY", "index": 0,
"safetyRatings": [
{ "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE" },
{ "category": "HARM_CATEGORY_HARASSMENT", "probability": "MEDIUM", "blocked": true }
]
}],
"usageMetadata": blocked_usage(), "modelVersion": "gemini-3.8-flash"
});
let (message, _) = content_filter_message(parse(&body));
assert_eq!(message, "Gemini blocked the transcription: SAFETY");
}
#[test]
fn gemini_raises_when_gemini_returns_no_candidates() {
let body = json!({ "usageMetadata": blocked_usage(), "modelVersion": "gemini-3.8-flash" });
let (message, _) = content_filter_message(parse(&body));
assert_eq!(message, "Gemini blocked the transcription");
}
#[test]
fn gemini_returns_an_empty_transcript_for_silent_audio() {
let body = json!({ "candidates": [{ "content": { "role": "model", "parts": [{ "text": "" }] },
"finishReason": "STOP", "index": 0 }],
"usageMetadata": blocked_usage(), "modelVersion": "gemini-3.8-flash" });
assert_eq!(parse(&body).unwrap().text.as_deref(), Some(""));
}
#[test]
fn gemini_keeps_the_text_a_blocked_transcript_already_carries() {
let body = json!({ "candidates": [{ "content": { "role": "model", "parts": [{ "text": "Guten Tag" }] },
"finishReason": "SAFETY", "index": 0 }],
"usageMetadata": blocked_usage(), "modelVersion": "gemini-3.8-flash" });
assert_eq!(parse(&body).unwrap().text.as_deref(), Some("Guten Tag"));
}
#[test]
fn interactions_preserve_speaker_only_annotations_without_fabricating_timing() {
let data = json!({ "status": "completed", "steps": [{ "type": "model_output", "content": [
{ "type": "text", "text": "Hello", "annotations": [
{ "type": "word_info", "text": "Hello", "speaker": "spk:0" }
] }
] }], "usage": { "total_input_tokens": 14, "total_output_tokens": 0 } });
let t = parse_interactions(&data, "gemini-3.5-transcribe").unwrap();
assert_eq!(t.text.as_deref(), Some("Hello"));
assert_eq!(
t.words,
Some(vec![json!({ "word": "Hello", "speaker": "spk:0" })])
);
assert_eq!((t.tokens().input, t.tokens().output), (Some(14), Some(0)));
assert_eq!(t.duration, None);
}
#[test]
fn interactions_leave_combining_custom_vocabulary_with_diarization_to_gemini() {
let options = json!({});
let names = vec!["Alice".to_string()];
let r = Request {
speaker_names: Some(&names),
prompt: Some("RubyLLM"),
..request("gemini-3.5-transcribe", &options)
};
let payload = interactions_payload(&ruby_wav(), &r).unwrap();
let config = &payload["generation_config"]["transcription_config"];
assert_eq!(config["custom_vocabulary"], json!(["RubyLLM"]));
assert_eq!(
config["mode"],
json!({ "type": "verbatim", "diarization_mode": "speaker" })
);
}
#[test]
fn interactions_reject_unknown_granularities() {
let error = Family::Interactions
.render_options(Some(&["segment"]), None, false)
.unwrap_err();
assert!(
matches!(&error, Error::Argument(m) if m.contains("must be word")),
"{error:?}"
);
}
#[test]
fn chat_completions_keeps_the_format_the_caller_asked_for_on_a_diarize_model() {
let options = json!({});
let r = Request {
format: Some("json"),
..request("gpt-4o-transcribe-diarize", &options)
};
let payload = multipart_payload(Family::OpenAI, &r).unwrap();
assert_eq!(payload["response_format"], json!("json"));
}
#[test]
fn chat_completions_maps_format_to_response_format() {
let options = json!({});
let r = Request {
format: Some("verbose_json"),
..request("whisper-1", &options)
};
let payload = multipart_payload(Family::OpenAI, &r).unwrap();
assert_eq!(payload["response_format"], json!("verbose_json"));
}
#[test]
fn chat_completions_maps_speaker_names_and_references_to_the_known_speaker_fields() {
let options = json!({});
let names = vec!["Alice".to_string()];
let references = vec![ruby_wav()];
let r = Request {
speaker_names: Some(&names),
speaker_references: Some(&references),
..request("gpt-4o-transcribe-diarize", &options)
};
let payload = multipart_payload(Family::OpenAI, &r).unwrap();
assert_eq!(payload["known_speaker_names"], json!(["Alice"]));
let refs = payload["known_speaker_references"].as_array().unwrap();
assert_eq!(refs.len(), 1);
assert!(
refs[0]
.as_str()
.unwrap()
.starts_with("data:audio/wav;base64,")
);
}
#[test]
fn chat_completions_merges_provider_options_over_rendered_defaults() {
let options =
json!({ "chunking_strategy": { "type": "server_vad" }, "response_format": "json" });
let payload = multipart_payload(
Family::OpenAI,
&request("gpt-4o-transcribe-diarize", &options),
)
.unwrap();
assert_eq!(
payload["chunking_strategy"],
json!({ "type": "server_vad" })
);
assert_eq!(payload["response_format"], json!("json"));
}