mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
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())]),
|
headers: BTreeMap::from([("content-type".into(), "application/json".into())]),
|
||||||
content_type: Some("application/json".into()),
|
content_type: Some("application/json".into()),
|
||||||
content_encoding: Some(encoding.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,
|
stream: false,
|
||||||
client_api_format: "openai:chat".into(),
|
client_api_format: "openai:chat".into(),
|
||||||
provider_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),
|
result.body.and_then(|body| body.json_body),
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"content_encoding": encoding,
|
"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,
|
trace_id: String,
|
||||||
url: String,
|
url: String,
|
||||||
model: String,
|
model: String,
|
||||||
|
service_tier: String,
|
||||||
content_encoding: String,
|
content_encoding: String,
|
||||||
stream: bool,
|
stream: bool,
|
||||||
accept: String,
|
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())
|
.and_then(|value| value.as_str())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.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
|
content_encoding: payload
|
||||||
.get("content_encoding")
|
.get("content_encoding")
|
||||||
.and_then(|value| value.as_str())
|
.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")
|
.header(TRACE_ID_HEADER, "trace-codex-cli-stream-local-123")
|
||||||
.body(
|
.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()
|
.send()
|
||||||
.await
|
.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"
|
"https://chatgpt.com/backend-api/codex/responses"
|
||||||
);
|
);
|
||||||
assert_eq!(seen_execution_runtime_request.model, "gpt-5.6-sol");
|
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_eq!(seen_execution_runtime_request.content_encoding, "zstd");
|
||||||
assert!(seen_execution_runtime_request.stream);
|
assert!(seen_execution_runtime_request.stream);
|
||||||
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
|
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ fn validate_final_openai_provider_request_contract(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
finalize_openai_provider_request,
|
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]
|
#[test]
|
||||||
fn dynamic_codex_card_preserves_default_effort_and_keeps_mode_model_specific() {
|
fn dynamic_codex_card_preserves_default_effort_and_keeps_mode_model_specific() {
|
||||||
let finalization = OpenAiProviderRequestFinalization {
|
let finalization = OpenAiProviderRequestFinalization {
|
||||||
|
|||||||
@@ -91,12 +91,6 @@ impl CodexResponsesModelCapabilities {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|candidate| candidate == effort.trim())
|
.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> {
|
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(
|
fn ensure_codex_reasoning_defaults(
|
||||||
@@ -2654,7 +2636,7 @@ mod tests {
|
|||||||
assert_eq!(body["reasoning"]["effort"], "high");
|
assert_eq!(body["reasoning"]["effort"], "high");
|
||||||
assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
|
assert_eq!(body["include"], json!(["reasoning.encrypted_content"]));
|
||||||
assert_eq!(body["parallel_tool_calls"], false);
|
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!(body["text"].get("verbosity").is_none());
|
||||||
assert_eq!(body["text"]["format"]["type"], "json_schema");
|
assert_eq!(body["text"]["format"]["type"], "json_schema");
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
: undefined"
|
: undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 max-w-full items-center gap-1"
|
class="flex min-w-0 max-w-full flex-wrap items-center gap-1"
|
||||||
:class="[modelRowClass, actualModel ? 'flex-wrap' : '']"
|
:class="modelRowClass"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="min-w-0 truncate"
|
class="min-w-0 truncate"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
:data-usage-model-badge="badge.key"
|
:data-usage-model-badge="badge.key"
|
||||||
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
|
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
|
||||||
:variant="badge.variant"
|
: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"
|
:class="badge.className"
|
||||||
:title="badge.title"
|
:title="badge.title"
|
||||||
:aria-label="badge.ariaLabel"
|
:aria-label="badge.ariaLabel"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
:data-usage-model-badge="badge.key"
|
:data-usage-model-badge="badge.key"
|
||||||
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
|
:data-request-detail-model-badge="context === 'detail' ? badge.key : undefined"
|
||||||
:variant="badge.variant"
|
: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"
|
:class="badge.className"
|
||||||
:title="badge.title"
|
:title="badge.title"
|
||||||
:aria-label="badge.ariaLabel"
|
:aria-label="badge.ariaLabel"
|
||||||
@@ -69,7 +69,7 @@ import { Badge } from '@/components/ui'
|
|||||||
import { isCyberPolicyError } from '../utils/cyberError'
|
import { isCyberPolicyError } from '../utils/cyberError'
|
||||||
import { formatServiceTierFact } from '../utils/service-tier'
|
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 {
|
interface ModelBadgePresentation {
|
||||||
key: ModelBadgeKey
|
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({
|
badges.push({
|
||||||
key: 'fast',
|
key: serviceTier === 'Fast' ? 'fast' : 'service-tier',
|
||||||
label: 'Fast',
|
label: serviceTier,
|
||||||
variant: 'outline-transparent',
|
variant: 'outline-transparent',
|
||||||
className: 'text-blue-500 dark:text-blue-300',
|
className: 'text-blue-500 dark:text-blue-300',
|
||||||
title: '上游请求档位:Fast\n计费档位:Fast',
|
title: `上游请求档位:${serviceTier}\n计费档位:${serviceTier}`,
|
||||||
ariaLabel: '上游请求档位:Fast,计费档位:Fast',
|
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({
|
apiMocks.getRequestDetail.mockResolvedValue({
|
||||||
...buildEmbeddingDetail(),
|
...buildEmbeddingDetail(),
|
||||||
id: 'usage-cyber-risk-demo',
|
id: 'usage-cyber-risk-demo',
|
||||||
@@ -322,13 +326,13 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
|||||||
model: 'gpt-5',
|
model: 'gpt-5',
|
||||||
reasoning: { effort: 'xhigh' },
|
reasoning: { effort: 'xhigh' },
|
||||||
},
|
},
|
||||||
service_tier: 'priority',
|
service_tier: tier,
|
||||||
// A response-side tier must not be used for the Fast badge or billing.
|
// A response-side tier must not be used for the Fast badge or billing.
|
||||||
actual_service_tier: 'default',
|
actual_service_tier: 'default',
|
||||||
provider_request_body: {
|
provider_request_body: {
|
||||||
model: 'gpt-5.1',
|
model: 'gpt-5.1',
|
||||||
reasoning: { effort: 'max' },
|
reasoning: { effort: 'max' },
|
||||||
service_tier: 'priority',
|
service_tier: tier,
|
||||||
},
|
},
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
status_code: 400,
|
status_code: 400,
|
||||||
@@ -369,10 +373,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
|||||||
.toContain('gpt-5.1')
|
.toContain('gpt-5.1')
|
||||||
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
expect(document.body.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||||||
.toContain('xhigh -> max')
|
.toContain('xhigh -> max')
|
||||||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
expect(document.body.querySelector(`[data-request-detail-model-badge="${badgeKey}"]`)?.textContent?.trim())
|
||||||
.toContain('Fast')
|
.toBe(label)
|
||||||
expect(document.body.querySelector('[data-request-detail-model-badge="fast"]')?.textContent?.trim())
|
|
||||||
.toBe('Fast')
|
|
||||||
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
expect(document.body.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||||||
.toContain('Cyber')
|
.toContain('Cyber')
|
||||||
const modelLayout = document.body.querySelector(
|
const modelLayout = document.body.querySelector(
|
||||||
@@ -386,8 +388,8 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
|||||||
.toBe(true)
|
.toBe(true)
|
||||||
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
expect(modelRow?.querySelector('[data-request-detail-model-badge="reasoning"]')?.textContent)
|
||||||
.toContain('xhigh -> max')
|
.toContain('xhigh -> max')
|
||||||
expect(modelRow?.querySelector('[data-request-detail-model-badge="fast"]')?.textContent)
|
expect(modelRow?.querySelector(`[data-request-detail-model-badge="${badgeKey}"]`)?.textContent)
|
||||||
.toContain('Fast')
|
.toContain(label)
|
||||||
expect(modelRow?.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
expect(modelRow?.querySelector('[data-request-detail-model-badge="cyber"]')?.textContent)
|
||||||
.toContain('Cyber')
|
.toContain('Cyber')
|
||||||
expect(modelLayout?.querySelector('[data-request-detail-model-badges-row]')).toBeNull()
|
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()))
|
expect([...serviceTierFacts?.querySelectorAll('dt') ?? []].map(node => node.textContent?.trim()))
|
||||||
.toEqual(['上游请求层级', '计费层级'])
|
.toEqual(['上游请求层级', '计费层级'])
|
||||||
expect([...serviceTierFacts?.querySelectorAll('dd') ?? []].map(node => node.textContent?.trim()))
|
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])(
|
it.each(['default', 'flex', null])(
|
||||||
'ignores the response-side tier %s when the request tier is Fast',
|
'ignores the response-side tier %s when the request tier is Fast',
|
||||||
(actualServiceTier) => {
|
(actualServiceTier) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user