mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat(openai): align GPT-5.6 and Codex request contracts
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user