use serde_json::{Value, json};
use super::*;
use crate::completion::{OnUnsupported, ProviderOptions, Reasoning};
use crate::providers::openai::completion::{GPT_4_1_MINI, GPT_6_SOL};
use crate::providers::openai::wire::{OPENAI, OpenAIConfig};
use crate::providers::openrouter::extension::OpenRouterExt;
use crate::test_utils::TraceCapture;
use crate::test_utils::provider_extensions::{
assert_no_reserved_leaf, body_with, encoded_body, recorded_reply, reply_of, request_with,
};
use crate::wire::Mode;
fn chat_wire(model: &str) -> crate::providers::openai::wire::Chat {
OpenAIConfig::with_key(&OPENAI, "key").chat(model)
}
fn body(chat: ChatOptions) -> Value {
body_with::<OpenAiExt, _>(&chat_wire(GPT_4_1_MINI), &OpenAiOptions::new().chat(chat))
}
#[test]
fn logit_bias_lands_under_logit_bias() {
let body = body(ChatOptions::new().logit_bias(50256, -100).logit_bias(13, 5));
assert_eq!(body["logit_bias"], json!({"13": 5, "50256": -100}));
}
#[test]
fn prediction_lands_as_content_prediction() {
let body = body(ChatOptions::new().prediction("fn main() {}"));
assert_eq!(
body["prediction"],
json!({"type": "content", "content": "fn main() {}"})
);
}
#[test]
fn logprobs_lands_at_top_level() {
assert_eq!(body(ChatOptions::new().logprobs(true))["logprobs"], true);
}
#[test]
fn top_logprobs_lands_at_top_level() {
assert_eq!(body(ChatOptions::new().top_logprobs(5))["top_logprobs"], 5);
}
#[test]
fn frequency_penalty_lands_at_top_level() {
assert_eq!(
body(ChatOptions::new().frequency_penalty(0.5))["frequency_penalty"],
0.5
);
}
#[test]
fn presence_penalty_lands_at_top_level() {
assert_eq!(
body(ChatOptions::new().presence_penalty(-0.5))["presence_penalty"],
-0.5
);
}
#[test]
fn modalities_land_at_top_level() {
let body = body(ChatOptions::new().modalities([Modality::Text, Modality::Audio]));
assert_eq!(body["modalities"], json!(["text", "audio"]));
}
#[test]
fn audio_lands_at_top_level() {
let body = body(ChatOptions::new().audio("alloy", AudioFormat::Pcm16));
assert_eq!(body["audio"], json!({"voice": "alloy", "format": "pcm16"}));
}
#[test]
fn web_search_options_land_at_top_level() {
let options = WebSearchOptions::new()
.search_context_size(SearchContextSize::Low)
.user_location(
ApproximateLocation::new()
.city("Paris")
.country("FR")
.region("Ile-de-France")
.timezone("Europe/Paris"),
);
let body = body(ChatOptions::new().web_search_options(options));
assert_eq!(
body["web_search_options"],
json!({
"search_context_size": "low",
"user_location": {
"type": "approximate",
"approximate": {
"city": "Paris",
"country": "FR",
"region": "Ile-de-France",
"timezone": "Europe/Paris"
}
}
})
);
}
#[test]
fn the_chat_section_is_not_sent_on_responses() {
let wire = OpenAIConfig::with_key(&OPENAI, "key").responses(GPT_4_1_MINI);
let options = OpenAiOptions::new().chat(ChatOptions::new().logprobs(true));
let capture = TraceCapture::default();
let body = tracing::subscriber::with_default(capture.subscriber(), || {
body_with::<OpenAiExt, _>(&wire, &options)
});
assert!(body.get("logprobs").is_none(), "{body}");
assert!(capture.warnings().is_empty(), "{:?}", capture.warnings());
}
#[test]
fn raw_additional_params_beat_typed_options() {
let request = request_with::<OpenAiExt>(
&OpenAiOptions::new().chat(ChatOptions::new().frequency_penalty(0.5)),
)
.additional_params(json!({"frequency_penalty": 1.0}));
let body = encoded_body(&chat_wire(GPT_4_1_MINI), request, Mode::Unary)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(body["frequency_penalty"], 1.0);
}
#[test]
fn typed_logprobs_on_gpt_6_while_reasoning_fail_the_body_check() {
let options = OpenAiOptions::new().chat(ChatOptions::new().logprobs(true));
let strict = request_with::<OpenAiExt>(&options).reasoning(crate::completion::Effort::Medium);
let error = encoded_body(&chat_wire(GPT_6_SOL), strict, Mode::Unary)
.err()
.map(|error| error.to_string())
.unwrap_or_default();
assert!(error.contains("`logprobs`"), "{error}");
let lenient = request_with::<OpenAiExt>(&options).on_unsupported(OnUnsupported::Ignore);
let body = encoded_body(&chat_wire(GPT_6_SOL), lenient, Mode::Unary)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
body.get("logprobs"),
None,
"a typed field is left out: {body}"
);
let unset = encoded_body(
&chat_wire(GPT_6_SOL),
request_with::<OpenAiExt>(&options),
Mode::Unary,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(unset["logprobs"], true, "no option set: sent as built");
let off = request_with::<OpenAiExt>(&options)
.reasoning(Reasoning::Off)
.on_unsupported(OnUnsupported::Error);
let body = encoded_body(&chat_wire(GPT_6_SOL), off, Mode::Unary)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(body["logprobs"], true);
}
#[test]
fn no_option_writes_a_leaf_the_request_or_a_mapped_option_owns() {
let chat = ChatOptions::new()
.logit_bias(1, 1)
.prediction("p")
.logprobs(true)
.top_logprobs(1)
.frequency_penalty(0.1)
.presence_penalty(0.1)
.modalities([Modality::Text])
.audio("alloy", AudioFormat::Mp3)
.web_search_options(WebSearchOptions::new().search_context_size(SearchContextSize::High));
assert_no_reserved_leaf::<OpenAiExt, _>(
&[
chat_wire(GPT_4_1_MINI),
chat_wire("gpt-5.6"),
chat_wire(GPT_6_SOL),
],
&OpenAiOptions::new().chat(chat),
);
}
#[tokio::test]
async fn chat_extras_from_a_unary_recording() {
let reply = reply_of(
chat_wire("gpt-5-mini"),
recorded_reply("openai", "corpus_matrix_chat/output_tool_thinking", 0),
)
.await;
let extras = reply
.extras::<OpenAiExt>()
.unwrap_or_else(|| panic!("an OpenAI reply"))
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(extras.service_tier.as_deref(), Some("default"));
assert_eq!(extras.system_fingerprint, None);
let completion = extras.completion_tokens_details.unwrap_or_default();
assert_eq!(completion.reasoning_tokens, Some(64));
assert_eq!(completion.accepted_prediction_tokens, Some(0));
let prompt = extras.prompt_tokens_details.unwrap_or_default();
assert_eq!(prompt.cached_tokens, Some(0));
assert_eq!(prompt.audio_tokens, Some(0));
assert!(reply.extras::<OpenRouterExt>().is_none());
}
#[test]
fn the_shared_section_serializes_under_the_shared_key() {
let options = OpenAiOptions::default().shared(
OpenAiShared::default()
.store(false)
.metadata("tenant", "acme")
.prompt_cache_key("k1")
.safety_identifier("u-1"),
);
let entry = ProviderOptions::new()
.with::<OpenAiExt>(&options)
.expect("the options are sections");
assert_eq!(
serde_json::to_value(entry.get::<OpenAiExt>()).expect("the sections serialize"),
json!({"*": {
"store": false,
"metadata": {"tenant": "acme"},
"prompt_cache_key": "k1",
"safety_identifier": "u-1"
}})
);
}
#[test]
fn empty_options_leave_no_entry() {
let entry = ProviderOptions::new()
.with::<OpenAiExt>(&OpenAiOptions::default())
.expect("the options are sections");
assert!(entry.is_empty());
}
#[test]
fn a_text_reasoning_reads_as_no_reasoning() {
let api = Api::from_static("openai.responses");
let extras = OpenAiExtras::from_reply(
&api,
&json!({"reasoning": "thought about it", "service_tier": "flex", "output": []}),
)
.expect("the view reads");
assert_eq!(
extras,
OpenAiExtras {
service_tier: Some("flex".to_owned()),
..OpenAiExtras::default()
}
);
assert_eq!(
OpenAiExtras::from_reply(&api, &json!({})).expect("the view reads"),
OpenAiExtras::default()
);
}
#[tokio::test]
async fn chat_extras_read_alike_from_a_recorded_stream() {
use crate::test_utils::provider_extensions::{recorded_stream, streamed_reply_of};
let read = |reply: crate::completion::CompletionResponse| {
reply
.extras::<OpenAiExt>()
.unwrap_or_else(|| panic!("an OpenAI reply"))
.unwrap_or_else(|error| panic!("{error}"))
};
let streamed = read(
streamed_reply_of(
chat_wire("gpt-4.1-nano"),
recorded_stream(
"openai",
"raw_stream_capture_matrix/chat_stream_raw_round_trips_typed",
0,
),
)
.await,
);
let unary = read(
reply_of(
chat_wire("gpt-4.1-nano"),
recorded_reply("openai", "raw_capture_matrix/chat_raw_round_trips_typed", 0),
)
.await,
);
assert_eq!(streamed, unary);
assert_eq!(streamed.service_tier.as_deref(), Some("default"));
}
#[test]
fn field_setters_equal_the_section_form() {
let shared = |section: OpenAiShared| OpenAiOptions::new().shared(section);
let responses = |section: OpenAiResponsesOptions| OpenAiOptions::new().responses(section);
let pairs = [
(
OpenAiOptions::new().store(false),
shared(OpenAiShared::default().store(false)),
),
(
OpenAiOptions::new().metadata("k", "v"),
shared(OpenAiShared::default().metadata("k", "v")),
),
(
OpenAiOptions::new().prompt_cache_key("key"),
shared(OpenAiShared::default().prompt_cache_key("key")),
),
(
OpenAiOptions::new().safety_identifier("user"),
shared(OpenAiShared::default().safety_identifier("user")),
),
(
OpenAiOptions::new().reasoning_summary(ReasoningSummary::Auto),
responses(OpenAiResponsesOptions::default().reasoning_summary(ReasoningSummary::Auto)),
),
(
OpenAiOptions::new().include([Include::ReasoningEncryptedContent]),
responses(
OpenAiResponsesOptions::default().include([Include::ReasoningEncryptedContent]),
),
),
(
OpenAiOptions::new().conversation("conv"),
responses(OpenAiResponsesOptions::default().conversation("conv")),
),
(
OpenAiOptions::new().truncation(Truncation::Auto),
responses(OpenAiResponsesOptions::default().truncation(Truncation::Auto)),
),
(
OpenAiOptions::new().background(true),
responses(OpenAiResponsesOptions::default().background(true)),
),
(
OpenAiOptions::new().max_tool_calls(3),
responses(OpenAiResponsesOptions::default().max_tool_calls(3)),
),
(
OpenAiOptions::new()
.store(false)
.metadata("k", "v")
.reasoning_summary(ReasoningSummary::Detailed)
.background(true),
OpenAiOptions::new()
.shared(OpenAiShared::default().store(false).metadata("k", "v"))
.responses(
OpenAiResponsesOptions::default()
.reasoning_summary(ReasoningSummary::Detailed)
.background(true),
),
),
];
for (short, long) in pairs {
assert_eq!(short, long);
let short = serde_json::to_value(&short).expect("options serialize");
assert_eq!(
short,
serde_json::to_value(&long).expect("options serialize")
);
assert_ne!(
short,
serde_json::to_value(OpenAiOptions::new()).expect("options serialize")
);
}
}