mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 12:40:20 +08:00
fix: align Responses routing and model permissions
This commit is contained in:
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user