feat(openai): unify tier authorization and settlement

This commit is contained in:
MMEXA
2026-07-11 12:27:05 +08:00
parent b2f596b8f0
commit 0b30cc6b0f
32 changed files with 3170 additions and 670 deletions
@@ -7,8 +7,8 @@ use crate::{
protocol::canonical::{
canonical_blocks_to_openai_chat_message, canonical_stop_reason_to_openai,
canonical_usage_to_openai, openai_extensions, openai_finish_reason_to_canonical,
openai_message_content_blocks, openai_usage_to_canonical, CanonicalContentBlock,
CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
openai_message_content_blocks, openai_service_tier_extension, openai_usage_to_canonical,
CanonicalContentBlock, CanonicalResponse, CanonicalResponseOutput, CanonicalRole,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
@@ -17,20 +17,8 @@ pub fn from(body: &Value, _ctx: &FormatContext) -> Option<CanonicalResponse> {
from_raw(body)
}
pub fn to(response: &CanonicalResponse, ctx: &FormatContext) -> Option<Value> {
let mut body = to_raw(response);
if body.get("service_tier").is_none() {
if let Some(service_tier) = ctx
.report_context_value()
.get("original_request_body")
.and_then(Value::as_object)
.and_then(|request| request.get("service_tier"))
.cloned()
{
body["service_tier"] = service_tier;
}
}
Some(body)
pub fn to(response: &CanonicalResponse, _ctx: &FormatContext) -> Option<Value> {
Some(to_raw(response))
}
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
@@ -186,17 +174,7 @@ pub fn to_raw(canonical: &CanonicalResponse) -> Value {
{
response["created"] = Value::from(created_at);
}
if let Some(service_tier) = canonical
.extensions
.get(OPENAI_RESPONSES_EXTENSION_NAMESPACE)
.or_else(|| {
canonical
.extensions
.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE)
})
.and_then(|value| value.get("service_tier"))
.cloned()
{
if let Some(service_tier) = openai_service_tier_extension(&canonical.extensions).cloned() {
response["service_tier"] = service_tier;
}
response
@@ -10,6 +10,13 @@ use crate::formats::shared::sse::{encode_done_sse, encode_json_sse};
use crate::formats::shared::stream_core::common::*;
use crate::formats::shared::AiSurfaceFinalizeError;
fn normalize_openai_service_tier(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty() && value.len() <= 64)
.map(str::to_ascii_lowercase)
}
#[derive(Default)]
struct OpenAIChatProviderToolState {
id: Option<String>,
@@ -21,6 +28,7 @@ struct OpenAIChatProviderToolState {
pub struct OpenAIChatProviderState {
response_id: Option<String>,
model: Option<String>,
actual_service_tier: Option<String>,
started: bool,
finished: bool,
pending_finish_reason: Option<String>,
@@ -46,6 +54,7 @@ struct OpenAIResponsesProviderToolResultState {
pub struct OpenAIResponsesProviderState {
response_id: Option<String>,
model: Option<String>,
actual_service_tier: Option<String>,
started: bool,
finished: bool,
text_parts: BTreeMap<String, String>,
@@ -60,6 +69,10 @@ pub struct OpenAIResponsesProviderState {
}
impl OpenAIChatProviderState {
pub(crate) fn actual_service_tier(&self) -> Option<&str> {
self.actual_service_tier.as_deref()
}
fn finish_usage(value: Option<&Value>) -> Option<CanonicalUsage> {
let usage_object = value?.as_object()?;
let has_token_fields = [
@@ -129,6 +142,11 @@ impl OpenAIChatProviderState {
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.model.clone());
if let Some(service_tier) =
normalize_openai_service_tier(chunk_object.get("service_tier").and_then(Value::as_str))
{
self.actual_service_tier = Some(service_tier);
}
let mut out = Vec::new();
let Some(chunk_choices) = chunk_object.get("choices").and_then(Value::as_array) else {
@@ -369,6 +387,10 @@ impl OpenAIChatProviderState {
}
impl OpenAIResponsesProviderState {
pub(crate) fn actual_service_tier(&self) -> Option<&str> {
self.actual_service_tier.as_deref()
}
fn identity(&self, report_context: &Value) -> (String, String) {
resolve_identity(
self.response_id.as_deref(),
@@ -1229,6 +1251,11 @@ impl OpenAIResponsesProviderState {
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| self.model.clone());
if let Some(service_tier) =
normalize_openai_service_tier(response.get("service_tier").and_then(Value::as_str))
{
self.actual_service_tier = Some(service_tier);
}
}
match value
@@ -1781,6 +1808,7 @@ impl OpenAIResponsesProviderState {
pub struct OpenAIChatClientEmitter {
response_id: Option<String>,
model: Option<String>,
actual_service_tier: Option<String>,
started: bool,
finished: bool,
next_tool_call_index: usize,
@@ -1826,6 +1854,7 @@ fn web_search_query_from_arguments(arguments: &str) -> String {
pub struct OpenAIResponsesClientEmitter {
response_id: Option<String>,
model: Option<String>,
actual_service_tier: Option<String>,
created_at: Option<i64>,
message_item_id: Option<String>,
reasoning_item_id: Option<String>,
@@ -1851,6 +1880,31 @@ pub struct OpenAIResponsesClientEmitter {
}
impl OpenAIChatClientEmitter {
pub(crate) fn set_actual_service_tier(&mut self, value: Option<&str>) {
if value.is_some_and(|value| {
self.actual_service_tier
.as_deref()
.is_some_and(|current| current.eq_ignore_ascii_case(value.trim()))
}) {
return;
}
if let Some(value) = normalize_openai_service_tier(value) {
self.actual_service_tier = Some(value);
}
}
fn encode_chunk(&self, mut chunk: Value) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
if let (Some(service_tier), Some(object)) =
(self.actual_service_tier.as_ref(), chunk.as_object_mut())
{
object.insert(
"service_tier".to_string(),
Value::String(service_tier.clone()),
);
}
encode_json_sse(None, &chunk)
}
fn update_identity(&mut self, frame: &CanonicalStreamFrame) {
self.response_id = Some(frame.id.clone());
self.model = Some(frame.model.clone());
@@ -1861,15 +1915,12 @@ impl OpenAIChatClientEmitter {
return Ok(Vec::new());
}
self.started = true;
encode_json_sse(
None,
&build_openai_chat_role_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
),
)
self.encode_chunk(build_openai_chat_role_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
))
}
fn chat_tool_call_index(&mut self, canonical_index: usize) -> usize {
@@ -1889,9 +1940,8 @@ impl OpenAIChatClientEmitter {
CanonicalStreamEvent::Start => self.ensure_started(),
CanonicalStreamEvent::TextDelta(text) => {
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
out.extend(
self.encode_chunk(build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
@@ -1899,61 +1949,54 @@ impl OpenAIChatClientEmitter {
text,
None,
None,
),
)?);
))?,
);
Ok(out)
}
CanonicalStreamEvent::ReasoningDelta(text) => {
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"reasoning_content": text,
},
"finish_reason": Value::Null
}]
}),
)?);
out.extend(self.encode_chunk(json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"reasoning_content": text,
},
"finish_reason": Value::Null
}]
}))?);
Ok(out)
}
CanonicalStreamEvent::ReasoningSummaryDone => {
// CPA strategy: emit "\n\n" as paragraph separator between
// reasoning sections, matching CPA's Chat downstream behavior.
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"reasoning_content": "\n\n",
},
"finish_reason": Value::Null
}]
}),
)?);
out.extend(self.encode_chunk(json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"reasoning_content": "\n\n",
},
"finish_reason": Value::Null
}]
}))?);
Ok(out)
}
CanonicalStreamEvent::ReasoningSignature(_) => Ok(Vec::new()),
CanonicalStreamEvent::ContentPart(part) => {
let placeholder = openai_stream_placeholder_for_content_part(&part);
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
out.extend(
self.encode_chunk(build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
@@ -1961,8 +2004,8 @@ impl OpenAIChatClientEmitter {
placeholder,
None,
None,
),
)?);
))?,
);
Ok(out)
}
CanonicalStreamEvent::ImageGenerationCall { item, .. } => {
@@ -1971,9 +2014,8 @@ impl OpenAIChatClientEmitter {
};
let placeholder = openai_stream_placeholder_for_content_part(&part);
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
out.extend(
self.encode_chunk(build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
@@ -1981,8 +2023,8 @@ impl OpenAIChatClientEmitter {
placeholder,
None,
None,
),
)?);
))?,
);
Ok(out)
}
CanonicalStreamEvent::OpenAiResponsesOutputItem { .. } => {
@@ -1997,9 +2039,8 @@ impl OpenAIChatClientEmitter {
} => {
let mut out = self.ensure_started()?;
let chat_index = self.chat_tool_call_index(index);
out.extend(encode_json_sse(
None,
&build_openai_chat_chunk(
out.extend(
self.encode_chunk(build_openai_chat_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
@@ -2015,35 +2056,32 @@ impl OpenAIChatClientEmitter {
}
})]),
None,
),
)?);
))?,
);
Ok(out)
}
CanonicalStreamEvent::ToolCallArgumentsDelta { index, arguments } => {
let mut out = self.ensure_started()?;
let chat_index = self.chat_tool_call_index(index);
out.extend(encode_json_sse(
None,
&json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"tool_calls": [{
"index": chat_index,
"function": {
"arguments": arguments,
}
}]
},
"finish_reason": Value::Null
}]
}),
)?);
out.extend(self.encode_chunk(json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": {
"tool_calls": [{
"index": chat_index,
"function": {
"arguments": arguments,
}
}]
},
"finish_reason": Value::Null
}]
}))?);
Ok(out)
}
CanonicalStreamEvent::ToolResultDelta {
@@ -2060,21 +2098,18 @@ impl OpenAIChatClientEmitter {
delta.insert("name".to_string(), Value::String(name));
}
delta.insert("content".to_string(), Value::String(content));
out.extend(encode_json_sse(
None,
&json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": Value::Object(delta),
"finish_reason": Value::Null
}]
}),
)?);
out.extend(self.encode_chunk(json!({
"id": self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
"object": "chat.completion.chunk",
"model": self.model.as_deref().unwrap_or("unknown"),
"choices": [{
"index": 0,
"delta": Value::Object(delta),
"finish_reason": Value::Null
}]
}))?);
Ok(out)
}
CanonicalStreamEvent::UnknownEvent(payload)
@@ -2093,27 +2128,25 @@ impl OpenAIChatClientEmitter {
return Ok(Vec::new());
}
let mut out = self.ensure_started()?;
out.extend(encode_json_sse(
None,
&build_openai_chat_finish_chunk(
out.extend(
self.encode_chunk(build_openai_chat_finish_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
finish_reason.as_deref(),
),
)?);
))?,
);
if let Some(usage) = usage {
out.extend(encode_json_sse(
None,
&build_openai_chat_usage_chunk_from_usage(
out.extend(
self.encode_chunk(build_openai_chat_usage_chunk_from_usage(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
&usage,
),
)?);
))?,
);
}
out.extend(encode_done_sse());
self.finished = true;
@@ -2126,16 +2159,13 @@ impl OpenAIChatClientEmitter {
if !self.started || self.finished {
return Ok(Vec::new());
}
let out = encode_json_sse(
let out = self.encode_chunk(build_openai_chat_finish_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
None,
&build_openai_chat_finish_chunk(
self.response_id
.as_deref()
.unwrap_or("chatcmpl-local-stream"),
self.model.as_deref().unwrap_or("unknown"),
None,
),
)?;
))?;
self.finished = true;
let mut bytes = out;
bytes.extend(encode_done_sse());
@@ -2144,6 +2174,19 @@ impl OpenAIChatClientEmitter {
}
impl OpenAIResponsesClientEmitter {
pub(crate) fn set_actual_service_tier(&mut self, value: Option<&str>) {
if value.is_some_and(|value| {
self.actual_service_tier
.as_deref()
.is_some_and(|current| current.eq_ignore_ascii_case(value.trim()))
}) {
return;
}
if let Some(value) = normalize_openai_service_tier(value) {
self.actual_service_tier = Some(value);
}
}
fn response_id(&self) -> &str {
self.response_id.as_deref().unwrap_or("resp-local-stream")
}
@@ -2191,6 +2234,14 @@ impl OpenAIResponsesClientEmitter {
{
response_object.insert("created_at".to_string(), Value::from(created_at));
}
if let (Some(service_tier), Some(response_object)) =
(self.actual_service_tier.as_ref(), response.as_object_mut())
{
response_object.insert(
"service_tier".to_string(),
Value::String(service_tier.clone()),
);
}
response
}
@@ -2815,6 +2866,12 @@ impl OpenAIResponsesClientEmitter {
response_object.insert("created_at".to_string(), Value::from(created_at));
}
ensure_modern_openai_responses_response_fields(response_object);
if let Some(service_tier) = self.actual_service_tier.as_ref() {
response_object.insert(
"service_tier".to_string(),
Value::String(service_tier.clone()),
);
}
}
response
}
@@ -2848,6 +2905,7 @@ impl OpenAIResponsesClientEmitter {
"authoritative OpenAI Responses terminal payload must be an object",
)
})?;
self.set_actual_service_tier(response_object.get("service_tier").and_then(Value::as_str));
if let Some(response_id) = response_object
.get("id")
.and_then(Value::as_str)
@@ -49,7 +49,7 @@ pub(crate) fn validate_openai_reasoning_request_with_source_model(
provider_model,
source_model,
body,
false,
None,
None,
)
}
@@ -60,7 +60,7 @@ pub(crate) fn validate_openai_reasoning_request_with_model_profile(
provider_model: &str,
source_model: &str,
body: &Value,
use_model_card_reasoning_contract: bool,
model_card_reasoning_efforts: Option<&[String]>,
supports_reasoning_mode: Option<bool>,
) -> Result<(), OpenAiReasoningContractViolation> {
let Some(object) = body.as_object() else {
@@ -106,7 +106,7 @@ pub(crate) fn validate_openai_reasoning_request_with_model_profile(
provider_api_format,
provider_model,
source_model,
use_model_card_reasoning_contract,
model_card_reasoning_efforts,
)?;
}
@@ -122,6 +122,12 @@ pub(crate) fn validate_openai_reasoning_request_with_model_profile(
{
validate_reasoning_context(context)?;
}
if let Some(summary) = reasoning
.and_then(|reasoning| reasoning.get("summary"))
.filter(|value| !value.is_null())
{
validate_reasoning_summary(summary)?;
}
Ok(())
}
@@ -132,7 +138,7 @@ fn validate_reasoning_effort(
provider_api_format: &str,
provider_model: &str,
source_model: &str,
use_model_card_reasoning_contract: bool,
model_card_reasoning_efforts: Option<&[String]>,
) -> Result<(), OpenAiReasoningContractViolation> {
let field = if source_api_format == "openai:chat" {
"reasoning_effort"
@@ -163,8 +169,22 @@ fn validate_reasoning_effort(
reason: "ultra is a Codex client preset, not an OpenAI wire effort".to_string(),
});
}
if use_model_card_reasoning_contract {
return Ok(());
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(());
@@ -252,6 +272,26 @@ fn validate_reasoning_context(value: &Value) -> Result<(), OpenAiReasoningContra
})
}
fn validate_reasoning_summary(value: &Value) -> Result<(), OpenAiReasoningContractViolation> {
let Some(summary) = value.as_str() else {
return Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidType,
field: "reasoning.summary".to_string(),
value: Some(value.to_string()),
reason: "reasoning summary must be a string".to_string(),
});
};
if matches!(summary, "auto" | "concise" | "detailed") {
return Ok(());
}
Err(OpenAiReasoningContractViolation {
kind: OpenAiReasoningViolationKind::InvalidEnum,
field: "reasoning.summary".to_string(),
value: Some(summary.to_string()),
reason: "reasoning summary is not a supported wire value".to_string(),
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
@@ -406,6 +446,33 @@ mod tests {
assert_eq!(invalid.kind, OpenAiReasoningViolationKind::InvalidEnum);
}
#[test]
fn reasoning_summary_accepts_only_openai_wire_values() {
for summary in ["auto", "concise", "detailed"] {
validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"summary": summary}}),
)
.expect("documented reasoning summary should be accepted");
}
for (summary, expected_kind) in [
(json!("none"), OpenAiReasoningViolationKind::InvalidEnum),
(json!(true), OpenAiReasoningViolationKind::InvalidType),
] {
let error = validate_openai_reasoning_request(
"openai:responses",
"openai:responses",
"gpt-5.6-sol",
&json!({"reasoning": {"summary": summary}}),
)
.expect_err("invalid reasoning summary should be rejected");
assert_eq!(error.kind, expected_kind);
}
}
#[test]
fn nullable_reasoning_fields_are_treated_as_unconfigured() {
for body in [
@@ -146,7 +146,7 @@ fn validate_openai_provider_request_contract_with_codex_model_capabilities(
provider_model,
source_model,
body,
model_capabilities.is_some(),
model_capabilities.map(|capabilities| capabilities.supported_reasoning_efforts.as_slice()),
None,
)
.map_err(OpenAiProviderRequestContractViolation::Reasoning)
@@ -212,6 +212,40 @@ mod tests {
.is_err());
}
#[test]
fn codex_finalization_enforces_model_card_reasoning_efforts() {
let mut body = json!({
"model": "gpt-5.6-sol",
"input": [],
"reasoning": {"effort": "minimal"}
});
let error = finalize_openai_provider_request(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format: "openai:responses",
provider_api_format: "openai:responses",
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: false,
require_body_stream_field: true,
},
)
.expect_err("GPT-5.6 Codex model card should reject minimal");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(
super::OpenAiReasoningContractViolation {
kind: crate::formats::openai::reasoning::OpenAiReasoningViolationKind::UnsupportedForModel,
..
}
)
));
}
#[test]
fn finalization_reapplies_codex_and_compact_projection_after_mutations() {
let mut body = json!({
@@ -419,26 +453,32 @@ mod tests {
"input": [],
"reasoning": {"effort": "vendoreffortx"}
});
finalize_openai_provider_request_with_codex_model_capabilities(
let error = finalize_openai_provider_request_with_codex_model_capabilities(
&mut custom,
finalization,
Some(&capabilities),
)
.expect("Codex custom reasoning efforts should remain exact");
assert_eq!(custom["reasoning"]["effort"], "vendoreffortx");
.expect_err("custom reasoning efforts should match the model card exactly");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
let mut ultra = json!({
"model": "codex-custom",
"input": [],
"reasoning": {"effort": "ultra"}
});
finalize_openai_provider_request_with_codex_model_capabilities(
let error = finalize_openai_provider_request_with_codex_model_capabilities(
&mut ultra,
finalization,
Some(&capabilities),
)
.expect("ultra should use max on the OpenAI wire contract");
assert_eq!(ultra["reasoning"]["effort"], "max");
.expect_err("ultra should require model-card support before mapping to max");
assert!(matches!(
error,
super::OpenAiProviderRequestContractViolation::Reasoning(_)
));
}
#[test]
@@ -626,7 +666,7 @@ mod tests {
"stream_options": {"include_usage": true},
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {"effort": "future"},
"reasoning": {"effort": "max"},
"text": {"verbosity": "medium"},
"tools": [{"type": "function", "name": "lookup", "parameters": {}}]
});
@@ -656,7 +696,7 @@ mod tests {
assert!(body.get(field).is_none(), "{field} must not reach Compact");
}
assert_eq!(body["parallel_tool_calls"], false);
assert_eq!(body["reasoning"]["effort"], "future");
assert_eq!(body["reasoning"]["effort"], "max");
assert_eq!(body["reasoning"]["context"], "all_turns");
assert_eq!(body["text"]["verbosity"], "medium");
assert!(body.get("tools").is_none());
@@ -1118,7 +1118,11 @@ fn ensure_codex_reasoning_defaults(
.get_mut("reasoning")
.and_then(Value::as_object_mut)
.and_then(|reasoning| {
if !capabilities.supports_reasoning_summary_parameter {
if !capabilities.supports_reasoning_summary_parameter
|| reasoning
.get("summary")
.is_some_and(codex_reasoning_summary_is_disabled)
{
reasoning.remove("summary");
}
reasoning.get("summary")
@@ -1154,7 +1158,9 @@ fn ensure_codex_reasoning_defaults(
}
}
if !capabilities.supports_reasoning_summary_parameter
|| reasoning_object.get("summary").is_some_and(Value::is_null)
|| reasoning_object
.get("summary")
.is_some_and(codex_reasoning_summary_is_disabled)
{
reasoning_object.remove("summary");
} else if !reasoning_object.contains_key("summary") {
@@ -1173,6 +1179,13 @@ fn ensure_codex_reasoning_defaults(
}
}
fn codex_reasoning_summary_is_disabled(value: &Value) -> bool {
value.is_null()
|| value
.as_str()
.is_some_and(|summary| summary.eq_ignore_ascii_case("none"))
}
fn remove_codex_reasoning_summary_delivery(body_object: &mut serde_json::Map<String, Value>) {
let remove_stream_options = body_object
.get_mut("stream_options")
@@ -2365,6 +2378,34 @@ mod tests {
assert!(provider_request_body.get("instructions").is_none());
}
#[test]
fn codex_responses_body_edits_omit_disabled_reasoning_summary_and_delivery() {
let mut provider_request_body = json!({
"input": [{"role": "user", "content": "hello"}],
"model": "gpt-5.6-sol",
"stream": true,
"reasoning": {"effort": "high", "summary": "none"},
"stream_options": {
"reasoning_summary_delivery": "sequential_cutoff",
"future_option": true
}
});
apply_codex_openai_responses_special_body_edits(
&mut provider_request_body,
"codex",
"openai:responses",
None,
None,
);
assert!(provider_request_body["reasoning"].get("summary").is_none());
assert_eq!(
provider_request_body["stream_options"],
json!({"future_option": true})
);
}
#[test]
fn codex_responses_body_edits_project_include_and_preserve_disabled_parallel_calls() {
let mut provider_request_body = json!( {
@@ -13,8 +13,8 @@ use crate::{
flush_openai_responses_message_item, is_openai_responses_raw_block,
is_openai_thinking_block, namespace_extension_object, openai_responses_extensions,
openai_responses_item_extension_object, openai_responses_output_to_canonical,
openai_responses_usage_to_canonical, CanonicalContentBlock, CanonicalResponse,
CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
openai_responses_usage_to_canonical, openai_service_tier_extension, CanonicalContentBlock,
CanonicalResponse, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
OPENAI_RESPONSES_EXTENSION_NAMESPACE, OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
},
};
@@ -371,9 +371,9 @@ pub fn to_raw(canonical: &CanonicalResponse, report_context: &Value, compact: bo
response.insert(key.to_string(), value.clone());
}
}
if let Some(service_tier) = request_object.get("service_tier").cloned() {
response.insert("service_tier".to_string(), service_tier);
}
}
if let Some(service_tier) = openai_service_tier_extension(&canonical.extensions).cloned() {
response.insert("service_tier".to_string(), service_tier);
}
let mut extension_fields = namespace_extension_object(
&canonical.extensions,
@@ -23,6 +23,7 @@ use crate::formats::shared::AiSurfaceFinalizeError;
pub struct StreamingStandardFormatMatrix {
provider: Option<ProviderStreamParser>,
client: Option<ClientStreamEmitter>,
propagated_actual_service_tier: Option<String>,
terminated: bool,
}
@@ -40,10 +41,21 @@ impl StreamingStandardFormatMatrix {
self.terminated = true;
return self.emit_error(error_body);
}
let Some(provider) = self.provider.as_mut() else {
let (provider, client, propagated_actual_service_tier) = (
&mut self.provider,
&mut self.client,
&mut self.propagated_actual_service_tier,
);
let Some(provider) = provider.as_mut() else {
return Ok(Vec::new());
};
let frames = provider.push_line(report_context, line)?;
if provider.actual_service_tier() != propagated_actual_service_tier.as_deref() {
*propagated_actual_service_tier = provider.actual_service_tier().map(ToOwned::to_owned);
if let Some(client) = client.as_mut() {
client.set_actual_service_tier(propagated_actual_service_tier.as_deref());
}
}
self.emit_frames(frames)
}
@@ -52,10 +64,21 @@ impl StreamingStandardFormatMatrix {
return Ok(Vec::new());
}
self.ensure_initialized(report_context);
let Some(provider) = self.provider.as_mut() else {
let (provider, client, propagated_actual_service_tier) = (
&mut self.provider,
&mut self.client,
&mut self.propagated_actual_service_tier,
);
let Some(provider) = provider.as_mut() else {
return Ok(Vec::new());
};
let frames = provider.finish(report_context)?;
if provider.actual_service_tier() != propagated_actual_service_tier.as_deref() {
*propagated_actual_service_tier = provider.actual_service_tier().map(ToOwned::to_owned);
if let Some(client) = client.as_mut() {
client.set_actual_service_tier(propagated_actual_service_tier.as_deref());
}
}
let mut out = self.emit_frames(frames)?;
if let Some(client) = self.client.as_mut() {
out.extend(client.finish()?);
@@ -133,14 +156,6 @@ impl StreamingStandardTerminalObserver {
report_context: &Value,
line: Vec<u8>,
) -> Result<(), AiSurfaceFinalizeError> {
if let Some(service_tier) = decode_json_data_line(&line)
.as_ref()
.and_then(provider_actual_service_tier_from_stream_event)
{
self.latest_summary
.get_or_insert_with(ExecutionStreamTerminalSummary::default)
.provider_actual_service_tier = Some(service_tier);
}
self.ensure_initialized(report_context);
let Some(provider) = self.provider.as_mut() else {
return Ok(());
@@ -148,7 +163,13 @@ impl StreamingStandardTerminalObserver {
match provider {
TerminalStreamParser::Standard(provider) => {
let frames = provider.push_line(report_context, line)?;
let actual_service_tier = provider.actual_service_tier().map(ToOwned::to_owned);
self.observe_frames(frames);
if let Some(actual_service_tier) = actual_service_tier {
self.latest_summary
.get_or_insert_with(ExecutionStreamTerminalSummary::default)
.provider_actual_service_tier = Some(actual_service_tier);
}
}
TerminalStreamParser::OpenAIImage(provider) => {
if let Some(summary) = provider.push_line(report_context, line)? {
@@ -254,17 +275,6 @@ impl StreamingStandardTerminalObserver {
}
}
fn provider_actual_service_tier_from_stream_event(event: &Value) -> Option<String> {
event
.get("response")
.and_then(|response| response.get("service_tier"))
.or_else(|| event.get("service_tier"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty() && value.len() <= 64)
.map(str::to_ascii_lowercase)
}
enum TerminalStreamParser {
Standard(ProviderStreamParser),
OpenAIImage(OpenAiImageStreamTerminalState),
@@ -333,6 +343,14 @@ impl ProviderStreamParser {
ProviderStreamParser::Gemini(state) => state.finish(report_context),
}
}
fn actual_service_tier(&self) -> Option<&str> {
match self {
ProviderStreamParser::OpenAIChat(state) => state.actual_service_tier(),
ProviderStreamParser::OpenAIResponses(state) => state.actual_service_tier(),
ProviderStreamParser::Claude(_) | ProviderStreamParser::Gemini(_) => None,
}
}
}
enum ClientStreamEmitter {
@@ -410,6 +428,14 @@ impl ClientStreamEmitter {
}
}
fn set_actual_service_tier(&mut self, value: Option<&str>) {
match self {
ClientStreamEmitter::OpenAIChat(state) => state.set_actual_service_tier(value),
ClientStreamEmitter::OpenAIResponses(state) => state.set_actual_service_tier(value),
ClientStreamEmitter::Claude(_) | ClientStreamEmitter::Gemini(_) => {}
}
}
fn finish(&mut self) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
match self {
ClientStreamEmitter::OpenAIChat(state) => state.finish(),
@@ -604,6 +630,15 @@ mod tests {
format!("data: {}\n", value).into_bytes()
}
fn json_data_events(bytes: &[u8]) -> Vec<Value> {
String::from_utf8_lossy(bytes)
.lines()
.filter_map(|line| line.strip_prefix("data: "))
.filter(|payload| *payload != "[DONE]")
.filter_map(|payload| serde_json::from_str(payload).ok())
.collect()
}
#[test]
fn transforms_provider_errors_to_openai_chat_error_bodies() {
let cases = [
@@ -1895,6 +1930,7 @@ mod tests {
"model": "gpt-5.6",
"service_tier": "Default",
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
})),
)
.expect("Chat terminal tier should be observed");
@@ -1904,6 +1940,17 @@ mod tests {
.and_then(|summary| summary.provider_actual_service_tier.as_deref()),
Some("default")
);
let chat_summary = chat_observer
.latest_summary()
.expect("Chat summary should exist");
assert!(chat_summary.observed_finish);
assert_eq!(
chat_summary
.standardized_usage
.as_ref()
.map(|usage| (usage.input_tokens, usage.output_tokens)),
Some((10, 2))
);
let responses_context = report_context("openai:responses", "openai:responses");
let mut responses_observer = StreamingStandardTerminalObserver::default();
@@ -1918,6 +1965,7 @@ mod tests {
"status": "completed",
"service_tier": "Flex",
"output": [],
"usage": {"input_tokens": 10, "output_tokens": 2, "total_tokens": 12},
},
"sequence_number": 1,
})),
@@ -1929,6 +1977,85 @@ mod tests {
.and_then(|summary| summary.provider_actual_service_tier.as_deref()),
Some("flex")
);
let responses_summary = responses_observer
.latest_summary()
.expect("Responses summary should exist");
assert!(responses_summary.observed_finish);
assert_eq!(
responses_summary
.standardized_usage
.as_ref()
.map(|usage| (usage.input_tokens, usage.output_tokens)),
Some((10, 2))
);
}
#[test]
fn openai_chat_client_chunks_carry_provider_actual_service_tier() {
let context = report_context("openai:chat", "openai:chat");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(
&context,
data_line(json!({
"id": "chatcmpl_tier_stream",
"object": "chat.completion.chunk",
"model": "gpt-5.6",
"service_tier": "Default",
"choices": [{
"index": 0,
"delta": {"role": "assistant", "content": "done"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}
})),
)
.expect("Chat stream should transform");
let events = json_data_events(&output);
assert!(!events.is_empty());
assert!(events
.iter()
.all(|event| event.get("service_tier") == Some(&json!("default"))));
}
#[test]
fn responses_actual_service_tier_reaches_transformed_chat_chunks() {
let context = report_context("openai:responses", "openai:chat");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(
&context,
data_line(json!({
"type": "response.completed",
"response": {
"id": "resp_tier_stream",
"object": "response",
"model": "gpt-5.6",
"status": "completed",
"service_tier": "Flex",
"output": [{
"type": "message",
"id": "msg_tier_stream",
"role": "assistant",
"status": "completed",
"content": [{
"type": "output_text",
"text": "done",
"annotations": []
}]
}],
"usage": {"input_tokens": 10, "output_tokens": 2, "total_tokens": 12}
}
})),
)
.expect("Responses stream should transform");
let events = json_data_events(&output);
assert!(!events.is_empty());
assert!(events
.iter()
.all(|event| event.get("service_tier") == Some(&json!("flex"))));
}
#[test]
@@ -93,14 +93,33 @@ pub fn maybe_bridge_standard_sync_json_to_stream(
) else {
return Ok(None);
};
let terminal_summary =
build_terminal_summary_from_openai_responses_response(&openai_responses_response);
let provider_actual_service_tier = provider_actual_service_tier_from_sync_response(
provider_body_json,
provider_api_format.as_str(),
);
let terminal_summary = build_terminal_summary_from_openai_responses_response(
&openai_responses_response,
provider_actual_service_tier.clone(),
);
let canonical_frames = build_canonical_frames_from_openai_responses_response(
&openai_responses_response,
&bridge_context,
)?;
let sse_body =
emit_client_stream_from_canonical_frames(canonical_frames, client_api_format.as_str())?;
let sse_body = if is_openai_responses_family_api_format(client_api_format.as_str()) {
emit_openai_responses_stream_with_authoritative_terminal(
canonical_frames,
&openai_responses_response,
openai_responses_terminal_event_type(&openai_responses_response)
.unwrap_or("response.completed"),
provider_actual_service_tier.as_deref(),
)?
} else {
emit_client_stream_from_canonical_frames(
canonical_frames,
client_api_format.as_str(),
provider_actual_service_tier.as_deref(),
)?
};
Ok(Some(SyncToStreamBridgeOutcome {
sse_body,
@@ -140,11 +159,15 @@ fn bridge_openai_responses_same_family_sync_json_to_stream(
canonical_frames,
response,
terminal_event_type,
provider_actual_service_tier_from_sync_response(response, provider_api_format).as_deref(),
)?;
Ok(Some(SyncToStreamBridgeOutcome {
sse_body,
terminal_summary: build_terminal_summary_from_openai_responses_response(response),
terminal_summary: build_terminal_summary_from_openai_responses_response(
response,
provider_actual_service_tier_from_sync_response(response, provider_api_format),
),
}))
}
@@ -1010,14 +1033,17 @@ fn openai_responses_terminal_event_type(response: &Value) -> Option<&'static str
fn emit_client_stream_from_canonical_frames(
canonical_frames: Vec<CanonicalStreamFrame>,
client_api_format: &str,
provider_actual_service_tier: Option<&str>,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
match client_api_format {
"openai:chat" => {
let mut emitter = OpenAIChatClientEmitter::default();
emitter.set_actual_service_tier(provider_actual_service_tier);
emit_with_openai_chat_emitter(&mut emitter, canonical_frames)
}
"openai:responses" | "openai:responses:compact" => {
let mut emitter = OpenAIResponsesClientEmitter::default();
emitter.set_actual_service_tier(provider_actual_service_tier);
emit_with_openai_responses_emitter(&mut emitter, canonical_frames)
}
"claude:messages" => {
@@ -1076,8 +1102,10 @@ fn emit_openai_responses_stream_with_authoritative_terminal(
canonical_frames: Vec<CanonicalStreamFrame>,
authoritative_response: &Value,
terminal_event_type: &'static str,
provider_actual_service_tier: Option<&str>,
) -> Result<Vec<u8>, AiSurfaceFinalizeError> {
let mut emitter = OpenAIResponsesClientEmitter::default();
emitter.set_actual_service_tier(provider_actual_service_tier);
let mut output = Vec::new();
for frame in canonical_frames {
if matches!(
@@ -1145,6 +1173,7 @@ fn emit_with_gemini_emitter(
fn build_terminal_summary_from_openai_responses_response(
openai_responses_response: &Value,
provider_actual_service_tier: Option<String>,
) -> Option<ExecutionStreamTerminalSummary> {
let response = openai_responses_response.as_object()?;
let response_id = response
@@ -1168,18 +1197,34 @@ fn build_terminal_summary_from_openai_responses_response(
finish_reason,
response_id,
model,
provider_actual_service_tier: response
.get("service_tier")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty() && value.len() <= 64)
.map(str::to_ascii_lowercase),
provider_actual_service_tier,
observed_finish: true,
unknown_event_count: 0,
parser_error: None,
})
}
fn provider_actual_service_tier_from_sync_response(
provider_response: &Value,
provider_api_format: &str,
) -> Option<String> {
if !matches!(
normalize_api_format(provider_api_format).as_str(),
"openai:chat" | "openai:responses" | "openai:responses:compact"
) {
return None;
}
provider_response
.get("response")
.and_then(Value::as_object)
.and_then(|response| response.get("service_tier"))
.or_else(|| provider_response.get("service_tier"))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty() && value.len() <= 64)
.map(str::to_ascii_lowercase)
}
fn resolve_openai_responses_finish_reason(output: &[Value]) -> String {
let has_tool_calls = output.iter().filter_map(Value::as_object).any(|item| {
item.get("type")
@@ -1367,6 +1412,112 @@ mod tests {
assert_eq!(event["response"], response);
}
#[test]
fn cross_format_bridge_keeps_provider_actual_service_tier_authoritative() {
let provider_response = json!({
"id": "chatcmpl_actual_tier",
"object": "chat.completion",
"model": "gpt-5.6-sol",
"service_tier": "Default",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "done"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}
});
let report_context = json!({
"original_request_body": {
"model": "gpt-5.6-sol",
"service_tier": "priority"
}
});
let outcome = maybe_bridge_standard_sync_json_to_stream(
&provider_response,
"openai:chat",
"openai:responses",
Some(&report_context),
)
.expect("cross-format bridge should succeed")
.expect("cross-format bridge should emit terminal SSE");
assert_eq!(
outcome
.terminal_summary
.as_ref()
.and_then(|summary| summary.provider_actual_service_tier.as_deref()),
Some("default")
);
let events = json_sse_events(&utf8(outcome.sse_body));
let terminal = events
.iter()
.find(|event| event["type"] == "response.completed")
.expect("terminal event should exist");
assert_eq!(terminal["response"]["service_tier"], "Default");
let response_events = events
.iter()
.filter(|event| {
matches!(
event.get("type").and_then(Value::as_str),
Some("response.created" | "response.in_progress" | "response.completed")
)
})
.collect::<Vec<_>>();
assert!(!response_events.is_empty());
assert!(response_events.iter().all(|event| {
event
.pointer("/response/service_tier")
.and_then(Value::as_str)
.is_some_and(|tier| tier.eq_ignore_ascii_case("default"))
}));
}
#[test]
fn sync_bridge_does_not_echo_requested_service_tier_as_provider_actual() {
let provider_response = json!({
"id": "chatcmpl_without_actual_tier",
"object": "chat.completion",
"model": "gpt-5.6-sol",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "done"},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}
});
let report_context = json!({
"original_request_body": {
"model": "gpt-5.6-sol",
"service_tier": "priority"
}
});
let outcome = maybe_bridge_standard_sync_json_to_stream(
&provider_response,
"openai:chat",
"openai:responses",
Some(&report_context),
)
.expect("cross-format bridge should succeed")
.expect("cross-format bridge should emit terminal SSE");
assert_eq!(
outcome
.terminal_summary
.as_ref()
.and_then(|summary| summary.provider_actual_service_tier.as_deref()),
None
);
let events = json_sse_events(&utf8(outcome.sse_body));
assert!(events.iter().all(|event| {
event
.get("response")
.and_then(|response| response.get("service_tier"))
.is_none()
}));
}
#[test]
fn bridges_output_only_compact_json_without_reshaping_the_terminal_response() {
let response = json!({
@@ -6577,6 +6577,23 @@ pub(crate) fn openai_responses_extension(extensions: &BTreeMap<String, Value>) -
.or_else(|| extensions.get(OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE))
}
pub(crate) fn openai_service_tier_extension(
extensions: &BTreeMap<String, Value>,
) -> Option<&Value> {
[
OPENAI_RESPONSES_EXTENSION_NAMESPACE,
OPENAI_RESPONSES_LEGACY_EXTENSION_NAMESPACE,
"openai",
]
.into_iter()
.find_map(|namespace| {
extensions
.get(namespace)
.and_then(Value::as_object)
.and_then(|object| object.get("service_tier"))
})
}
pub(crate) fn openai_responses_item_extension_object(
extensions: &BTreeMap<String, Value>,
existing: &Map<String, Value>,