diff --git a/apps/aether-gateway/src/execution_runtime/transport.rs b/apps/aether-gateway/src/execution_runtime/transport.rs index 5cfeb3cf4..c5c52ae26 100644 --- a/apps/aether-gateway/src/execution_runtime/transport.rs +++ b/apps/aether-gateway/src/execution_runtime/transport.rs @@ -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"}, })) ); } diff --git a/apps/aether-gateway/src/tests/ai_execute/stream_cli/direct.rs b/apps/aether-gateway/src/tests/ai_execute/stream_cli/direct.rs index 8ad2bb958..f4224aced 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream_cli/direct.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream_cli/direct.rs @@ -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"); diff --git a/crates/aether-ai/formats/src/formats/openai/request_contract.rs b/crates/aether-ai/formats/src/formats/openai/request_contract.rs index 771f8a184..8805cd279 100644 --- a/crates/aether-ai/formats/src/formats/openai/request_contract.rs +++ b/crates/aether-ai/formats/src/formats/openai/request_contract.rs @@ -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 { diff --git a/crates/aether-ai/formats/src/formats/openai/responses/codex.rs b/crates/aether-ai/formats/src/formats/openai/responses/codex.rs index 919b7f659..9af15f2d5 100644 --- a/crates/aether-ai/formats/src/formats/openai/responses/codex.rs +++ b/crates/aether-ai/formats/src/formats/openai/responses/codex.rs @@ -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"); diff --git a/frontend/src/features/usage/components/UsageModelDisplay.vue b/frontend/src/features/usage/components/UsageModelDisplay.vue index 123d3094f..7c316b5dd 100644 --- a/frontend/src/features/usage/components/UsageModelDisplay.vue +++ b/frontend/src/features/usage/components/UsageModelDisplay.vue @@ -8,8 +8,8 @@ : undefined" >
(() => { }) } - 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}`, }) } diff --git a/frontend/src/features/usage/components/__tests__/RequestDetailDrawer.pricing.spec.ts b/frontend/src/features/usage/components/__tests__/RequestDetailDrawer.pricing.spec.ts index e72abbea3..dabe45e2e 100644 --- a/frontend/src/features/usage/components/__tests__/RequestDetailDrawer.pricing.spec.ts +++ b/frontend/src/features/usage/components/__tests__/RequestDetailDrawer.pricing.spec.ts @@ -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]) }) }) diff --git a/frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts b/frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts index b3dfe1896..cfd1c2b06 100644 --- a/frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts +++ b/frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts @@ -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('[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) => {