Merge pull request #841 from zhefox/fix/codex-service-tier-passthrough

fix(codex): preserve explicit service tiers and adapt usage badges
This commit is contained in:
ZheFox
2026-09-22 22:28:20 +08:00
committed by GitHub
7 changed files with 127 additions and 43 deletions
@@ -9737,7 +9737,10 @@ mod tests {
headers: BTreeMap::from([("content-type".into(), "application/json".into())]),
content_type: Some("application/json".into()),
content_encoding: Some(encoding.into()),
body: RequestBody::from_json(json!({"model": "gpt-4.1"})),
body: RequestBody::from_json(json!({
"model": "gpt-4.1",
"service_tier": "ultrafast"
})),
stream: false,
client_api_format: "openai:chat".into(),
provider_api_format: "openai:chat".into(),
@@ -9758,7 +9761,7 @@ mod tests {
result.body.and_then(|body| body.json_body),
Some(json!({
"content_encoding": encoding,
"body": {"model": "gpt-4.1"},
"body": {"model": "gpt-4.1", "service_tier": "ultrafast"},
}))
);
}
@@ -38,6 +38,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
trace_id: String,
url: String,
model: String,
service_tier: String,
content_encoding: String,
stream: bool,
accept: String,
@@ -383,6 +384,13 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
service_tier: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("service_tier"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
content_encoding: payload
.get("content_encoding")
.and_then(|value| value.as_str())
@@ -605,7 +613,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
)
.header(TRACE_ID_HEADER, "trace-codex-cli-stream-local-123")
.body(
r#"{"model":"gpt-5.6-sol","instructions":"Use the configured tools.","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"compact"}]},{"type":"compaction_trigger"}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],"context_management":[{"type":"compaction","compact_threshold":128000}],"parallel_tool_calls":true,"prompt_cache_key":"thread-codex-stream-local-123","client_metadata":{"session_id":"session-codex-stream-local-123","thread_id":"thread-codex-stream-local-123"},"stream":true}"#,
r#"{"model":"gpt-5.6-sol","service_tier":"ultrafast","instructions":"Use the configured tools.","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"compact"}]},{"type":"compaction_trigger"}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],"context_management":[{"type":"compaction","compact_threshold":128000}],"parallel_tool_calls":true,"prompt_cache_key":"thread-codex-stream-local-123","client_metadata":{"session_id":"session-codex-stream-local-123","thread_id":"thread-codex-stream-local-123"},"stream":true}"#,
)
.send()
.await
@@ -716,6 +724,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
"https://chatgpt.com/backend-api/codex/responses"
);
assert_eq!(seen_execution_runtime_request.model, "gpt-5.6-sol");
assert_eq!(seen_execution_runtime_request.service_tier, "ultrafast");
assert_eq!(seen_execution_runtime_request.content_encoding, "zstd");
assert!(seen_execution_runtime_request.stream);
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
@@ -229,7 +229,7 @@ fn validate_final_openai_provider_request_contract(
#[cfg(test)]
mod tests {
use serde_json::json;
use serde_json::{json, Value};
use super::{
finalize_openai_provider_request,
@@ -587,6 +587,61 @@ mod tests {
}
}
#[test]
fn codex_finalization_preserves_explicit_service_tiers() {
let capabilities = CodexResponsesModelCapabilities {
use_responses_lite: false,
supports_reasoning_summary_parameter: false,
default_reasoning_effort: None,
default_reasoning_summary: None,
supported_reasoning_efforts: Vec::new(),
supports_parallel_tool_calls: true,
support_verbosity: false,
default_verbosity: None,
supported_service_tiers: vec!["priority".to_string()],
};
for source_api_format in ["openai:responses", "openai:chat"] {
for provider_api_format in ["openai:responses", "openai:responses:compact"] {
for model_capabilities in [None, Some(&capabilities)] {
for service_tier in [
Some("ultrafast"),
Some("priority"),
Some("default"),
Some("auto"),
Some("flex"),
Some("future-tier"),
None,
] {
let mut body = json!({"model": "gpt-5.6-sol", "input": []});
if let Some(service_tier) = service_tier {
body["service_tier"] = json!(service_tier);
}
finalize_openai_provider_request_with_codex_model_capabilities(
&mut body,
OpenAiProviderRequestFinalization {
source_api_format,
provider_api_format,
provider_type: "codex",
provider_model: "gpt-5.6-sol",
source_model: "gpt-5.6-sol",
body_rules: None,
upstream_is_stream: true,
require_body_stream_field: true,
},
model_capabilities,
)
.expect("explicit service tiers should be validated by the upstream");
assert_eq!(
body.get("service_tier").and_then(Value::as_str),
service_tier,
"{source_api_format} -> {provider_api_format}",
);
}
}
}
}
}
#[test]
fn dynamic_codex_card_preserves_default_effort_and_keeps_mode_model_specific() {
let finalization = OpenAiProviderRequestFinalization {
@@ -91,12 +91,6 @@ impl CodexResponsesModelCapabilities {
.iter()
.any(|candidate| candidate == effort.trim())
}
fn supports_service_tier(&self, service_tier: &str) -> bool {
self.supported_service_tiers
.iter()
.any(|candidate| candidate == service_tier)
}
}
fn codex_namespaced_model_suffix(model: &str) -> Option<&str> {
@@ -1214,18 +1208,6 @@ fn apply_codex_model_request_capabilities(
}
}
}
if !body_rules_handle_path(body_rules, "service_tier") {
let service_tier = body_object
.get("service_tier")
.and_then(Value::as_str)
.map(str::to_string);
if !service_tier.as_deref().is_some_and(|service_tier| {
service_tier != "default" && capabilities.supports_service_tier(service_tier)
}) {
body_object.remove("service_tier");
}
}
}
fn ensure_codex_reasoning_defaults(
@@ -2654,7 +2636,7 @@ mod tests {
assert_eq!(body["reasoning"]["effort"], "high");
assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
assert_eq!(body["parallel_tool_calls"], false);
assert!(body.get("service_tier").is_none());
assert_eq!(body["service_tier"], "priority");
assert!(body["text"].get("verbosity").is_none());
assert_eq!(body["text"]["format"]["type"], "json_schema");
@@ -8,8 +8,8 @@
: undefined"
>
<div
class="flex min-w-0 max-w-full items-center gap-1"
:class="[modelRowClass, actualModel ? 'flex-wrap' : '']"
class="flex min-w-0 max-w-full flex-wrap items-center gap-1"
:class="modelRowClass"
>
<span
class="min-w-0 truncate"
@@ -29,7 +29,7 @@
:data-usage-model-badge="badge.key"
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
:variant="badge.variant"
class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4"
class="h-4 max-w-full shrink-0 truncate rounded-full px-1.5 text-[10px] leading-4"
:class="badge.className"
:title="badge.title"
:aria-label="badge.ariaLabel"
@@ -51,7 +51,7 @@
:data-usage-model-badge="badge.key"
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
:variant="badge.variant"
class="h-4 shrink-0 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4"
class="h-4 max-w-full shrink-0 truncate rounded-full px-1.5 text-[10px] leading-4"
:class="badge.className"
:title="badge.title"
:aria-label="badge.ariaLabel"
@@ -69,7 +69,7 @@ import { Badge } from '@/components/ui'
import { isCyberPolicyError } from '../utils/cyberError'
import { formatServiceTierFact } from '../utils/service-tier'
type ModelBadgeKey = 'compact' | 'reasoning' | 'fast' | 'cyber' | 'reasoning_tokens'
type ModelBadgeKey = 'compact' | 'reasoning' | 'fast' | 'service-tier' | 'cyber' | 'reasoning_tokens'
interface ModelBadgePresentation {
key: ModelBadgeKey
@@ -154,14 +154,16 @@ const modelBadges = computed<ModelBadgePresentation[]>(() => {
})
}
if (props.showServiceTierBadge && formatServiceTierFact(props.record.service_tier) === 'Fast') {
const serviceTier = formatServiceTierFact(props.record.service_tier)
if (props.showServiceTierBadge && serviceTier
&& !['auto', 'default', 'standard'].includes(serviceTier.toLowerCase())) {
badges.push({
key: 'fast',
label: 'Fast',
key: serviceTier === 'Fast' ? 'fast' : 'service-tier',
label: serviceTier,
variant: 'outline-transparent',
className: 'text-blue-500 dark:text-blue-300',
title: '上游请求档位:Fast\n计费档位:Fast',
ariaLabel: '上游请求档位:Fast,计费档位:Fast',
title: `上游请求档位:${serviceTier}\n计费档位:${serviceTier}`,
ariaLabel: `上游请求档位:${serviceTier},计费档位:${serviceTier}`,
})
}
@@ -309,7 +309,11 @@ describe('RequestDetailDrawer settlement pricing', () => {
})
})
it('shows mapping, reasoning, Fast, and Cyber together in the model header', async () => {
it.each([
{ tier: 'priority', label: 'Fast', badgeKey: 'fast' },
{ tier: 'ultrafast', label: 'ultrafast', badgeKey: 'service-tier' },
{ tier: 'future-tier', label: 'future-tier', badgeKey: 'service-tier' },
])('shows mapping, reasoning, $tier, and Cyber together in the model header', async ({ tier, label, badgeKey }) => {
apiMocks.getRequestDetail.mockResolvedValue({
...buildEmbeddingDetail(),
id: 'usage-cyber-risk-demo',
@@ -322,13 +326,13 @@ describe('RequestDetailDrawer settlement pricing', () => {
model: 'gpt-5',
reasoning: { effort: 'xhigh' },
},
service_tier: 'priority',
service_tier: tier,
// A response-side tier must not be used for the Fast badge or billing.
actual_service_tier: 'default',
provider_request_body: {
model: 'gpt-5.1',
reasoning: { effort: 'max' },
service_tier: 'priority',
service_tier: tier,
},
status: 'failed',
status_code: 400,
@@ -369,10 +373,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
.toContain('gpt-5.1')
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
.toContain('xhigh -> max')
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
.toContain('Fast')
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent?.trim())
.toBe('Fast')
expect(document.body.querySelector(`[data-request-detail-model-badge="${badgeKey}"]`)?.textContent?.trim())
.toBe(label)
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
.toContain('Cyber')
const modelLayout = document.body.querySelector(
@@ -386,8 +388,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
.toBe(true)
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
.toContain('xhigh -> max')
expect(modelRow?.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
.toContain('Fast')
expect(modelRow?.querySelector(`[data-request-detail-model-badge="${badgeKey}"]`)?.textContent)
.toContain(label)
expect(modelRow?.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
.toContain('Cyber')
expect(modelLayout?.querySelector('[data-request-detail-model-badges-row]')).toBeNull()
@@ -395,7 +397,7 @@ describe('RequestDetailDrawer settlement pricing', () => {
expect([...serviceTierFacts?.querySelectorAll('dt') ?? []].map(node => node.textContent?.trim()))
.toEqual(['上游请求层级', '计费层级'])
expect([...serviceTierFacts?.querySelectorAll('dd') ?? []].map(node => node.textContent?.trim()))
.toEqual(['Fast', 'Fast'])
.toEqual([label, label])
})
})
@@ -560,6 +560,37 @@ describe('UsageRecordsTable', () => {
},
)
it.each(['ultrafast', 'flex', 'future-tier', ' UltraFast '])(
'shows the final provider request tier %s without a fixed badge allowlist',
(requested) => {
const root = mountUsageRecordsTable([buildRecord({
service_tier: requested,
actual_service_tier: 'default',
})])
const badges = [...root.querySelectorAll<HTMLElement>('[data-usage-model-badge="service-tier"]')]
expect(badges.length).toBeGreaterThan(0)
for (const badge of badges) {
expect(badge.textContent?.trim()).toBe(requested.trim())
expect(badge.title).toBe(`上游请求档位:${requested.trim()}\n计费档位:${requested.trim()}`)
expect(badge.getAttribute('aria-label')).toBe(
`上游请求档位:${requested.trim()},计费档位:${requested.trim()}`,
)
}
},
)
it.each(['auto', 'default', 'standard', ' DEFAULT ', '', ' ', null])(
'does not show a badge for the default or absent request tier %s',
(requested) => {
const root = mountUsageRecordsTable([buildRecord({
service_tier: requested,
actual_service_tier: 'ultrafast',
})])
expect(root.querySelector('[data-usage-model-badge="service-tier"]')).toBeNull()
expect(root.querySelector('[data-usage-model-badge="fast"]')).toBeNull()
},
)
it.each(['default', 'flex', null])(
'ignores the response-side tier %s when the request tier is Fast',
(actualServiceTier) => {