fix: align Responses routing and model permissions

This commit is contained in:
zhefox
2026-08-03 18:48:01 +08:00
parent 0318808db9
commit 669f4bddc5
14 changed files with 728 additions and 396 deletions
@@ -1,7 +1,5 @@
use serde_json::Value;
use crate::formats::shared::model_directives::ReasoningEffort;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenAiReasoningViolationKind {
InvalidType,
@@ -50,17 +48,15 @@ pub(crate) fn validate_openai_reasoning_request_with_source_model(
source_model,
body,
None,
None,
)
}
pub(crate) fn validate_openai_reasoning_request_with_model_profile(
source_api_format: &str,
provider_api_format: &str,
_provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
model_card_reasoning_efforts: Option<&[String]>,
supports_reasoning_mode: Option<bool>,
) -> Result<(), OpenAiReasoningContractViolation> {
let Some(object) = body.as_object() else {
@@ -102,14 +98,7 @@ pub(crate) fn validate_openai_reasoning_request_with_model_profile(
_ => None,
};
if let Some(value) = effort.filter(|value| !value.is_null()) {
validate_reasoning_effort(
value,
source_api_format.as_str(),
provider_api_format,
provider_model,
source_model,
model_card_reasoning_efforts,
)?;
validate_reasoning_effort(value, source_api_format.as_str())?;
}
if source_api_format != "openai:search" {
@@ -139,10 +128,6 @@ pub(crate) fn validate_openai_reasoning_request_with_model_profile(
fn validate_reasoning_effort(
value: &Value,
source_api_format: &str,
provider_api_format: &str,
provider_model: &str,
source_model: &str,
model_card_reasoning_efforts: Option<&[String]>,
) -> Result<(), OpenAiReasoningContractViolation> {
let field = if source_api_format == "openai:chat" {
"reasoning_effort"
@@ -165,48 +150,7 @@ fn validate_reasoning_effort(
reason: "reasoning effort must not be empty".to_string(),
});
}
if raw.trim() == "ultra" {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "ultra is a Codex client preset, not an OpenAI wire effort".to_string(),
});
}
if let Some(supported_efforts) =
model_card_reasoning_efforts.filter(|values| !values.is_empty())
{
if supported_efforts
.iter()
.any(|effort| effort == raw.trim() || (raw.trim() == "max" && effort == "ultra"))
{
return Ok(());
}
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::UnsupportedForModel,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "provider model card does not support the requested reasoning effort"
.to_string(),
});
}
let Some(effort) = ReasoningEffort::parse(raw) else {
return Ok(());
};
if crate::reasoning_effort_supported_for_model(
provider_api_format,
provider_model,
source_model,
effort,
) {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::UnsupportedForModel,
field: field.to_string(),
value: Some(raw.to_string()),
reason: "provider model does not support the requested reasoning effort".to_string(),
})
Ok(())
}
fn validate_reasoning_mode(
@@ -303,69 +247,32 @@ mod tests {
use super::{validate_openai_reasoning_request, OpenAiReasoningViolationKind};
#[test]
fn mapped_model_is_authoritative_for_openai_reasoning_effort() {
let alias = json!({
"model": "deployment-alias",
"reasoning": {"effort": "max"}
});
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&alias,
)
.expect("GPT-5.6 should accept max");
let error = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.4",
&alias,
)
.expect_err("GPT-5.4 should reject max");
assert_eq!(
error.kind,
OpenAiReasoningViolationKind::UnsupportedForModel
);
}
#[test]
fn gpt_5_6_rejects_known_unsupported_effort_and_preserves_custom_effort() {
let unsupported = json!({
"model": "gpt-5.6-terra",
"reasoning_effort": "minimal"
});
let error = validate_openai_reasoning_request(
"openai:chat",
"openai:chat",
"gpt-5.6-terra",
&unsupported,
)
.expect_err("known unsupported effort should be rejected");
assert_eq!(
error.kind,
OpenAiReasoningViolationKind::UnsupportedForModel
);
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-terra",
&json!({
"model": "gpt-5.6-terra",
"reasoning": {"effort": "future"}
}),
)
.expect("model-advertised custom effort should pass through");
let ultra = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-terra",
&json!({"reasoning": {"effort": "ultra"}}),
)
.expect_err("Codex local ultra preset should not enter the OpenAI wire contract");
assert_eq!(ultra.kind, OpenAiReasoningViolationKind::InvalidEnum);
fn reasoning_effort_support_is_deferred_to_upstream() {
for (format, body, provider_model) in [
(
"openai:responses",
json!({"reasoning": {"effort": "max"}}),
"gpt-5.4",
),
(
"openai:chat",
json!({"reasoning_effort": "minimal"}),
"gpt-5.6-terra",
),
(
"openai:responses",
json!({"reasoning": {"effort": "future"}}),
"gpt-5.6-terra",
),
(
"openai:responses",
json!({"reasoning": {"effort": "ultra"}}),
"gpt-5.6-terra",
),
] {
validate_openai_reasoning_request(format, format, provider_model, &body)
.expect("well-formed reasoning efforts should be validated by the upstream");
}
let empty = validate_openai_reasoning_request(
"openai:responses",
@@ -76,11 +76,6 @@ pub fn finalize_openai_provider_request_with_codex_model_capabilities(
)
}
}
super::responses::codex::normalize_codex_openai_reasoning_wire_effort(
body,
finalization.provider_type,
finalization.provider_api_format,
);
super::responses::codex::apply_openai_responses_compact_special_body_edits(
body,
finalization.provider_api_format,
@@ -108,12 +103,11 @@ pub fn finalize_openai_provider_request_with_codex_model_capabilities(
finalization.provider_api_format,
)
.map_err(OpenAiProviderRequestContractViolation::CodexCompact)?;
validate_openai_provider_request_contract_with_codex_model_capabilities(
validate_final_openai_provider_request_contract(
finalization.provider_api_format,
provider_model,
finalization.source_model,
body,
model_capabilities,
)
}
@@ -123,21 +117,19 @@ pub fn validate_openai_provider_request_contract(
source_model: &str,
body: &Value,
) -> Result<(), OpenAiProviderRequestContractViolation> {
validate_openai_provider_request_contract_with_codex_model_capabilities(
validate_final_openai_provider_request_contract(
provider_api_format,
provider_model,
source_model,
body,
None,
)
}
fn validate_openai_provider_request_contract_with_codex_model_capabilities(
fn validate_final_openai_provider_request_contract(
provider_api_format: &str,
provider_model: &str,
source_model: &str,
body: &Value,
model_capabilities: Option<&super::responses::codex::CodexResponsesModelCapabilities>,
) -> Result<(), OpenAiProviderRequestContractViolation> {
super::responses::request::validate_openai_responses_request_contract(
body,
@@ -157,7 +149,6 @@ fn validate_openai_provider_request_contract_with_codex_model_capabilities(
provider_model,
source_model,
body,
model_capabilities.map(|capabilities| capabilities.supported_reasoning_efforts.as_slice()),
None,
)
.map_err(OpenAiProviderRequestContractViolation::Reasoning)
@@ -224,14 +215,14 @@ mod tests {
}
#[test]
fn codex_finalization_enforces_model_card_reasoning_efforts() {
fn codex_finalization_defers_explicit_reasoning_effort_to_upstream() {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "minimal"}
});
let error = finalize_openai_provider_request(
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
@@ -244,17 +235,35 @@ mod tests {
require_body_stream_field: true,
},
)
.expect_err("GPT-5.6 Codex model card should reject minimal");
.expect("explicit reasoning effort support should be validated by the upstream");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(
super::OpenAiReasoningContractViolation {
kind: crate::formats::openai::reasoning::OpenAiReasoningViolationKind::UnsupportedForModel,
..
}
)
));
assert_eq!(body["reasoning"]["effort"], "minimal");
}
#[test]
fn codex_finalization_accepts_none_for_gpt_5_4_mini() {
let mut body = json!({
"model": "gpt-5.4-mini",
"input": [{"role": "user", "content": "hello"}],
"reasoning": {"effort": "none"}
});
finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.4-mini",
source_model: "gpt-5.4-mini",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect("GPT-5.4 mini should accept the official none reasoning effort");
assert_eq!(body["reasoning"]["effort"], "none");
}
#[test]
@@ -364,17 +373,7 @@ mod tests {
}
#[test]
fn codex_ultra_preset_uses_max_for_every_codex_model_on_the_wire() {
let mut sol = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "ultra"}
});
let mut luna = json!({
"model": "gpt-5.6-luna",
"input": [],
"reasoning": {"effort": "ultra"}
});
fn codex_finalization_preserves_explicit_reasoning_effort() {
let finalization_for = |model| OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
@@ -386,17 +385,20 @@ mod tests {
require_body_stream_field: true,
};
finalize_openai_provider_request(&mut sol, finalization_for("gpt-5.6-sol"))
.expect("Sol ultra preset should map to the OpenAI wire contract");
assert_eq!(sol["reasoning"]["effort"], "max");
finalize_openai_provider_request(&mut luna, finalization_for("gpt-5.6-luna"))
.expect("Luna ultra preset should map to the OpenAI wire contract");
assert_eq!(luna["reasoning"]["effort"], "max");
for (model, effort) in [("gpt-5.6-sol", "max"), ("gpt-5.6-luna", "ultra")] {
let mut body = json!({
"model": model,
"input": [],
"reasoning": {"effort": effort}
});
finalize_openai_provider_request(&mut body, finalization_for(model))
.expect("explicit effort support should be validated by the upstream");
assert_eq!(body["reasoning"]["effort"], effort);
}
}
#[test]
fn dynamic_codex_card_controls_default_effort_and_keeps_mode_model_specific() {
fn dynamic_codex_card_preserves_default_effort_and_keeps_mode_model_specific() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
@@ -425,8 +427,8 @@ mod tests {
finalization,
Some(&capabilities),
)
.expect("card default ultra should use the max wire effort");
assert_eq!(default_body["reasoning"]["effort"], "max");
.expect("card default effort should be preserved for the upstream");
assert_eq!(default_body["reasoning"]["effort"], "ultra");
let mut mode_body = json!({
"model": "gpt-5.7-sol",
@@ -458,12 +460,12 @@ mod tests {
finalization,
Some(&ultra_only),
)
.expect("Codex maps the Ultra preset to max without card-list wire validation");
assert_eq!(ultra_only_body["reasoning"]["effort"], "max");
.expect("explicit effort support should be validated by the upstream");
assert_eq!(ultra_only_body["reasoning"]["effort"], "ultra");
}
#[test]
fn codex_search_normalizes_reasoning_and_projects_the_typed_request() {
fn codex_search_projects_wire_reasoning_and_the_typed_request() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:search",
provider_api_format: "openai:search",
@@ -478,7 +480,7 @@ mod tests {
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {
"effort": "ultra",
"effort": "max",
"summary": "auto",
"context": "current_turn",
"future_reasoning_field": true
@@ -505,7 +507,7 @@ mod tests {
}
#[test]
fn codex_search_validates_reasoning_effort_against_model_card() {
fn codex_search_defers_explicit_reasoning_effort_to_upstream() {
let finalization = OpenAiProviderRequestFinalization {
source_api_format: "openai:search",
provider_api_format: "openai:search",
@@ -524,17 +526,14 @@ mod tests {
finalize_openai_provider_request(&mut supported, finalization)
.expect("published Search effort should pass");
let mut unsupported = json!({
let mut unpublished = json!({
"id": "session-1",
"model": "gpt-5.6-sol",
"reasoning": {"effort": "none"}
});
let error = finalize_openai_provider_request(&mut unsupported, finalization)
.expect_err("unpublished Search effort should be rejected");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
finalize_openai_provider_request(&mut unpublished, finalization)
.expect("unpublished Search effort support should be validated by the upstream");
assert_eq!(unpublished["reasoning"]["effort"], "none");
}
#[test]
@@ -575,32 +574,26 @@ mod tests {
"input": [],
"reasoning": {"effort": "vendoreffortx"}
});
let error = finalize_openai_provider_request_with_codex_model_capabilities(
finalize_openai_provider_request_with_codex_model_capabilities(
&mut custom,
finalization,
Some(&capabilities),
)
.expect_err("custom reasoning efforts should match the model card exactly");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
.expect("explicit custom effort support should be validated by the upstream");
assert_eq!(custom["reasoning"]["effort"], "vendoreffortx");
let mut ultra = json!({
"model": "codex-custom",
"input": [],
"reasoning": {"effort": "ultra"}
});
let error = finalize_openai_provider_request_with_codex_model_capabilities(
finalize_openai_provider_request_with_codex_model_capabilities(
&mut ultra,
finalization,
Some(&capabilities),
)
.expect_err("ultra should require model-card support before mapping to max");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
.expect("explicit effort support should be validated by the upstream");
assert_eq!(ultra["reasoning"]["effort"], "ultra");
}
#[test]
@@ -176,6 +176,16 @@ pub fn codex_responses_model_capabilities_from_card(
})
.collect();
}
if model_id == "gpt-5.4-mini" {
capabilities.default_reasoning_effort = Some("none".to_string());
if !capabilities.supported_reasoning_efforts.is_empty()
&& !capabilities.supports_reasoning_effort("none")
{
capabilities
.supported_reasoning_efforts
.insert(0, "none".to_string());
}
}
if let Some(value) = card
.get("supports_parallel_tool_calls")
.and_then(Value::as_bool)
@@ -501,10 +511,10 @@ pub fn bundled_codex_model_cards() -> &'static [Value] {
model_id: "gpt-5.4-mini",
display_name: "GPT-5.4 Mini",
description: "Small, fast, and cost-efficient model for simpler coding tasks.",
default_reasoning_level: "medium",
default_reasoning_level: "none",
default_reasoning_summary: "none",
use_responses_lite: false,
efforts: &["low", "medium", "high", "xhigh"],
efforts: &["none", "low", "medium", "high", "xhigh"],
default_verbosity: "medium",
supports_priority_tier: false,
}),
@@ -1207,39 +1217,6 @@ fn remove_codex_reasoning_summary_delivery(body_object: &mut serde_json::Map<Str
}
}
fn normalize_codex_reasoning_effort(body_object: &mut serde_json::Map<String, Value>) {
let Some(reasoning) = body_object
.get_mut("reasoning")
.and_then(Value::as_object_mut)
else {
return;
};
let is_ultra = reasoning
.get("effort")
.and_then(Value::as_str)
.is_some_and(|effort| effort == "ultra");
if is_ultra {
reasoning.insert("effort".to_string(), json!("max"));
}
}
pub fn normalize_codex_openai_reasoning_wire_effort(
provider_request_body: &mut Value,
provider_type: &str,
provider_api_format: &str,
) {
if !provider_type.trim().eq_ignore_ascii_case("codex")
|| !(aether_ai_formats::is_openai_responses_family_format(provider_api_format)
|| aether_ai_formats::api_format_alias_matches(provider_api_format, "openai:search"))
{
return;
}
let Some(body_object) = provider_request_body.as_object_mut() else {
return;
};
normalize_codex_reasoning_effort(body_object);
}
fn is_codex_responses_lite_additional_tools_item(value: &Value) -> bool {
value
.get("type")
@@ -1777,7 +1754,6 @@ pub fn apply_codex_openai_responses_special_body_edits_with_source_model_and_cap
supports_reasoning_mode,
body_rules,
);
normalize_codex_reasoning_effort(body_object);
apply_codex_model_request_capabilities(
body_object,
provider_api_format,
@@ -2071,7 +2047,7 @@ mod tests {
"instructions": "Use the tools.",
"input": [{"id": "msg-1", "type": "message", "role": "user", "content": []}],
"tools": [{"type": "function", "name": "lookup"}],
"reasoning": {"effort": "ultra"},
"reasoning": {"effort": "max"},
"parallel_tool_calls": true,
"service_tier": "priority",
"text": {"format": {"type": "json_schema"}},
@@ -2254,7 +2230,7 @@ mod tests {
}
#[test]
fn ultra_reasoning_always_uses_max_on_the_provider_wire() {
fn special_body_edits_do_not_rewrite_explicit_ultra_effort() {
for provider_api_format in ["openai:responses", "openai:responses:compact"] {
let mut body = json!({
"model": "gpt-5.6-luna",
@@ -2270,7 +2246,7 @@ mod tests {
None,
None,
);
assert_eq!(body["reasoning"]["effort"], "max");
assert_eq!(body["reasoning"]["effort"], "ultra");
}
let mut custom = json!({
@@ -2472,6 +2448,46 @@ mod tests {
}
}
#[test]
fn bundled_gpt_5_4_mini_supports_official_reasoning_efforts() {
let capabilities =
resolve_codex_responses_model_capabilities("gpt-5.4-mini", "gpt-5.4-mini", None);
assert_eq!(
capabilities.default_reasoning_effort.as_deref(),
Some("none")
);
assert_eq!(
capabilities.supported_reasoning_efforts,
vec!["none", "low", "medium", "high", "xhigh"]
);
assert!(capabilities.supports_reasoning_effort("none"));
let metadata = build_codex_model_catalog_metadata(&[json!({
"slug": "gpt-5.4-mini",
"default_reasoning_level": "medium",
"supported_reasoning_levels": [
{"effort": "low"},
{"effort": "medium"},
{"effort": "high"},
{"effort": "xhigh"}
]
})]);
let cached_capabilities = resolve_codex_responses_model_capabilities(
"gpt-5.4-mini",
"gpt-5.4-mini",
Some(&metadata),
);
assert_eq!(
cached_capabilities.default_reasoning_effort.as_deref(),
Some("none")
);
assert_eq!(
cached_capabilities.supported_reasoning_efforts,
vec!["none", "low", "medium", "high", "xhigh"]
);
}
#[test]
fn model_card_preserves_custom_reasoning_effort_values() {
let metadata = build_codex_model_catalog_metadata(&[json!({
@@ -2806,6 +2822,56 @@ mod tests {
);
}
#[test]
fn codex_responses_body_edits_preserve_explicit_system_messages() {
let explicit_system_message = json!({
"type": "message",
"role": "system",
"content": [{
"type": "input_text",
"text": "Keep this caller-supplied role."
}]
});
let mut standard_body = json!({
"model": "gpt-5.4",
"instructions": "Standard base instructions.",
"input": [explicit_system_message.clone()]
});
apply_codex_openai_responses_special_body_edits(
&mut standard_body,
"codex",
"openai:responses",
None,
None,
);
assert_eq!(standard_body["instructions"], "Standard base instructions.");
assert_eq!(standard_body["input"], json!([explicit_system_message]));
let mut lite_body = json!({
"model": "gpt-5.6-sol",
"instructions": "Lite base instructions.",
"input": [explicit_system_message.clone()]
});
apply_codex_openai_responses_special_body_edits(
&mut lite_body,
"codex",
"openai:responses",
None,
None,
);
assert!(lite_body.get("instructions").is_none());
assert_eq!(lite_body["input"][0]["type"], "additional_tools");
assert_eq!(lite_body["input"][1]["role"], "developer");
assert_eq!(
lite_body["input"][1]["content"][0]["text"],
"Lite base instructions."
);
assert_eq!(lite_body["input"][2], explicit_system_message);
}
#[test]
fn codex_responses_body_edits_preserve_function_tools_for_codex_backend() {
let mut provider_request_body = json!({
@@ -4763,7 +4763,7 @@ mod tests {
}
#[test]
fn runtime_reasoning_effort_uses_concrete_mapped_model_as_authoritative_capability() {
fn runtime_reasoning_effort_is_preserved_across_concrete_model_mapping() {
let alias_to_gpt_5_6 = json!({
"model": "deployment-alias",
"messages": [{"role": "user", "content": "hello"}],
@@ -4784,22 +4784,19 @@ mod tests {
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "max"
});
let error = convert_request(
let converted = convert_request(
"openai:chat",
"openai:responses",
&gpt_5_6_to_gpt_5_4,
&FormatContext::default().with_mapped_model("gpt-5.4"),
)
.expect_err("a concrete GPT-5.4 mapped target must reject max");
assert!(matches!(
error,
super::FormatError::InvalidTargetField { ref field, .. }
if field == "reasoning_effort"
));
.expect("mapped model capability cards must not reject explicit efforts");
assert_eq!(converted["model"], "gpt-5.4");
assert_eq!(converted["reasoning"]["effort"], "max");
}
#[test]
fn pure_conversion_reasoning_effort_uses_the_concrete_mapped_model() {
fn pure_conversion_preserves_effort_across_concrete_model_mapping() {
let alias_to_gpt_5_6 = json!({
"model": "deployment-alias",
"messages": [{"role": "user", "content": "hello"}],
@@ -4819,40 +4816,32 @@ mod tests {
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "max"
});
let error = convert_request_pure_with_context(
let converted = convert_request_pure_with_context(
"openai:chat",
"openai:responses",
&gpt_5_6_to_gpt_5_4,
&FormatContext::default().with_mapped_model("gpt-5.4"),
)
.expect_err("a concrete GPT-5.4 target must reject max");
assert!(matches!(
error,
super::FormatError::InvalidTargetField { ref field, .. }
if field == "reasoning_effort"
));
.expect("mapped model capability cards must not reject explicit efforts");
assert_eq!(converted.value["reasoning"]["effort"], "max");
}
#[test]
fn runtime_openai_cross_format_enforces_known_efforts_and_preserves_custom_efforts() {
fn runtime_openai_cross_format_preserves_explicit_efforts() {
let alias_minimal = json!({
"model": "deployment-alias",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "minimal"
});
for target in ["openai:responses", "openai:responses:compact"] {
let error = convert_request(
let converted = convert_request(
"openai:chat",
target,
&alias_minimal,
&FormatContext::default().with_mapped_model("gpt-5.6-terra"),
)
.expect_err("mapped GPT-5.6 deployments must reject minimal effort");
assert!(matches!(
error,
super::FormatError::InvalidTargetField { ref field, .. }
if field == "reasoning_effort"
));
.expect("mapped model capability cards must not reject explicit efforts");
assert_eq!(converted["reasoning"]["effort"], "minimal");
}
let custom = json!({
@@ -4877,18 +4866,14 @@ mod tests {
"reasoning": {"effort": "ultra"}
});
for source in ["openai:responses", "openai:responses:compact"] {
let error = convert_request(
let converted = convert_request(
source,
"openai:chat",
&ultra,
&FormatContext::default().with_mapped_model("gpt-5.6-sol"),
)
.expect_err("Codex local ultra preset should not enter the OpenAI wire contract");
assert!(matches!(
error,
super::FormatError::InvalidEnumValue { ref field, ref value, .. }
if field == "reasoning.effort" && value == "ultra"
));
.expect("explicit effort support should be validated by the upstream");
assert_eq!(converted["reasoning_effort"], "ultra");
}
}
@@ -4909,42 +4894,30 @@ mod tests {
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "ultra"
});
let error = convert_request_pure("openai:chat", "openai:responses", &ultra)
.expect_err("Codex local ultra preset should not enter the OpenAI wire contract");
assert!(matches!(
error,
super::FormatError::InvalidEnumValue { ref field, ref value, .. }
if field == "reasoning_effort" && value == "ultra"
));
let converted = convert_request_pure("openai:chat", "openai:responses", &ultra)
.expect("explicit effort support should be validated by the upstream");
assert_eq!(converted.value["reasoning"]["effort"], "ultra");
}
#[test]
fn pure_openai_cross_format_rejects_gpt_5_6_minimal_effort() {
fn pure_openai_cross_format_preserves_unpublished_effort() {
let chat = json!({
"model": "gpt-5.6-sol",
"messages": [{"role": "user", "content": "hello"}],
"reasoning_effort": "minimal"
});
let error = convert_request_pure("openai:chat", "openai:responses", &chat)
.expect_err("GPT-5.6 does not publish minimal as a supported effort");
assert!(matches!(
error,
super::FormatError::InvalidTargetField { ref field, .. }
if field == "reasoning_effort"
));
let converted = convert_request_pure("openai:chat", "openai:responses", &chat)
.expect("upstream should validate unpublished reasoning efforts");
assert_eq!(converted.value["reasoning"]["effort"], "minimal");
let responses = json!({
"model": "gpt-5.6-terra",
"input": [{"role": "user", "content": "hello"}],
"reasoning": {"effort": "minimal"}
});
let error = convert_request_pure("openai:responses", "openai:chat", &responses)
.expect_err("GPT-5.6 does not publish minimal as a supported effort");
assert!(matches!(
error,
super::FormatError::InvalidTargetField { ref field, .. }
if field == "reasoning.effort"
));
let converted = convert_request_pure("openai:responses", "openai:chat", &responses)
.expect("upstream should validate unpublished reasoning efforts");
assert_eq!(converted.value["reasoning_effort"], "minimal");
}
#[test]
@@ -8,12 +8,11 @@ pub const MODEL_DIRECTIVE_API_FORMATS: [&str; 6] = [
"claude:messages",
"gemini:generate_content",
];
pub const OPENAI_MODEL_DIRECTIVE_SUFFIXES: [&str; 9] = [
"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "fast",
];
const OPENAI_SEARCH_MODEL_DIRECTIVE_SUFFIXES: [&str; 8] = [
"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra",
pub const OPENAI_MODEL_DIRECTIVE_SUFFIXES: [&str; 8] = [
"none", "minimal", "low", "medium", "high", "xhigh", "max", "fast",
];
const OPENAI_SEARCH_MODEL_DIRECTIVE_SUFFIXES: [&str; 7] =
["none", "minimal", "low", "medium", "high", "xhigh", "max"];
pub const CROSS_PROVIDER_MODEL_DIRECTIVE_SUFFIXES: [&str; 5] =
["low", "medium", "high", "xhigh", "max"];
@@ -32,7 +31,6 @@ pub struct ModelDirectiveSuffixResolution {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelOverride {
ReasoningEffort(ReasoningEffort),
CodexReasoningPreset(CodexReasoningPreset),
ServiceTier(ServiceTier),
}
@@ -40,7 +38,6 @@ impl ModelOverride {
pub fn suffix(&self) -> &'static str {
match self {
Self::ReasoningEffort(effort) => effort.as_str(),
Self::CodexReasoningPreset(preset) => preset.as_str(),
Self::ServiceTier(tier) => tier.as_directive_suffix(),
}
}
@@ -136,19 +133,6 @@ impl ReasoningEffort {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodexReasoningPreset {
Ultra,
}
impl CodexReasoningPreset {
pub fn as_str(self) -> &'static str {
match self {
Self::Ultra => "ultra",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceTier {
Priority,
@@ -288,16 +272,12 @@ fn parse_model_override(suffix: &str) -> Option<ModelOverride> {
}
fn parse_model_override_for_model(suffix: &str, model: &str) -> Option<ModelOverride> {
if suffix.eq_ignore_ascii_case("ultra") && codex_ultra_preset_supported_for_model(model) {
return Some(ModelOverride::CodexReasoningPreset(
CodexReasoningPreset::Ultra,
));
}
let _ = model;
parse_model_override(suffix)
}
pub fn model_directive_suffix_has_builtin_mapping(suffix: &str) -> bool {
parse_model_override(suffix).is_some() || suffix.eq_ignore_ascii_case("ultra")
parse_model_override(suffix).is_some()
}
pub fn model_directive_builtin_suffix_supported_for_source_model(
@@ -308,14 +288,7 @@ pub fn model_directive_builtin_suffix_supported_for_source_model(
}
fn model_directive_suffix_is_reasoning(suffix: &str) -> bool {
ReasoningEffort::parse(suffix).is_some() || suffix.eq_ignore_ascii_case("ultra")
}
fn codex_ultra_preset_supported_for_model(model: &str) -> bool {
crate::formats::openai::responses::codex::resolve_codex_responses_model_capabilities(
model, model, None,
)
.supports_reasoning_effort("ultra")
ReasoningEffort::parse(suffix).is_some()
}
pub fn model_directive_base_model(model: &str) -> Option<String> {
@@ -389,14 +362,6 @@ pub fn apply_model_directive_overrides_from_model(
)?;
applied_override = true;
}
ModelOverride::CodexReasoningPreset(preset) => {
apply_codex_reasoning_preset_override(
&mut patched_body,
provider_api_format,
*preset,
)?;
applied_override = true;
}
ModelOverride::ServiceTier(tier) => {
if is_openai_search {
continue;
@@ -437,9 +402,6 @@ pub fn default_model_directive_mapping_patch(
source_model,
effort,
)?,
ModelOverride::CodexReasoningPreset(preset) => {
apply_codex_reasoning_preset_override(&mut patch, provider_api_format, preset)?
}
ModelOverride::ServiceTier(tier) => {
apply_service_tier_override(&mut patch, provider_api_format, tier)?
}
@@ -533,29 +495,6 @@ fn apply_reasoning_effort_override(
}
}
fn apply_codex_reasoning_preset_override(
provider_request_body: &mut Value,
provider_api_format: &str,
preset: CodexReasoningPreset,
) -> Option<()> {
match crate::normalize_api_format_alias(provider_api_format).as_str() {
"openai:chat" => {
set_object_string(provider_request_body, "reasoning_effort", preset.as_str())
}
"openai:responses" | "openai:responses:compact" | "openai:search" => {
let object = provider_request_body.as_object_mut()?;
let reasoning = object
.entry("reasoning".to_string())
.or_insert_with(|| json!({}));
reasoning
.as_object_mut()?
.insert("effort".to_string(), json!(preset.as_str()));
Some(())
}
_ => None,
}
}
fn apply_service_tier_override(
provider_request_body: &mut Value,
provider_api_format: &str,
@@ -877,9 +816,8 @@ mod tests {
use super::{
apply_model_directive_overrides_from_model, default_model_directive_suffixes,
default_model_directives_config, parse_model_directive,
parse_model_directive_with_suffixes, CodexReasoningPreset, ModelDirective,
ModelDirectiveSuffixResolution, ModelOverride, ReasoningEffort, ServiceTier,
MODEL_DIRECTIVE_API_FORMATS,
parse_model_directive_with_suffixes, ModelDirective, ModelDirectiveSuffixResolution,
ModelOverride, ReasoningEffort, ServiceTier, MODEL_DIRECTIVE_API_FORMATS,
};
#[test]
@@ -1023,18 +961,9 @@ mod tests {
}
#[test]
fn parses_gpt_5_6_ultra_as_an_internal_reasoning_preset() {
assert_eq!(
parse_model_directive("gpt-5.6-sol-ultra"),
Some(ModelDirective {
base_model: "gpt-5.6-sol".to_string(),
overrides: vec![ModelOverride::CodexReasoningPreset(
CodexReasoningPreset::Ultra,
)],
})
);
fn does_not_treat_ultra_as_a_builtin_model_directive() {
for model in [
"gpt-5.6-sol-ultra",
"gpt-5.6-ultra",
"gpt-5.6-luna-ultra",
"gpt-5.4-ultra",
@@ -1042,15 +971,6 @@ mod tests {
] {
assert_eq!(parse_model_directive(model), None);
}
let mut unsupported = json!({"model": "gpt-5.4"});
assert!(apply_model_directive_overrides_from_model(
&mut unsupported,
"openai:responses",
"gpt-5.4",
"gpt-5.4-ultra",
)
.is_none());
}
#[test]
@@ -27,6 +27,7 @@ pub struct StreamingStandardFormatMatrix {
provider: Option<ProviderStreamParser>,
client: Option<ClientStreamEmitter>,
propagated_actual_service_tier: Option<String>,
pending_sse_event: Option<String>,
terminated: bool,
history_recorded: bool,
pending_history_record: Option<ResponseHistoryRecord>,
@@ -42,6 +43,7 @@ impl StreamingStandardFormatMatrix {
return Ok(Vec::new());
}
self.ensure_initialized(report_context);
let line = self.apply_sse_event_type(line);
if let Some(error_body) = build_client_error_body_for_line(report_context, &line) {
self.terminated = true;
return self.emit_error(error_body);
@@ -64,6 +66,10 @@ impl StreamingStandardFormatMatrix {
self.emit_frames(report_context, frames)
}
fn apply_sse_event_type(&mut self, line: Vec<u8>) -> Vec<u8> {
apply_sse_event_type(&mut self.pending_sse_event, line)
}
pub fn finish(&mut self, report_context: &Value) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
if self.terminated {
return Ok(Vec::new());
@@ -172,10 +178,46 @@ impl StreamingStandardFormatMatrix {
}
}
fn apply_sse_event_type(pending_sse_event: &mut Option<String>, line: Vec<u8>) -> Vec<u8> {
let Ok(text) = std::str::from_utf8(&line) else {
return line;
};
let trimmed = text.trim_matches('\r').trim();
if let Some(event) = trimmed.strip_prefix("event:").map(str::trim) {
*pending_sse_event = (!event.is_empty()).then(|| event.to_string());
return line;
}
if trimmed.is_empty() {
*pending_sse_event = None;
return line;
}
if !trimmed.starts_with("data:") {
return line;
}
let Some(event) = pending_sse_event.take() else {
return line;
};
let Some(mut payload) = decode_json_data_line(&line) else {
return line;
};
let Some(payload) = payload.as_object_mut() else {
return line;
};
if payload.contains_key("type") {
return line;
}
payload.insert("type".to_string(), Value::String(event));
let mut normalized = b"data: ".to_vec();
normalized.extend(serde_json::to_vec(&payload).expect("JSON value serialization cannot fail"));
normalized.push(b'\n');
normalized
}
#[derive(Default)]
pub struct StreamingStandardTerminalObserver {
provider: Option<TerminalStreamParser>,
latest_summary: Option<ExecutionStreamTerminalSummary>,
pending_sse_event: Option<String>,
}
impl StreamingStandardTerminalObserver {
@@ -185,6 +227,7 @@ impl StreamingStandardTerminalObserver {
line: Vec<u8>,
) -> Result<(), AiSurfaceFinalizeError> {
self.ensure_initialized(report_context);
let line = apply_sse_event_type(&mut self.pending_sse_event, line);
let Some(provider) = self.provider.as_mut() else {
return Ok(());
};
@@ -671,6 +714,72 @@ mod tests {
.collect()
}
fn event_only_line(event: &str) -> Vec<u8> {
format!("event: {event}\n").into_bytes()
}
#[test]
fn event_only_stream_types_convert_across_standard_formats() {
let responses_payload = json!({
"response": {
"id": "resp_event_only_123",
"model": "gpt-5.4",
"status": "in_progress",
"output": [],
},
});
let mut claude_matrix = StreamingStandardFormatMatrix::default();
let claude_context = report_context("openai:responses", "claude:messages");
assert!(claude_matrix
.transform_line(&claude_context, event_only_line("response.created"))
.expect("event should parse")
.is_empty());
let claude_output = claude_matrix
.transform_line(&claude_context, data_line(responses_payload))
.expect("response.created should convert to Claude");
assert!(String::from_utf8_lossy(&claude_output).contains("event: message_start"));
let mut gemini_matrix = StreamingStandardFormatMatrix::default();
let gemini_context = report_context("openai:responses", "gemini:generate_content");
gemini_matrix
.transform_line(
&gemini_context,
event_only_line("response.output_text.delta"),
)
.expect("event should parse");
let gemini_output = gemini_matrix
.transform_line(
&gemini_context,
data_line(json!({
"response_id": "resp_event_only_123",
"delta": "hello",
})),
)
.expect("text delta should convert to Gemini");
assert!(String::from_utf8_lossy(&gemini_output).contains("\"text\":\"hello\""));
let mut chat_matrix = StreamingStandardFormatMatrix::default();
let chat_context = report_context("claude:messages", "openai:chat");
chat_matrix
.transform_line(&chat_context, event_only_line("message_start"))
.expect("event should parse");
let chat_output = chat_matrix
.transform_line(
&chat_context,
data_line(json!({
"message": {
"id": "msg_event_only_123",
"model": "claude-sonnet-4-5",
},
})),
)
.expect("message_start should convert to Chat");
assert!(
String::from_utf8_lossy(&chat_output).contains("\"delta\":{\"role\":\"assistant\"}")
);
}
#[test]
fn streamed_chat_tool_call_records_responses_continuation_history() {
let report_context = json!({
@@ -1983,6 +2092,48 @@ mod tests {
assert_eq!(usage.cache_read_tokens, 0);
}
#[test]
fn terminal_observer_uses_event_type_when_data_omits_type() {
let report_context = report_context("openai:responses", "openai:chat");
let mut observer = StreamingStandardTerminalObserver::default();
observer
.push_line(&report_context, event_only_line("response.completed"))
.expect("event should parse");
observer
.push_line(
&report_context,
data_line(json!({
"response": {
"id": "resp_terminal_event_only_123",
"model": "gpt-5.4",
"status": "completed",
"output": [],
"usage": {
"input_tokens": 2,
"output_tokens": 3,
"total_tokens": 5,
},
},
})),
)
.expect("response.completed should parse");
let summary = observer.latest_summary().expect("summary should exist");
assert!(summary.observed_finish);
assert_eq!(
summary.response_id.as_deref(),
Some("resp_terminal_event_only_123")
);
assert_eq!(
summary
.standardized_usage
.as_ref()
.map(|usage| usage.input_tokens),
Some(2)
);
}
#[test]
fn terminal_observer_does_not_infer_provider_stream_event_api_format() {
let report_context = report_context("openai:chat", "openai:responses");
@@ -1056,6 +1056,32 @@ data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp
assert!(!output.contains("data: [DONE]"));
}
#[test]
fn standard_rewriter_converts_event_only_response_created_to_chat_role_chunk() {
let report_context = json!({
"provider_api_format": "openai:responses",
"client_api_format": "openai:chat",
"needs_conversion": true,
});
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
.expect("rewriter should exist");
let event_output = rewriter
.push_chunk(b"event: response.created\n")
.expect("event line should be buffered");
assert!(event_output.is_empty());
let output = rewriter
.push_chunk(
b"data: {\"response\":{\"id\":\"resp_created_123\",\"model\":\"gpt-5.4\",\"status\":\"in_progress\",\"output\":[]}}\n\n",
)
.expect("rewrite should succeed");
let output = String::from_utf8(output).expect("output should be utf8");
assert!(output.contains("\"object\":\"chat.completion.chunk\""));
assert!(output.contains("\"id\":\"resp_created_123\""));
assert!(output.contains("\"model\":\"gpt-5.4\""));
assert!(output.contains("\"delta\":{\"role\":\"assistant\"}"));
}
#[test]
fn explicit_responses_stream_format_preserves_function_call_metadata() {
let report_context = json!({