feat(openai): align GPT-5.6 and Codex request contracts

This commit is contained in:
MMEXA
2026-07-11 07:40:12 +08:00
parent bc1da3bf3f
commit dfa121dd5b
178 changed files with 17947 additions and 3541 deletions
+208 -4
View File
@@ -1,8 +1,8 @@
use super::{
hash_api_key, sample_models_candidate_row, unrestricted_models_snapshot,
InMemoryAuthApiKeySnapshotRepository, InMemoryMinimalCandidateSelectionReadRepository,
InMemoryVideoTaskRepository, UpsertVideoTask, VideoTaskLookupKey, VideoTaskReadRepository,
VideoTaskStatus, VideoTaskWriteRepository, DEVELOPMENT_ENCRYPTION_KEY,
InMemoryVideoTaskRepository, StoredAuthApiKeySnapshot, UpsertVideoTask, VideoTaskLookupKey,
VideoTaskReadRepository, VideoTaskStatus, VideoTaskWriteRepository, DEVELOPMENT_ENCRYPTION_KEY,
};
use crate::image_capabilities::openai_image_gateway_max_generation_count;
use crate::tests::{
@@ -26,6 +26,95 @@ use std::collections::HashMap;
use std::future::pending;
use std::sync::atomic::{AtomicBool, Ordering};
fn codex_models_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
StoredAuthApiKeySnapshot::new(
user_id.to_string(),
"alice".to_string(),
Some("[email protected]".to_string()),
"user".to_string(),
"local".to_string(),
true,
false,
Some(json!(["codex"])),
Some(json!(["openai:responses"])),
Some(json!(["frontier-sol", "broken-luna"])),
api_key_id.to_string(),
Some("codex-models".to_string()),
true,
false,
false,
Some(10),
Some(5),
Some(4_102_444_800),
Some(json!(["codex"])),
Some(json!(["openai:responses"])),
Some(json!(["frontier-sol", "broken-luna"])),
)
.expect("Codex models auth snapshot should build")
}
fn sample_codex_models_candidate_row(
provider_id: &str,
global_model_name: &str,
source_model_name: &str,
) -> StoredMinimalCandidateSelectionRow {
let mut row = sample_models_candidate_row(
provider_id,
"codex",
"openai:responses",
global_model_name,
10,
);
row.provider_type = "codex".to_string();
row.key_auth_type = "oauth".to_string();
row.model_provider_model_name = source_model_name.to_string();
row.model_provider_model_mappings = Some(vec![
aether_data_contracts::repository::candidate_selection::StoredProviderModelMapping {
name: source_model_name.to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
},
]);
row
}
fn complete_codex_model_card(source_model_name: &str) -> serde_json::Value {
json!({
"id": source_model_name,
"api_formats": ["openai:responses"],
"slug": source_model_name,
"display_name": "GPT-5.6-Sol",
"description": "Frontier coding model",
"default_reasoning_level": "low",
"supported_reasoning_levels": [
{"effort": "low", "description": "Low"},
{"effort": "medium", "description": "Medium"},
{"effort": "high", "description": "High"},
{"effort": "xhigh", "description": "XHigh"},
{"effort": "max", "description": "Max"},
{"effort": "ultra", "description": "Ultra"}
],
"shell_type": "shell_command",
"visibility": "list",
"supported_in_api": true,
"priority": 1,
"availability_nux": null,
"upgrade": null,
"base_instructions": "Use the current Codex instructions.",
"model_messages": null,
"supports_reasoning_summaries": true,
"support_verbosity": true,
"default_verbosity": "low",
"apply_patch_tool_type": "freeform",
"truncation_policy": {"mode": "tokens", "limit": 10000},
"supports_parallel_tool_calls": true,
"experimental_supported_tools": [],
"minimal_client_version": "0.144.0",
"future_capability": {"enabled": true}
})
}
fn gemini_operation_status_label(status: VideoTaskStatus) -> &'static str {
match status {
VideoTaskStatus::Pending => "Pending",
@@ -360,6 +449,121 @@ async fn gateway_handles_public_openai_models_without_hitting_fallback_probe() {
fallback_probe_handle.abort();
}
#[tokio::test]
async fn gateway_serves_codex_model_cards_for_versioned_models_requests() {
let codex_row =
sample_codex_models_candidate_row("provider-codex-models", "frontier-sol", "gpt-5.6-sol");
let incomplete_codex_row = sample_codex_models_candidate_row(
"provider-codex-incomplete",
"broken-luna",
"gpt-5.6-luna",
);
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
codex_row.clone(),
incomplete_codex_row.clone(),
sample_models_candidate_row(
"provider-openai-responses",
"openai",
"openai:responses",
"custom-responses-model",
20,
),
]));
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![
(
Some(hash_api_key("sk-codex-models")),
codex_models_snapshot("key-codex-models", "user-codex-models"),
),
(
Some(hash_api_key("sk-standard-models")),
unrestricted_models_snapshot("key-standard-models", "user-standard-models"),
),
]));
let state = AppState::new()
.expect("gateway should build")
.with_data_state_for_tests(
crate::data::GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
candidate_repository,
auth_repository,
),
);
state
.runtime_kv_setex(
&format!(
"upstream_models:{}:{}",
codex_row.provider_id, codex_row.key_id
),
&serde_json::to_string(&vec![complete_codex_model_card("gpt-5.6-sol")])
.expect("model cache should serialize"),
60,
)
.await
.expect("model cache should seed");
state
.runtime_kv_setex(
&format!(
"upstream_models:{}:{}",
incomplete_codex_row.provider_id, incomplete_codex_row.key_id
),
&serde_json::to_string(&vec![json!({
"id": "gpt-5.6-luna",
"slug": "gpt-5.6-luna",
"display_name": "GPT-5.6-Luna"
})])
.expect("incomplete model cache should serialize"),
60,
)
.await
.expect("incomplete model cache should seed");
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let codex_response = client
.get(format!("{gateway_url}/v1/models?client_version=0.144.1"))
.header("authorization", "Bearer sk-codex-models")
.send()
.await
.expect("Codex models request should succeed");
assert_eq!(codex_response.status(), StatusCode::OK);
let codex_payload: serde_json::Value = codex_response
.json()
.await
.expect("Codex models body should parse");
assert_eq!(codex_payload["models"].as_array().map(Vec::len), Some(1));
assert_eq!(codex_payload["models"][0]["slug"], "frontier-sol");
assert_eq!(
codex_payload["models"][0]["supported_reasoning_levels"][5]["effort"],
"ultra"
);
assert_eq!(
codex_payload["models"][0]["future_capability"],
json!({"enabled": true})
);
assert!(codex_payload["models"][0].get("id").is_none());
assert!(codex_payload["models"][0].get("api_formats").is_none());
assert!(codex_payload.get("object").is_none());
let standard_response = client
.get(format!("{gateway_url}/v1/models"))
.header("authorization", "Bearer sk-standard-models")
.send()
.await
.expect("standard models request should succeed");
assert_eq!(standard_response.status(), StatusCode::OK);
let standard_payload: serde_json::Value = standard_response
.json()
.await
.expect("standard models body should parse");
assert_eq!(standard_payload["object"], "list");
assert!(standard_payload["data"].is_array());
assert!(standard_payload.get("models").is_none());
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_openai_models_list_drops_disabled_global_model_after_cache_invalidation() {
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
@@ -1212,7 +1416,7 @@ async fn gateway_does_not_locally_reject_image_model_name_on_chat_completions()
}
#[tokio::test]
async fn gateway_rejects_image_request_with_n_greater_than_four_without_hitting_fallback_probe() {
async fn gateway_rejects_image_request_above_gateway_limit_without_hitting_fallback_probe() {
let fallback_probe_hits = Arc::new(Mutex::new(0usize));
let fallback_probe_hits_clone = Arc::clone(&fallback_probe_hits);
let fallback_probe = Router::new().route(
@@ -1247,7 +1451,7 @@ async fn gateway_rejects_image_request_with_n_greater_than_four_without_hitting_
serde_json::to_vec(&json!({
"model": "grok-imagine-image-lite",
"prompt": "draw",
"n": 5,
"n": openai_image_gateway_max_generation_count() + 1,
"response_format": "b64_json"
}))
.expect("request body should encode"),
@@ -1,8 +1,9 @@
use super::{
hash_api_key, sample_endpoint, sample_key, sample_models_candidate_row, sample_provider,
unrestricted_models_snapshot, InMemoryAuthApiKeySnapshotRepository,
InMemoryMinimalCandidateSelectionReadRepository, InMemoryProviderCatalogReadRepository,
InMemoryRequestCandidateRepository, DEVELOPMENT_ENCRYPTION_KEY,
hash_api_key, run_frontdoor_async_test, sample_endpoint, sample_key,
sample_models_candidate_row, sample_provider, unrestricted_models_snapshot,
InMemoryAuthApiKeySnapshotRepository, InMemoryMinimalCandidateSelectionReadRepository,
InMemoryProviderCatalogReadRepository, InMemoryRequestCandidateRepository,
DEVELOPMENT_ENCRYPTION_KEY,
};
use crate::tests::{
any, build_router, build_router_with_state, build_state_with_execution_runtime_override, json,
@@ -160,8 +161,15 @@ async fn gateway_returns_internal_gateway_plan_sync_proxy_public_action_without_
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_internal_gateway_execute_sync_locally() {
#[test]
fn gateway_handles_internal_gateway_execute_sync_locally() {
run_frontdoor_async_test(
"gateway_handles_internal_gateway_execute_sync_locally",
gateway_handles_internal_gateway_execute_sync_locally_impl(),
);
}
async fn gateway_handles_internal_gateway_execute_sync_locally_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let fallback_probe = Router::new().route(
@@ -5735,7 +5735,7 @@ async fn gateway_handles_users_me_usage_locally_without_proxying_upstream() {
payload["records"][0]["cache_creation_ephemeral_5m_input_tokens"],
4
);
assert_eq!(payload["records"][0]["effective_input_tokens"], 105);
assert_eq!(payload["records"][0]["effective_input_tokens"], 95);
assert_eq!(
payload["records"][0]["cache_creation_ephemeral_1h_input_tokens"],
6
@@ -5762,10 +5762,7 @@ async fn gateway_handles_users_me_usage_locally_without_proxying_upstream() {
payload["summary_by_model"][0]["cache_creation_ephemeral_1h_tokens"],
6
);
assert_eq!(
payload["summary_by_model"][0]["effective_input_tokens"],
105
);
assert_eq!(payload["summary_by_model"][0]["effective_input_tokens"], 95);
assert_eq!(payload["summary_by_model"][0]["total_input_context"], 120);
assert!(payload.get("summary_by_provider").is_none());
assert_eq!(payload["billing"]["id"], "wallet-auth-1");
@@ -191,7 +191,7 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["today"]["requests"], 1);
assert_eq!(payload["today"]["tokens"], 160);
assert_eq!(payload["today"]["tokens"], 150);
assert_eq!(payload["api_keys"]["total"], 2);
assert_eq!(payload["api_keys"]["active"], 1);
assert_eq!(payload["stats"][3]["subValue"], json!("输入 240 / 输出 60"));
@@ -646,7 +646,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["today"]["requests"], 2);
assert_eq!(payload["today"]["tokens"], 17_450);
assert_eq!(payload["today"]["tokens"], 16_250);
assert_eq!(payload["today"]["cost"], json!(2.5));
assert_eq!(payload["cost_stats"]["cost_savings"], json!(0.025));
let stats = payload["stats"].as_array().expect("stats should be array");
@@ -664,10 +664,10 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
.iter()
.find(|item| item["name"] == json!("今日 Token"))
.expect("today token stats card should exist");
assert_eq!(today_token_stats["value"], json!("17.4K"));
assert_eq!(today_token_stats["value"], json!("16.2K"));
assert_eq!(
today_token_stats["subValue"],
json!("输入 12.1K / 输出 3.1K · 写缓存 1.25K / 读缓存 1K")
json!("输入 10.9K / 输出 3.1K · 写缓存 1.25K / 读缓存 1K")
);
assert_eq!(payload["users"]["total"], 2);
assert_eq!(payload["users"]["active"], 1);