mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-03 16:07:46 +08:00
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:
@@ -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}`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+12
-10
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user