mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
Merge remote-tracking branch 'origin/main' into codex/pool-key-bulk-management-20260714
# Conflicts: # apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs # frontend/src/api/endpoints/pool.ts
This commit is contained in:
@@ -138,6 +138,7 @@ async fn gateway_executes_openai_chat_sync_upstream_stream_via_local_finalize_re
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -637,6 +638,7 @@ async fn gateway_executes_openai_chat_cross_format_upstream_stream_via_local_fin
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1082,6 +1084,7 @@ async fn gateway_executes_openai_chat_cross_format_tool_use_upstream_stream_via_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1564,6 +1567,7 @@ async fn gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_fi
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2074,6 +2078,7 @@ async fn gateway_executes_openai_chat_cross_format_claude_upstream_sync_via_loca
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2433,6 +2438,7 @@ async fn gateway_executes_openai_chat_cross_format_gemini_upstream_sync_via_loca
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -113,6 +113,7 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses:compact".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -113,6 +113,7 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -586,6 +587,7 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1071,6 +1073,7 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -135,6 +135,7 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -650,6 +651,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -131,6 +131,7 @@ async fn gateway_executes_claude_chat_sync_same_format_via_local_finalize_respon
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -587,6 +588,7 @@ async fn gateway_executes_claude_chat_sync_upstream_stream_via_local_finalize_re
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1048,6 +1050,7 @@ async fn gateway_executes_claude_cli_sync_upstream_stream_via_local_finalize_res
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -136,6 +136,7 @@ async fn gateway_executes_gemini_chat_sync_same_format_via_local_finalize_respon
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -640,6 +641,7 @@ async fn gateway_executes_gemini_chat_sync_upstream_stream_via_local_finalize_re
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1134,6 +1136,7 @@ async fn gateway_executes_gemini_cli_sync_upstream_stream_via_local_finalize_res
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1642,6 +1645,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_upstream_stream_via_local_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -111,6 +111,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -131,6 +131,7 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -463,8 +464,8 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
|
||||
accept: String,
|
||||
authorization: String,
|
||||
x_client_request_id: String,
|
||||
session_id: String,
|
||||
conversation_id: String,
|
||||
codex_session_id: String,
|
||||
codex_thread_id: String,
|
||||
instructions: String,
|
||||
user_text: String,
|
||||
prompt_cache_key: String,
|
||||
@@ -535,6 +536,7 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -742,15 +744,15 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
session_id: payload
|
||||
codex_session_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("session_id"))
|
||||
.and_then(|value| value.get("session-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
conversation_id: payload
|
||||
codex_thread_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("conversation_id"))
|
||||
.and_then(|value| value.get("thread-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
@@ -889,19 +891,16 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"trace-openai-chat-cli-local-123"
|
||||
seen_execution_runtime_request.codex_thread_id
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.prompt_cache_key,
|
||||
"bc749eb7-a9e2-5793-8d14-abd659c700b0"
|
||||
seen_execution_runtime_request.codex_session_id,
|
||||
seen_execution_runtime_request.codex_thread_id
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.session_id,
|
||||
"d1e9b802644e1f52"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.conversation_id,
|
||||
"d1e9b802644e1f52"
|
||||
assert!(seen_execution_runtime_request.prompt_cache_key.is_empty());
|
||||
assert_ne!(
|
||||
seen_execution_runtime_request.codex_thread_id,
|
||||
seen_execution_runtime_request.trace_id
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.instructions,
|
||||
@@ -1023,6 +1022,7 @@ async fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candi
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1461,6 +1461,7 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1964,6 +1965,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::{
|
||||
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
|
||||
to_bytes, Arc, Body, Json, Mutex, Request, Router, StatusCode, TRACE_ID_HEADER,
|
||||
};
|
||||
use crate::ai_serving::CODEX_OPENAI_IMAGE_INTERNAL_MODEL;
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
@@ -55,13 +54,9 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
struct SeenExecutionRuntimeStreamRequest {
|
||||
trace_id: String,
|
||||
url: String,
|
||||
model: String,
|
||||
authorization: String,
|
||||
x_client_request_id: String,
|
||||
tool_type: String,
|
||||
tool_action: String,
|
||||
tool_partial_images: Option<u64>,
|
||||
request_stream: bool,
|
||||
headers: serde_json::Value,
|
||||
body: serde_json::Value,
|
||||
plan_stream: bool,
|
||||
}
|
||||
|
||||
@@ -136,6 +131,7 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -180,7 +176,7 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(serde_json::json!({"upstream_stream_policy":"force_stream"})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -272,65 +268,26 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
model: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("model"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
authorization: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("authorization"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
x_client_request_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-client-request-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_type: payload
|
||||
headers: payload.get("headers").cloned().unwrap_or_default(),
|
||||
body: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_action: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("action"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_partial_images: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("partial_images"))
|
||||
.and_then(|value| value.as_u64()),
|
||||
request_stream: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("stream"))
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
plan_stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
});
|
||||
let frames = concat!(
|
||||
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"text/event-stream\"}}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.output_item.done\\ndata: {\\\"type\\\":\\\"response.output_item.done\\\",\\\"output_index\\\":0,\\\"item\\\":{\\\"id\\\":\\\"ig_123\\\",\\\"type\\\":\\\"image_generation_call\\\",\\\"result\\\":\\\"aGVsbG8=\\\"}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.completed\\ndata: {\\\"type\\\":\\\"response.completed\\\",\\\"response\\\":{\\\"tool_usage\\\":{\\\"image_gen\\\":{\\\"input_tokens\\\":11,\\\"output_tokens\\\":22,\\\"total_tokens\\\":33}}}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"application/json\"}}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"{\\\"created\\\":1776991097,\\\"data\\\":[{\\\"b64_json\\\":\\\"aGVsbG8=\\\",\\\"revised_prompt\\\":\\\"水墨视觉海报\\\"}],\\\"usage\\\":{\\\"input_tokens\\\":11,\\\"output_tokens\\\":22,\\\"total_tokens\\\":33}}\"}}\n",
|
||||
"{\"type\":\"telemetry\",\"payload\":{\"kind\":\"telemetry\",\"telemetry\":{\"elapsed_ms\":41}}}\n",
|
||||
"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n"
|
||||
);
|
||||
@@ -397,26 +354,28 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-codex-image-stream-local-123")
|
||||
.body(
|
||||
"{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张中国历史视觉海报\",\"stream\":true,\"partial_images\":1}",
|
||||
"{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张水墨视觉海报\",\"background\":\"auto\",\"quality\":\"auto\",\"size\":\"auto\",\"stream\":true,\"response_format\":\"b64_json\"}",
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("text/event-stream")
|
||||
);
|
||||
let response_status = response.status();
|
||||
let response_content_type = response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_string);
|
||||
let response_text = response.text().await.expect("body should read");
|
||||
assert!(response_text.contains("event: image_generation.partial_image"));
|
||||
assert!(response_text.contains("\"type\":\"image_generation.partial_image\""));
|
||||
assert!(response_text.contains("\"b64_json\":\"aGVsbG8=\""));
|
||||
assert_eq!(response_status, StatusCode::OK, "{response_text}");
|
||||
assert_eq!(
|
||||
response_content_type.as_deref(),
|
||||
Some("text/event-stream"),
|
||||
"{response_text}"
|
||||
);
|
||||
assert!(response_text.contains("event: image_generation.completed"));
|
||||
assert!(response_text.contains("\"type\":\"image_generation.completed\""));
|
||||
assert!(response_text.contains("\"b64_json\":\"aGVsbG8=\""));
|
||||
assert!(response_text.contains("\"total_tokens\":33"));
|
||||
assert!(!response_text.contains("response.completed"));
|
||||
|
||||
@@ -444,25 +403,34 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.url,
|
||||
"https://chatgpt.com/backend-api/codex/responses"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.model,
|
||||
CODEX_OPENAI_IMAGE_INTERNAL_MODEL
|
||||
"https://chatgpt.com/backend-api/codex/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.authorization,
|
||||
"Bearer refreshed-codex-image-stream-access-token"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"trace-codex-image-stream-local-123"
|
||||
seen_execution_runtime_request.body,
|
||||
json!({
|
||||
"prompt": "生成一张水墨视觉海报",
|
||||
"background": "auto",
|
||||
"model": "gpt-image-2",
|
||||
"quality": "auto",
|
||||
"size": "auto"
|
||||
})
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.tool_type, "image_generation");
|
||||
assert_eq!(seen_execution_runtime_request.tool_action, "generate");
|
||||
assert_eq!(seen_execution_runtime_request.tool_partial_images, Some(1));
|
||||
assert!(seen_execution_runtime_request.request_stream);
|
||||
assert!(seen_execution_runtime_request.plan_stream);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.headers["user-agent"],
|
||||
"codex_cli_rs/0.144.1"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.headers["originator"],
|
||||
"codex_cli_rs"
|
||||
);
|
||||
for header in ["x-client-request-id", "session-id", "thread-id"] {
|
||||
assert!(seen_execution_runtime_request.headers.get(header).is_none());
|
||||
}
|
||||
assert!(!seen_execution_runtime_request.plan_stream);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
@@ -550,6 +518,7 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -594,7 +563,7 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(serde_json::json!({"upstream_stream_policy":"force_stream"})),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
@@ -772,8 +741,8 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
|
||||
seen_execution_runtime_request.trace_id,
|
||||
"trace-codex-image-stream-json-123"
|
||||
);
|
||||
assert!(seen_execution_runtime_request.request_stream);
|
||||
assert!(seen_execution_runtime_request.plan_stream);
|
||||
assert!(!seen_execution_runtime_request.request_stream);
|
||||
assert!(!seen_execution_runtime_request.plan_stream);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
@@ -873,6 +842,7 @@ fn image_bridge_candidate_row(
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(false),
|
||||
model_is_active: true,
|
||||
@@ -1152,12 +1122,14 @@ async fn gateway_routes_openai_responses_stream_image_intent_to_openai_image_pla
|
||||
seen_plan.url,
|
||||
"https://images.example.com/v1/images/generations"
|
||||
);
|
||||
assert!(seen_plan.plan_stream);
|
||||
assert!(!seen_plan.plan_stream);
|
||||
assert_eq!(seen_plan.auth_header, "Bearer sk-upstream-image-bridge");
|
||||
assert_eq!(seen_plan.body_json["stream"], true);
|
||||
assert_eq!(seen_plan.body_json["input"], "Draw a mountain observatory");
|
||||
assert_eq!(seen_plan.body_json["tools"][0]["type"], "image_generation");
|
||||
assert_eq!(seen_plan.body_json["tools"][0]["size"], "1024x1024");
|
||||
assert_eq!(seen_plan.body_json["model"], "gpt-image-2");
|
||||
assert_eq!(seen_plan.body_json["prompt"], "Draw a mountain observatory");
|
||||
assert_eq!(seen_plan.body_json["size"], "1024x1024");
|
||||
assert!(seen_plan.body_json.get("stream").is_none());
|
||||
assert!(seen_plan.body_json.get("input").is_none());
|
||||
assert!(seen_plan.body_json.get("tools").is_none());
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
|
||||
@@ -150,6 +150,7 @@ fn candidate_row() -> StoredMinimalCandidateSelectionRow {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: Some(vec!["endpoint-ai-execute-stream-pii-redaction".to_string()]),
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
any, build_router_with_state, build_state_with_execution_runtime_override, json,
|
||||
run_stream_cli_test, start_server, to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue,
|
||||
Infallible, Json, Mutex, Request, Response, Router, StatusCode,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
|
||||
};
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::auth::{
|
||||
@@ -23,33 +23,37 @@ use aether_data_contracts::repository::provider_catalog::{
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[test]
|
||||
fn gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision(
|
||||
) {
|
||||
fn gateway_executes_openai_responses_compact_as_unary_request() {
|
||||
run_stream_cli_test(
|
||||
"gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision",
|
||||
gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision_impl,
|
||||
"gateway_executes_openai_responses_compact_as_unary_request",
|
||||
gateway_executes_openai_responses_compact_as_unary_request_impl,
|
||||
);
|
||||
}
|
||||
|
||||
async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision_impl(
|
||||
) {
|
||||
async fn gateway_executes_openai_responses_compact_as_unary_request_impl() {
|
||||
#[derive(Debug, Clone)]
|
||||
struct SeenExecutionRuntimeStreamRequest {
|
||||
trace_id: String,
|
||||
url: String,
|
||||
model: String,
|
||||
content_encoding: String,
|
||||
stream: bool,
|
||||
accept: String,
|
||||
turn_state: String,
|
||||
authorization: String,
|
||||
chatgpt_account_id: String,
|
||||
fedramp: String,
|
||||
responses_lite: String,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
x_client_request_id_present: bool,
|
||||
endpoint_tag: String,
|
||||
conditional_header: String,
|
||||
renamed_header: String,
|
||||
dropped_header_present: bool,
|
||||
metadata_mode: String,
|
||||
metadata_source: String,
|
||||
metadata_origin: String,
|
||||
instructions: String,
|
||||
store_present: bool,
|
||||
body: serde_json::Value,
|
||||
proxy_node_id: String,
|
||||
transport_profile_id: String,
|
||||
}
|
||||
@@ -71,7 +75,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
false,
|
||||
Some(serde_json::json!(["openai"])),
|
||||
Some(serde_json::json!(["openai:responses:compact"])),
|
||||
Some(serde_json::json!(["gpt-5"])),
|
||||
Some(serde_json::json!(["gpt-5.6-sol"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
@@ -82,7 +86,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
Some(4_102_444_800_i64),
|
||||
Some(serde_json::json!(["openai"])),
|
||||
Some(serde_json::json!(["openai:responses:compact"])),
|
||||
Some(serde_json::json!(["gpt-5"])),
|
||||
Some(serde_json::json!(["gpt-5.6-sol"])),
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
@@ -91,7 +95,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
StoredMinimalCandidateSelectionRow {
|
||||
provider_id: "provider-openai-compact-local-1".to_string(),
|
||||
provider_name: "openai".to_string(),
|
||||
provider_type: "custom".to_string(),
|
||||
provider_type: "codex".to_string(),
|
||||
provider_priority: 10,
|
||||
provider_is_active: true,
|
||||
endpoint_id: "endpoint-openai-compact-local-1".to_string(),
|
||||
@@ -101,7 +105,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
endpoint_is_active: true,
|
||||
key_id: "key-openai-compact-local-1".to_string(),
|
||||
key_name: "prod".to_string(),
|
||||
key_auth_type: "bearer".to_string(),
|
||||
key_auth_type: "oauth".to_string(),
|
||||
key_is_active: true,
|
||||
key_api_formats: Some(vec!["openai:responses:compact".to_string()]),
|
||||
key_allowed_models: None,
|
||||
@@ -110,15 +114,16 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
key_global_priority_by_format: Some(serde_json::json!({"openai:responses:compact": 1})),
|
||||
model_id: "model-openai-compact-local-1".to_string(),
|
||||
global_model_id: "global-model-openai-compact-local-1".to_string(),
|
||||
global_model_name: "gpt-5".to_string(),
|
||||
global_model_name: "gpt-5.6-sol".to_string(),
|
||||
global_model_mappings: None,
|
||||
global_model_supports_streaming: Some(true),
|
||||
model_provider_model_name: "gpt-5-upstream".to_string(),
|
||||
model_provider_model_name: "deployment-production".to_string(),
|
||||
model_provider_model_mappings: Some(vec![StoredProviderModelMapping {
|
||||
name: "gpt-5-upstream".to_string(),
|
||||
name: "deployment-production".to_string(),
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses:compact".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -131,7 +136,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
"provider-openai-compact-local-1".to_string(),
|
||||
"openai".to_string(),
|
||||
Some("https://example.com".to_string()),
|
||||
"custom".to_string(),
|
||||
"codex".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
@@ -161,15 +166,12 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
"https://api.openai.example".to_string(),
|
||||
Some(serde_json::json!([
|
||||
{"action":"set","key":"x-endpoint-tag","value":"openai-compact-local"},
|
||||
{"action":"set","key":"x-conditional-tag","value":"header-condition-hit","condition":{"path":"instructions","op":"exists","source":"current"}},
|
||||
{"action":"set","key":"x-conditional-tag","value":"header-condition-hit","condition":{"path":"reasoning","op":"exists","source":"current"}},
|
||||
{"action":"rename","from":"x-client-rename","to":"x-upstream-rename"},
|
||||
{"action":"drop","key":"x-drop-me"}
|
||||
])),
|
||||
Some(serde_json::json!([
|
||||
{"action":"set","path":"instructions","value":"You are GPT-5.","condition":{"path":"instructions","op":"not_exists","source":"current"}},
|
||||
{"action":"set","path":"metadata.mode","value":"safe","condition":{"path":"metadata.mode","op":"not_exists","source":"current"}},
|
||||
{"action":"rename","from":"metadata.client","to":"metadata.source"},
|
||||
{"action":"set","path":"metadata.origin","value":"from-original","condition":{"path":"metadata.client","op":"exists","source":"original"}},
|
||||
{"action":"set","path":"instructions","value":"Use the configured tools.","condition":{"path":"instructions","op":"not_exists","source":"current"}},
|
||||
{"action":"drop","path":"store"}
|
||||
])),
|
||||
Some(2),
|
||||
@@ -186,7 +188,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
"key-openai-compact-local-1".to_string(),
|
||||
"provider-openai-compact-local-1".to_string(),
|
||||
"prod".to_string(),
|
||||
"bearer".to_string(),
|
||||
"oauth".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
@@ -198,7 +200,13 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
"sk-upstream-openai-compact",
|
||||
)
|
||||
.expect("api key should encrypt"),
|
||||
None,
|
||||
Some(
|
||||
encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"account_id":"acc-compact-local-123","is_fedramp":true}"#,
|
||||
)
|
||||
.expect("auth config should encrypt"),
|
||||
),
|
||||
None,
|
||||
Some(serde_json::json!({"openai:responses:compact": 1})),
|
||||
None,
|
||||
@@ -242,7 +250,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/gateway/decision-stream",
|
||||
"/api/internal/gateway/decision-sync",
|
||||
any(move |_request: Request| {
|
||||
let decision_hits_inner = Arc::clone(&decision_hits_clone);
|
||||
async move {
|
||||
@@ -252,7 +260,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/gateway/plan-stream",
|
||||
"/api/internal/gateway/plan-sync",
|
||||
any(move |_request: Request| {
|
||||
let plan_hits_inner = Arc::clone(&plan_hits_clone);
|
||||
async move {
|
||||
@@ -262,7 +270,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/gateway/report-stream",
|
||||
"/api/internal/gateway/report-sync",
|
||||
any(move |request: Request| {
|
||||
let seen_report_inner = Arc::clone(&seen_report_clone);
|
||||
async move {
|
||||
@@ -299,135 +307,166 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
);
|
||||
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/stream",
|
||||
"/v1/execute/sync",
|
||||
any(move |request: Request| {
|
||||
let seen_execution_runtime_inner = Arc::clone(&seen_execution_runtime_clone);
|
||||
async move {
|
||||
let (parts, body) = request.into_parts();
|
||||
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&raw_body).expect("execution runtime payload should parse");
|
||||
*seen_execution_runtime_inner.lock().expect("mutex should lock") =
|
||||
Some(SeenExecutionRuntimeStreamRequest {
|
||||
trace_id: parts
|
||||
.headers
|
||||
.get(TRACE_ID_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
url: payload
|
||||
.get("url")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
model: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("model"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
accept: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("accept"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
authorization: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("authorization"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
endpoint_tag: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-endpoint-tag"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
conditional_header: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-conditional-tag"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
renamed_header: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-upstream-rename"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
dropped_header_present: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-drop-me"))
|
||||
.is_some(),
|
||||
metadata_mode: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("metadata"))
|
||||
.and_then(|value| value.get("mode"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
metadata_source: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("metadata"))
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
metadata_origin: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("metadata"))
|
||||
.and_then(|value| value.get("origin"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
instructions: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("instructions"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
store_present: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("store"))
|
||||
.is_some(),
|
||||
proxy_node_id: payload
|
||||
.get("proxy")
|
||||
.and_then(|value| value.get("node_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
transport_profile_id: payload
|
||||
.get("transport_profile").and_then(|value| value.get("profile_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
});
|
||||
let stream = concat!(
|
||||
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"text/event-stream\"}}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.completed\\ndata: {\\\"type\\\":\\\"response.completed\\\",\\\"response\\\":{\\\"id\\\":\\\"resp-compact-local-123\\\",\\\"object\\\":\\\"response\\\",\\\"model\\\":\\\"gpt-5-upstream\\\",\\\"output\\\":[]}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"telemetry\",\"payload\":{\"kind\":\"telemetry\",\"telemetry\":{\"elapsed_ms\":41,\"ttfb_ms\":11}}}\n",
|
||||
"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n"
|
||||
);
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::from(stream))
|
||||
.expect("response should build");
|
||||
response.headers_mut().insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/x-ndjson"),
|
||||
);
|
||||
response
|
||||
let payload: serde_json::Value = serde_json::from_slice(&raw_body)
|
||||
.expect("execution runtime payload should parse");
|
||||
*seen_execution_runtime_inner
|
||||
.lock()
|
||||
.expect("mutex should lock") = Some(SeenExecutionRuntimeStreamRequest {
|
||||
trace_id: parts
|
||||
.headers
|
||||
.get(TRACE_ID_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
url: payload
|
||||
.get("url")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
model: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("model"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
content_encoding: payload
|
||||
.get("content_encoding")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
accept: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("accept"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
turn_state: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-codex-turn-state"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
authorization: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("authorization"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
chatgpt_account_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("chatgpt-account-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
fedramp: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-openai-fedramp"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
responses_lite: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-openai-internal-codex-responses-lite"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
session_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("session-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
thread_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("thread-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
x_client_request_id_present: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-client-request-id"))
|
||||
.is_some(),
|
||||
endpoint_tag: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-endpoint-tag"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
conditional_header: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-conditional-tag"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
renamed_header: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-upstream-rename"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
dropped_header_present: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-drop-me"))
|
||||
.is_some(),
|
||||
instructions: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("instructions"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
store_present: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("store"))
|
||||
.is_some(),
|
||||
body: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
proxy_node_id: payload
|
||||
.get("proxy")
|
||||
.and_then(|value| value.get("node_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
transport_profile_id: payload
|
||||
.get("transport_profile")
|
||||
.and_then(|value| value.get("profile_id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
});
|
||||
Json(json!({
|
||||
"request_id": "trace-openai-compact-local-123",
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"x-codex-turn-state": "turn-state-compact-123"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"output": [{
|
||||
"type": "compaction",
|
||||
"id": "cmp-compact-local-123",
|
||||
"encrypted_content": "encrypted-compact-history"
|
||||
}]
|
||||
}
|
||||
},
|
||||
"telemetry": {"elapsed_ms": 41, "ttfb_ms": 11}
|
||||
}))
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -474,23 +513,40 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
)
|
||||
.header("x-client-rename", "rename-openai-compact")
|
||||
.header("x-drop-me", "drop-openai-compact")
|
||||
.header("x-codex-turn-state", "turn-state-inbound-123")
|
||||
.header("session-id", "session-compact-local-123")
|
||||
.header("thread-id", "thread-compact-local-123")
|
||||
.header(TRACE_ID_HEADER, "trace-openai-compact-local-123")
|
||||
.body("{\"model\":\"gpt-5\",\"input\":\"hello\",\"stream\":true,\"metadata\":{\"client\":\"desktop-openai-compact\"},\"store\":false}")
|
||||
.body(r#"{"model":"gpt-5.6-sol","input":"hello","client_metadata":{"origin":"codex"},"include":["reasoning.encrypted_content"],"store":false,"stream":true,"stream_options":{"reasoning_summary_delivery":"sequential_cutoff"},"tool_choice":"auto","parallel_tool_calls":true,"reasoning":{"effort":"high"},"text":{"verbosity":"medium"},"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}],"prompt_cache_key":"session:compact-e2e"}"#)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
if response.status() != StatusCode::OK {
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body = response.text().await.expect("error body should read");
|
||||
panic!("Compact request failed: status={status}, headers={headers:?}, body={body}");
|
||||
}
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-codex-turn-state")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("turn-state-compact-123")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(EXECUTION_PATH_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some(EXECUTION_PATH_EXECUTION_RUNTIME_STREAM)
|
||||
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC)
|
||||
);
|
||||
let body: serde_json::Value = response.json().await.expect("body should parse");
|
||||
assert_eq!(
|
||||
body["output"][0]["encrypted_content"],
|
||||
"encrypted-compact-history"
|
||||
);
|
||||
let body = response.text().await.expect("body should read");
|
||||
assert!(body.contains("event: response.completed"));
|
||||
assert!(body.contains("\"model\":\"gpt-5-upstream\""));
|
||||
|
||||
let seen_execution_runtime_request = seen_execution_runtime
|
||||
.lock()
|
||||
@@ -505,13 +561,36 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
seen_execution_runtime_request.url,
|
||||
"https://api.openai.example/custom/v1/responses/compact"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.model, "gpt-5-upstream");
|
||||
assert!(seen_execution_runtime_request.stream);
|
||||
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.model,
|
||||
"deployment-production"
|
||||
);
|
||||
assert!(seen_execution_runtime_request.content_encoding.is_empty());
|
||||
assert!(!seen_execution_runtime_request.stream);
|
||||
assert_ne!(seen_execution_runtime_request.accept, "text/event-stream");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.turn_state,
|
||||
"turn-state-inbound-123"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.authorization,
|
||||
"Bearer sk-upstream-openai-compact"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.chatgpt_account_id,
|
||||
"acc-compact-local-123"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.fedramp, "true");
|
||||
assert_eq!(seen_execution_runtime_request.responses_lite, "true");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.session_id,
|
||||
"session-compact-local-123"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.thread_id,
|
||||
"thread-compact-local-123"
|
||||
);
|
||||
assert!(!seen_execution_runtime_request.x_client_request_id_present);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.endpoint_tag,
|
||||
"openai-compact-local"
|
||||
@@ -525,20 +604,61 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
"rename-openai-compact"
|
||||
);
|
||||
assert!(!seen_execution_runtime_request.dropped_header_present);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.instructions,
|
||||
"You are GPT-5."
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.metadata_mode, "safe");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.metadata_source,
|
||||
"desktop-openai-compact"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.metadata_origin,
|
||||
"from-original"
|
||||
);
|
||||
assert!(seen_execution_runtime_request.instructions.is_empty());
|
||||
assert!(!seen_execution_runtime_request.store_present);
|
||||
for field in [
|
||||
"client_metadata",
|
||||
"include",
|
||||
"store",
|
||||
"stream",
|
||||
"stream_options",
|
||||
"tool_choice",
|
||||
] {
|
||||
assert!(
|
||||
seen_execution_runtime_request.body.get(field).is_none(),
|
||||
"Compact request must omit {field}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["parallel_tool_calls"],
|
||||
json!(false)
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["reasoning"]["effort"],
|
||||
json!("high")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["text"]["verbosity"],
|
||||
json!("medium")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["reasoning"]["context"],
|
||||
json!("all_turns")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["input"][0]["type"],
|
||||
json!("additional_tools")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["input"][0]["tools"][0]["name"],
|
||||
json!("lookup")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["input"][1]["role"],
|
||||
json!("developer")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["input"][1]["content"][0]["text"],
|
||||
json!("Use the configured tools.")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["input"][2]["content"][0]["text"],
|
||||
json!("hello")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.body["prompt_cache_key"],
|
||||
json!("session:compact-e2e")
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.proxy_node_id,
|
||||
"proxy-node-openai-compact-local"
|
||||
@@ -558,7 +678,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
assert!(
|
||||
!*seen_report.lock().expect("mutex should lock"),
|
||||
"report-stream should stay local when request candidate persistence is available"
|
||||
"report-sync should stay local when request candidate persistence is available"
|
||||
);
|
||||
|
||||
assert_eq!(*decision_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
@@ -38,10 +38,25 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
trace_id: String,
|
||||
url: String,
|
||||
model: String,
|
||||
content_encoding: String,
|
||||
stream: bool,
|
||||
accept: String,
|
||||
authorization: String,
|
||||
chatgpt_account_id: String,
|
||||
fedramp: String,
|
||||
x_client_request_id: String,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
prompt_cache_key: String,
|
||||
responses_lite: String,
|
||||
has_top_level_tools: bool,
|
||||
has_top_level_instructions: bool,
|
||||
has_additional_tools: bool,
|
||||
parallel_tool_calls: bool,
|
||||
reasoning_effort: String,
|
||||
reasoning_context: String,
|
||||
has_compaction_trigger: bool,
|
||||
has_context_management: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -74,7 +89,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
false,
|
||||
Some(serde_json::json!(["openai", "codex"])),
|
||||
Some(serde_json::json!(["openai:responses"])),
|
||||
Some(serde_json::json!(["gpt-5.4"])),
|
||||
Some(serde_json::json!(["gpt-5.6-sol"])),
|
||||
api_key_id.to_string(),
|
||||
Some("default".to_string()),
|
||||
true,
|
||||
@@ -85,7 +100,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
Some(4_102_444_800_i64),
|
||||
Some(serde_json::json!(["openai", "codex"])),
|
||||
Some(serde_json::json!(["openai:responses"])),
|
||||
Some(serde_json::json!(["gpt-5.4"])),
|
||||
Some(serde_json::json!(["gpt-5.6-sol"])),
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
@@ -113,15 +128,16 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
key_global_priority_by_format: Some(serde_json::json!({"openai:responses": 1})),
|
||||
model_id: "model-codex-cli-stream-local-1".to_string(),
|
||||
global_model_id: "global-model-codex-cli-stream-local-1".to_string(),
|
||||
global_model_name: "gpt-5.4".to_string(),
|
||||
global_model_name: "gpt-5.6-sol".to_string(),
|
||||
global_model_mappings: None,
|
||||
global_model_supports_streaming: Some(true),
|
||||
model_provider_model_name: "gpt-5.4".to_string(),
|
||||
model_provider_model_name: "gpt-5.6-sol".to_string(),
|
||||
model_provider_model_mappings: Some(vec![StoredProviderModelMapping {
|
||||
name: "gpt-5.4".to_string(),
|
||||
name: "gpt-5.6-sol".to_string(),
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -176,7 +192,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
|
||||
let encrypted_auth_config = encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"provider_type":"codex","refresh_token":"rt-codex-stream-local-123"}"#,
|
||||
r#"{"provider_type":"codex","refresh_token":"rt-codex-stream-local-123","account_id":"acc-codex-stream-local-123","is_fedramp":true}"#,
|
||||
)
|
||||
.expect("auth config should encrypt");
|
||||
StoredProviderCatalogKey::new(
|
||||
@@ -367,6 +383,11 @@ 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(),
|
||||
content_encoding: payload
|
||||
.get("content_encoding")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
@@ -383,16 +404,110 @@ 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(),
|
||||
chatgpt_account_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("chatgpt-account-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
fedramp: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-openai-fedramp"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
x_client_request_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-client-request-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
session_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("session-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
thread_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("thread-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
prompt_cache_key: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("prompt_cache_key"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
responses_lite: payload
|
||||
.get("headers")
|
||||
.and_then(|value| {
|
||||
value.get("x-openai-internal-codex-responses-lite")
|
||||
})
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
has_top_level_tools: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.is_some_and(|body| body.get("tools").is_some()),
|
||||
has_top_level_instructions: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.is_some_and(|body| body.get("instructions").is_some()),
|
||||
has_additional_tools: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("input"))
|
||||
.and_then(|value| value.as_array())
|
||||
.and_then(|input| input.first())
|
||||
.and_then(|item| item.get("type"))
|
||||
.and_then(|value| value.as_str())
|
||||
== Some("additional_tools"),
|
||||
parallel_tool_calls: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("parallel_tool_calls"))
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(true),
|
||||
reasoning_effort: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("reasoning"))
|
||||
.and_then(|value| value.get("effort"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
reasoning_context: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("reasoning"))
|
||||
.and_then(|value| value.get("context"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
has_compaction_trigger: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("input"))
|
||||
.and_then(|value| value.as_array())
|
||||
.is_some_and(|input| {
|
||||
input.iter().any(|item| {
|
||||
item.get("type").and_then(|value| value.as_str())
|
||||
== Some("compaction_trigger")
|
||||
})
|
||||
}),
|
||||
has_context_management: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.is_some_and(|body| body.get("context_management").is_some()),
|
||||
});
|
||||
let frames = concat!(
|
||||
"{\"type\":\"headers\",\"payload\":{\"kind\":\"headers\",\"status_code\":200,\"headers\":{\"content-type\":\"text/event-stream\"}}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.completed\\ndata: {\\\"type\\\":\\\"response.completed\\\",\\\"response\\\":{\\\"id\\\":\\\"resp_codex_cli_stream_local_123\\\",\\\"object\\\":\\\"response\\\",\\\"model\\\":\\\"gpt-5.4\\\",\\\"status\\\":\\\"completed\\\",\\\"usage\\\":{\\\"input_tokens\\\":1,\\\"output_tokens\\\":2,\\\"total_tokens\\\":3}}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.output_item.done\\ndata: {\\\"type\\\":\\\"response.output_item.done\\\",\\\"item\\\":{\\\"type\\\":\\\"compaction\\\",\\\"encrypted_content\\\":\\\"ENCRYPTED_CONTEXT_COMPACTION_SUMMARY\\\"}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"data\",\"payload\":{\"kind\":\"data\",\"text\":\"event: response.completed\\ndata: {\\\"type\\\":\\\"response.completed\\\",\\\"response\\\":{\\\"id\\\":\\\"resp_codex_cli_stream_local_123\\\",\\\"object\\\":\\\"response\\\",\\\"model\\\":\\\"gpt-5.6-sol\\\",\\\"status\\\":\\\"completed\\\",\\\"usage\\\":{\\\"input_tokens\\\":1,\\\"output_tokens\\\":2,\\\"total_tokens\\\":3}}}\\n\\n\"}}\n",
|
||||
"{\"type\":\"telemetry\",\"payload\":{\"kind\":\"telemetry\",\"telemetry\":{\"elapsed_ms\":41}}}\n",
|
||||
"{\"type\":\"eof\",\"payload\":{\"kind\":\"eof\"}}\n"
|
||||
);
|
||||
@@ -469,8 +584,16 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {client_api_key}"),
|
||||
)
|
||||
.header("session-id", "session-codex-stream-local-123")
|
||||
.header("thread-id", "thread-codex-stream-local-123")
|
||||
.header(
|
||||
"x-client-request-id",
|
||||
"thread-codex-stream-local-123",
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-codex-cli-stream-local-123")
|
||||
.body("{\"model\":\"gpt-5.4\",\"input\":\"hello\",\"stream\":true}")
|
||||
.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}"#,
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
@@ -478,9 +601,20 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_body =
|
||||
strip_sse_keepalive_comments(&response.text().await.expect("body should read"));
|
||||
assert!(response_body.contains("event: response.output_item.done\n"));
|
||||
assert!(response_body.contains("\"type\":\"compaction\""));
|
||||
assert!(response_body.contains("ENCRYPTED_CONTEXT_COMPACTION_SUMMARY"));
|
||||
let data_line = response_body
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("data: "))
|
||||
.filter_map(|line| line.strip_prefix("data: "))
|
||||
.find(|line| {
|
||||
serde_json::from_str::<serde_json::Value>(line)
|
||||
.ok()
|
||||
.and_then(|event| event.get("type").cloned())
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.as_deref()
|
||||
== Some("response.completed")
|
||||
})
|
||||
.expect("completed event data should exist");
|
||||
let completed_event: serde_json::Value =
|
||||
serde_json::from_str(data_line).expect("completed event should parse");
|
||||
@@ -495,7 +629,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
"response": {
|
||||
"id": "resp_codex_cli_stream_local_123",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"model": "gpt-5.6-sol",
|
||||
"status": "completed",
|
||||
"usage": {
|
||||
"input_tokens": 1,
|
||||
@@ -542,7 +676,8 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
seen_execution_runtime_request.url,
|
||||
"https://chatgpt.com/backend-api/codex/responses"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.model, "gpt-5.4");
|
||||
assert_eq!(seen_execution_runtime_request.model, "gpt-5.6-sol");
|
||||
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");
|
||||
assert_eq!(
|
||||
@@ -550,9 +685,35 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
"Bearer refreshed-codex-stream-access-token"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"trace-codex-cli-stream-local-123"
|
||||
seen_execution_runtime_request.chatgpt_account_id,
|
||||
"acc-codex-stream-local-123"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.fedramp, "true");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"thread-codex-stream-local-123"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.session_id,
|
||||
"session-codex-stream-local-123"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.thread_id,
|
||||
"thread-codex-stream-local-123"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.thread_id,
|
||||
seen_execution_runtime_request.prompt_cache_key
|
||||
);
|
||||
assert!(seen_execution_runtime_request.responses_lite.is_empty());
|
||||
assert!(seen_execution_runtime_request.has_top_level_tools);
|
||||
assert!(seen_execution_runtime_request.has_top_level_instructions);
|
||||
assert!(!seen_execution_runtime_request.has_additional_tools);
|
||||
assert!(seen_execution_runtime_request.parallel_tool_calls);
|
||||
assert_eq!(seen_execution_runtime_request.reasoning_effort, "low");
|
||||
assert!(seen_execution_runtime_request.reasoning_context.is_empty());
|
||||
assert!(seen_execution_runtime_request.has_compaction_trigger);
|
||||
assert!(seen_execution_runtime_request.has_context_management);
|
||||
|
||||
let stored_candidates = request_candidate_repository
|
||||
.list_by_request_id("trace-codex-cli-stream-local-123")
|
||||
|
||||
@@ -182,6 +182,7 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -700,6 +701,7 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1161,6 +1163,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1694,6 +1697,7 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -101,6 +101,7 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -101,6 +101,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -553,6 +554,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1108,6 +1110,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1573,6 +1576,7 @@ async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -91,6 +91,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -296,6 +297,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
let candidate_selection_repository =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
@@ -497,6 +499,7 @@ async fn gateway_surfaces_local_execution_runtime_miss_reason_when_all_openai_ch
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -775,6 +778,7 @@ async fn gateway_retries_next_local_openai_chat_sync_candidate_after_auth_failur
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -150,6 +150,7 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: Some(vec!["endpoint-redaction-1".to_string()]),
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -454,6 +455,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -647,6 +649,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
let candidate_selection_repository =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
@@ -839,6 +842,7 @@ async fn gateway_executes_openai_chat_sync_with_regex_model_mapping_in_execution
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1130,6 +1134,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1151,6 +1156,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
|
||||
priority: 2,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
row
|
||||
}
|
||||
@@ -1698,6 +1704,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_cli_syn
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2102,6 +2109,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2531,6 +2539,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_sync_fa
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2939,6 +2948,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_sync_fa
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -3387,6 +3397,7 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -113,6 +113,7 @@ fn candidate_row(test_id: &str) -> StoredMinimalCandidateSelectionRow {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: Some(vec![format!("endpoint-{test_id}")]),
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -129,6 +129,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -197,6 +197,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -835,6 +836,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -694,6 +695,7 @@ async fn gateway_returns_claude_chat_error_for_local_sync_failure_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -124,6 +124,7 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -570,6 +571,7 @@ async fn gateway_returns_claude_cli_error_for_local_sync_failure_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -853,6 +855,7 @@ async fn gateway_marks_claude_cli_cross_format_runtime_miss_when_format_conversi
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -141,6 +141,7 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -650,6 +651,7 @@ async fn gateway_waits_for_api_key_concurrency_slot_then_executes_openai_respons
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1042,6 +1044,7 @@ async fn gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_b
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1412,6 +1415,7 @@ async fn gateway_returns_openai_responses_error_for_local_sync_failure_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1725,6 +1729,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_cl
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2131,6 +2136,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_sy
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2516,6 +2522,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_ch
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["claude:messages".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -2904,6 +2911,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_ch
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -3223,6 +3231,9 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
|
||||
model: String,
|
||||
authorization: String,
|
||||
x_client_request_id: String,
|
||||
session_id: String,
|
||||
thread_id: String,
|
||||
prompt_cache_key: String,
|
||||
stream_present: bool,
|
||||
plan_stream: bool,
|
||||
}
|
||||
@@ -3298,6 +3309,7 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -3509,6 +3521,25 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
session_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("session-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
thread_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("thread-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
prompt_cache_key: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("prompt_cache_key"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
stream_present: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
@@ -3646,7 +3677,16 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"trace-codex-cli-local-123"
|
||||
seen_execution_runtime_request.thread_id
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.session_id,
|
||||
seen_execution_runtime_request.thread_id
|
||||
);
|
||||
assert!(seen_execution_runtime_request.prompt_cache_key.is_empty());
|
||||
assert_ne!(
|
||||
seen_execution_runtime_request.thread_id,
|
||||
seen_execution_runtime_request.trace_id
|
||||
);
|
||||
assert!(seen_execution_runtime_request.stream_present);
|
||||
assert!(seen_execution_runtime_request.plan_stream);
|
||||
|
||||
@@ -123,6 +123,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -561,6 +562,7 @@ async fn gateway_returns_gemini_cli_error_for_local_sync_failure_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -864,6 +866,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1426,6 +1429,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1878,6 +1882,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_via_local_decision_gate_af
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -123,6 +123,7 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -551,6 +552,7 @@ async fn gateway_returns_gemini_chat_error_for_local_sync_failure_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::{
|
||||
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
|
||||
to_bytes, Arc, Body, Json, Mutex, Request, Router, StatusCode, TRACE_ID_HEADER,
|
||||
};
|
||||
use crate::ai_serving::CODEX_OPENAI_IMAGE_INTERNAL_MODEL;
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
@@ -130,6 +129,7 @@ async fn gateway_converts_openai_image_sync_to_gemini_image_provider_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -411,10 +411,10 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
url: String,
|
||||
authorization: String,
|
||||
model: String,
|
||||
action: String,
|
||||
prompt: String,
|
||||
image_url: String,
|
||||
request_stream: bool,
|
||||
body_stream: Option<bool>,
|
||||
}
|
||||
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
@@ -482,6 +482,7 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -574,12 +575,6 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let content = body_json
|
||||
.get("input")
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("content"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!([]));
|
||||
*seen_execution_runtime_inner
|
||||
.lock()
|
||||
.expect("mutex should lock") = Some(SeenExecutionRuntimeSyncRequest {
|
||||
@@ -605,39 +600,22 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
action: body_json
|
||||
.get("tools")
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("action"))
|
||||
prompt: body_json
|
||||
.get("prompt")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
prompt: content
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|item| {
|
||||
item.get("type").and_then(|value| value.as_str()) == Some("input_text")
|
||||
})
|
||||
.and_then(|item| item.get("text"))
|
||||
image_url: body_json
|
||||
.get("image")
|
||||
.and_then(|value| value.get("image_url"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
image_url: content
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|item| {
|
||||
item.get("type").and_then(|value| value.as_str()) == Some("input_image")
|
||||
})
|
||||
.and_then(|item| item.get("image_url"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
request_stream: body_json
|
||||
request_stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(true),
|
||||
body_stream: body_json.get("stream").and_then(serde_json::Value::as_bool),
|
||||
});
|
||||
Json(json!({
|
||||
"request_id": "trace-gemini-image-to-openai-123",
|
||||
@@ -647,21 +625,16 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"id": "resp_img_bridge_123",
|
||||
"object": "response",
|
||||
"created": 1776839946,
|
||||
"model": "gpt-image-2-upstream",
|
||||
"status": "completed",
|
||||
"usage": {
|
||||
"input_tokens": 3,
|
||||
"output_tokens": 4,
|
||||
"total_tokens": 7
|
||||
},
|
||||
"output": [{
|
||||
"type": "image_generation_call",
|
||||
"status": "completed",
|
||||
"output_format": "png",
|
||||
"data": [{
|
||||
"revised_prompt": "converted gemini prompt",
|
||||
"result": "aGVsbG8="
|
||||
"b64_json": "aGVsbG8="
|
||||
}]
|
||||
}
|
||||
},
|
||||
@@ -750,14 +723,13 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.url,
|
||||
"https://api.openai.com/v1/images/generations"
|
||||
"https://api.openai.com/v1/images/edits"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.authorization,
|
||||
"Bearer sk-upstream-openai-image"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.model, "gpt-image-2-upstream");
|
||||
assert_eq!(seen_execution_runtime_request.action, "edit");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.prompt,
|
||||
"Change the background"
|
||||
@@ -767,6 +739,7 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
|
||||
"data:image/png;base64,aGVsbG8="
|
||||
);
|
||||
assert!(!seen_execution_runtime_request.request_stream);
|
||||
assert_eq!(seen_execution_runtime_request.body_stream, None);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
@@ -785,18 +758,9 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
struct SeenExecutionRuntimeSyncRequest {
|
||||
trace_id: String,
|
||||
url: String,
|
||||
model: String,
|
||||
authorization: String,
|
||||
x_client_request_id: String,
|
||||
prompt: String,
|
||||
content_is_string: bool,
|
||||
tool_type: String,
|
||||
tool_size: String,
|
||||
tool_quality: String,
|
||||
tool_background: String,
|
||||
tool_choice_type: String,
|
||||
tool_has_n: bool,
|
||||
request_stream: bool,
|
||||
headers: serde_json::Value,
|
||||
body: serde_json::Value,
|
||||
plan_stream: bool,
|
||||
}
|
||||
|
||||
@@ -871,6 +835,7 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1011,98 +976,18 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
model: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("model"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
authorization: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("authorization"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
x_client_request_id: payload
|
||||
.get("headers")
|
||||
.and_then(|value| value.get("x-client-request-id"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
prompt: payload
|
||||
headers: payload.get("headers").cloned().unwrap_or_default(),
|
||||
body: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("input"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("content"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
content_is_string: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("input"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("content"))
|
||||
.is_some_and(|value| value.is_string()),
|
||||
tool_type: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_size: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("size"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_quality: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("quality"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_background: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.get("background"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_choice_type: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tool_choice"))
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tool_has_n: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("tools"))
|
||||
.and_then(|value| value.get(0))
|
||||
.and_then(|value| value.as_object())
|
||||
.is_some_and(|object| object.contains_key("n")),
|
||||
request_stream: payload
|
||||
.get("body")
|
||||
.and_then(|value| value.get("json_body"))
|
||||
.and_then(|value| value.get("stream"))
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false),
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
plan_stream: payload
|
||||
.get("stream")
|
||||
.and_then(|value| value.as_bool())
|
||||
@@ -1112,20 +997,11 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
"request_id": "trace-codex-image-local-123",
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"body_bytes_b64": base64::engine::general_purpose::STANDARD.encode(
|
||||
concat!(
|
||||
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_img_123\",\"created_at\":1776839946}}\n\n",
|
||||
"data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"ig_123\",\"type\":\"image_generation_call\",\"status\":\"generating\",\"output_format\":\"png\",\"quality\":\"medium\",\"size\":\"1024x1024\",\"revised_prompt\":\"中国历史视觉海报\",\"result\":\"aGVsbG8=\"}}\n\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_img_123\",\"object\":\"response\",\"model\":\"__CODEX_IMAGE_MODEL__\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":2440,\"output_tokens\":184,\"total_tokens\":2624},\"tool_usage\":{\"image_gen\":{\"input_tokens\":171,\"input_tokens_details\":{\"image_tokens\":0,\"text_tokens\":171},\"output_tokens\":1372,\"output_tokens_details\":{\"image_tokens\":1372,\"text_tokens\":0},\"total_tokens\":1543}}}}\n\n",
|
||||
"data: [DONE]\n\n"
|
||||
)
|
||||
.replace(
|
||||
"__CODEX_IMAGE_MODEL__",
|
||||
CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
|
||||
)
|
||||
r#"{"created":1776839946,"data":[{"b64_json":"aGVsbG8=","revised_prompt":"水墨视觉海报"}],"usage":{"input_tokens":171,"output_tokens":1372,"total_tokens":1543}}"#
|
||||
)
|
||||
},
|
||||
"telemetry": {
|
||||
@@ -1182,7 +1058,7 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
format!("Bearer {client_api_key}"),
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-codex-image-local-123")
|
||||
.body("{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张中国历史视觉海报\",\"size\":\"1024x1024\",\"n\":1,\"response_format\":\"b64_json\"}")
|
||||
.body("{\"model\":\"gpt-image-2\",\"prompt\":\"生成一张水墨视觉海报\",\"background\":\"auto\",\"quality\":\"auto\",\"size\":\"auto\",\"n\":1,\"response_format\":\"b64_json\"}")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
@@ -1191,10 +1067,7 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
let response_json: serde_json::Value = response.json().await.expect("body should parse");
|
||||
assert_eq!(response_json["created"], 1776839946);
|
||||
assert_eq!(response_json["data"][0]["b64_json"], "aGVsbG8=");
|
||||
assert_eq!(
|
||||
response_json["data"][0]["revised_prompt"],
|
||||
"中国历史视觉海报"
|
||||
);
|
||||
assert_eq!(response_json["data"][0]["revised_prompt"], "水墨视觉海报");
|
||||
assert_eq!(response_json["usage"]["input_tokens"], 171);
|
||||
assert_eq!(response_json["usage"]["output_tokens"], 1372);
|
||||
|
||||
@@ -1229,35 +1102,34 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.url,
|
||||
"https://chatgpt.com/backend-api/codex/responses"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.model,
|
||||
CODEX_OPENAI_IMAGE_INTERNAL_MODEL
|
||||
"https://chatgpt.com/backend-api/codex/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.authorization,
|
||||
"Bearer refreshed-codex-image-access-token"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.x_client_request_id,
|
||||
"trace-codex-image-local-123"
|
||||
seen_execution_runtime_request.body,
|
||||
json!({
|
||||
"prompt": "生成一张水墨视觉海报",
|
||||
"background": "auto",
|
||||
"model": "gpt-image-2",
|
||||
"n": 1,
|
||||
"quality": "auto",
|
||||
"size": "auto"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.prompt,
|
||||
"生成一张中国历史视觉海报"
|
||||
seen_execution_runtime_request.headers["user-agent"],
|
||||
"codex_cli_rs/0.144.1"
|
||||
);
|
||||
assert!(seen_execution_runtime_request.content_is_string);
|
||||
assert_eq!(seen_execution_runtime_request.tool_type, "image_generation");
|
||||
assert_eq!(seen_execution_runtime_request.tool_size, "1024x1024");
|
||||
assert_eq!(seen_execution_runtime_request.tool_quality, "high");
|
||||
assert_eq!(seen_execution_runtime_request.tool_background, "auto");
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.tool_choice_type,
|
||||
"image_generation"
|
||||
seen_execution_runtime_request.headers["originator"],
|
||||
"codex_cli_rs"
|
||||
);
|
||||
assert!(!seen_execution_runtime_request.tool_has_n);
|
||||
assert!(seen_execution_runtime_request.request_stream);
|
||||
for header in ["x-client-request-id", "session-id", "thread-id"] {
|
||||
assert!(seen_execution_runtime_request.headers.get(header).is_none());
|
||||
}
|
||||
assert!(!seen_execution_runtime_request.plan_stream);
|
||||
|
||||
let persisted_transport_state =
|
||||
@@ -1373,6 +1245,7 @@ async fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url_imp
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:image".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -29,3 +29,4 @@ mod cli;
|
||||
mod gemini;
|
||||
mod image;
|
||||
mod pii_redaction_formats;
|
||||
mod search;
|
||||
|
||||
@@ -608,6 +608,7 @@ fn candidate_row(case: &RedactionFormatCase) -> StoredMinimalCandidateSelectionR
|
||||
priority: 1,
|
||||
api_formats: Some(vec![case.provider_format.api_format().to_string()]),
|
||||
endpoint_ids: Some(vec![format!("endpoint-{}", case.test_id)]),
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
use super::{
|
||||
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
|
||||
to_bytes, Arc, Body, Json, Mutex, Request, Router, StatusCode,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
|
||||
};
|
||||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
||||
use aether_data::repository::auth::{
|
||||
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||||
};
|
||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data_contracts::repository::candidate_selection::{
|
||||
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
|
||||
};
|
||||
use aether_data_contracts::repository::candidates::{
|
||||
RequestCandidateReadRepository, RequestCandidateStatus,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const SEARCH_SYNC_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
|
||||
|
||||
fn run_search_sync_test<F, Fut>(test_name: &'static str, make_future: F)
|
||||
where
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: std::future::Future<Output = ()> + 'static,
|
||||
{
|
||||
let handle = std::thread::Builder::new()
|
||||
.name(test_name.to_string())
|
||||
.stack_size(SEARCH_SYNC_TEST_STACK_BYTES)
|
||||
.spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("test runtime should build");
|
||||
runtime.block_on(make_future());
|
||||
})
|
||||
.expect("search sync test thread should spawn");
|
||||
|
||||
if let Err(payload) = handle.join() {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_executes_codex_search_with_responses_permission_and_search_contract() {
|
||||
run_search_sync_test(
|
||||
"gateway_executes_codex_search_with_responses_permission_and_search_contract",
|
||||
gateway_executes_codex_search_with_responses_permission_and_search_contract_impl,
|
||||
);
|
||||
}
|
||||
|
||||
async fn gateway_executes_codex_search_with_responses_permission_and_search_contract_impl() {
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn auth_snapshot() -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
"user-search-1".to_string(),
|
||||
"alice".to_string(),
|
||||
Some("[email protected]".to_string()),
|
||||
"user".to_string(),
|
||||
"local".to_string(),
|
||||
true,
|
||||
false,
|
||||
Some(json!(["openai", "codex"])),
|
||||
Some(json!(["openai:responses"])),
|
||||
None,
|
||||
"api-key-search-1".to_string(),
|
||||
Some("search-client".to_string()),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some(60),
|
||||
Some(5),
|
||||
Some(4_102_444_800_i64),
|
||||
Some(json!(["openai", "codex"])),
|
||||
Some(json!(["openai:responses"])),
|
||||
None,
|
||||
)
|
||||
.expect("auth snapshot should build")
|
||||
}
|
||||
|
||||
fn candidate_row() -> StoredMinimalCandidateSelectionRow {
|
||||
StoredMinimalCandidateSelectionRow {
|
||||
provider_id: "provider-codex-search-1".to_string(),
|
||||
provider_name: "codex".to_string(),
|
||||
provider_type: "codex".to_string(),
|
||||
provider_priority: 10,
|
||||
provider_is_active: true,
|
||||
endpoint_id: "endpoint-codex-search-1".to_string(),
|
||||
endpoint_api_format: "openai:search".to_string(),
|
||||
endpoint_api_family: Some("openai".to_string()),
|
||||
endpoint_kind: Some("search".to_string()),
|
||||
endpoint_is_active: true,
|
||||
key_id: "key-codex-search-1".to_string(),
|
||||
key_name: "oauth".to_string(),
|
||||
key_auth_type: "oauth".to_string(),
|
||||
key_is_active: true,
|
||||
key_api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
key_allowed_models: None,
|
||||
key_capabilities: None,
|
||||
key_internal_priority: 5,
|
||||
key_global_priority_by_format: Some(json!({"openai:search": 1})),
|
||||
model_id: "model-codex-search-1".to_string(),
|
||||
global_model_id: "global-model-codex-search-1".to_string(),
|
||||
global_model_name: "gpt-5.6-sol".to_string(),
|
||||
global_model_mappings: None,
|
||||
global_model_supports_streaming: Some(false),
|
||||
model_provider_model_name: "gpt-5.6-sol".to_string(),
|
||||
model_provider_model_mappings: Some(vec![StoredProviderModelMapping {
|
||||
name: "gpt-5.6-sol".to_string(),
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(false),
|
||||
model_is_active: true,
|
||||
model_is_available: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn provider() -> StoredProviderCatalogProvider {
|
||||
StoredProviderCatalogProvider::new(
|
||||
"provider-codex-search-1".to_string(),
|
||||
"codex".to_string(),
|
||||
Some("https://chatgpt.com".to_string()),
|
||||
"codex".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
Some(900.0),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn endpoint() -> StoredProviderCatalogEndpoint {
|
||||
StoredProviderCatalogEndpoint::new(
|
||||
"endpoint-codex-search-1".to_string(),
|
||||
"provider-codex-search-1".to_string(),
|
||||
"openai:search".to_string(),
|
||||
Some("openai".to_string()),
|
||||
Some("search".to_string()),
|
||||
true,
|
||||
)
|
||||
.expect("endpoint should build")
|
||||
.with_transport_fields(
|
||||
"https://chatgpt.com/backend-api/codex".to_string(),
|
||||
None,
|
||||
None,
|
||||
Some(2),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("endpoint transport should build")
|
||||
}
|
||||
|
||||
fn key() -> StoredProviderCatalogKey {
|
||||
let auth_config = encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"provider_type":"codex","account_id":"account-search-1","is_fedramp":true}"#,
|
||||
)
|
||||
.expect("auth config should encrypt");
|
||||
StoredProviderCatalogKey::new(
|
||||
"key-codex-search-1".to_string(),
|
||||
"provider-codex-search-1".to_string(),
|
||||
"oauth".to_string(),
|
||||
"oauth".to_string(),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("key should build")
|
||||
.with_transport_fields(
|
||||
Some(json!(["openai:responses"])),
|
||||
encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
"codex-search-access-token",
|
||||
)
|
||||
.expect("access token should encrypt"),
|
||||
Some(auth_config),
|
||||
None,
|
||||
Some(json!({"openai:search": 1})),
|
||||
None,
|
||||
Some(4_102_444_800),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("key transport should build")
|
||||
}
|
||||
|
||||
let seen_plans = Arc::new(Mutex::new(Vec::<serde_json::Value>::new()));
|
||||
let seen_plans_clone = Arc::clone(&seen_plans);
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |request: Request| {
|
||||
let seen_plans_inner = Arc::clone(&seen_plans_clone);
|
||||
async move {
|
||||
let (_, body) = request.into_parts();
|
||||
let bytes = to_bytes(body, usize::MAX).await.expect("body should read");
|
||||
let payload: serde_json::Value =
|
||||
serde_json::from_slice(&bytes).expect("execution payload should parse");
|
||||
let request_id = payload["request_id"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let provider_id = payload["provider_id"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
seen_plans_inner
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.push(payload);
|
||||
let execution_result = if request_id == "trace-search-error-1" {
|
||||
json!({
|
||||
"request_id": request_id,
|
||||
"status_code": 400,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"x-search-upstream": "rate-limited"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"error": {
|
||||
"type": "rate_limit_error",
|
||||
"message": "Search capacity reached",
|
||||
"param": null,
|
||||
"code": "rate_limit_exceeded"
|
||||
},
|
||||
"future_error_field": {"retryable": true}
|
||||
}
|
||||
},
|
||||
"telemetry": {"elapsed_ms": 17}
|
||||
})
|
||||
} else if request_id == "trace-search-failover-1"
|
||||
&& provider_id == "provider-codex-search-1"
|
||||
{
|
||||
json!({
|
||||
"request_id": request_id,
|
||||
"status_code": 500,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"x-search-upstream": "primary"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"error": {
|
||||
"type": "server_error",
|
||||
"message": "Search backend unavailable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry": {"elapsed_ms": 11}
|
||||
})
|
||||
} else if request_id == "trace-search-failover-1" {
|
||||
json!({
|
||||
"request_id": request_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"x-search-upstream": "backup"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"output": "search fallback result"
|
||||
}
|
||||
},
|
||||
"telemetry": {"elapsed_ms": 23}
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"request_id": request_id,
|
||||
"status_code": 201,
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
"x-search-upstream": "alpha"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"output": "search result",
|
||||
"encrypted_output": "encrypted-search-result",
|
||||
"future_response_field": {"enabled": true}
|
||||
}
|
||||
},
|
||||
"telemetry": {"elapsed_ms": 42}
|
||||
})
|
||||
};
|
||||
(
|
||||
StatusCode::OK,
|
||||
[("x-search-source", "codex-alpha")],
|
||||
Json(execution_result),
|
||||
)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
let client_api_key = "sk-client-search";
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key(client_api_key)),
|
||||
auth_snapshot(),
|
||||
)]));
|
||||
let candidate_repository = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed({
|
||||
let primary = candidate_row();
|
||||
let mut backup = primary.clone();
|
||||
backup.provider_id = "provider-codex-search-2".to_string();
|
||||
backup.provider_name = "codex-backup".to_string();
|
||||
backup.provider_priority = 20;
|
||||
backup.endpoint_id = "endpoint-codex-search-2".to_string();
|
||||
backup.key_id = "key-codex-search-2".to_string();
|
||||
backup.key_name = "oauth-backup".to_string();
|
||||
backup.key_internal_priority = 6;
|
||||
backup.key_global_priority_by_format = Some(json!({"openai:search": 2}));
|
||||
backup.model_id = "model-codex-search-2".to_string();
|
||||
vec![primary, backup]
|
||||
}));
|
||||
let catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
{
|
||||
let primary = provider();
|
||||
let mut backup = primary.clone();
|
||||
backup.id = "provider-codex-search-2".to_string();
|
||||
backup.name = "codex-backup".to_string();
|
||||
vec![primary, backup]
|
||||
},
|
||||
{
|
||||
let primary = endpoint();
|
||||
let mut backup = primary.clone();
|
||||
backup.id = "endpoint-codex-search-2".to_string();
|
||||
backup.provider_id = "provider-codex-search-2".to_string();
|
||||
vec![primary, backup]
|
||||
},
|
||||
{
|
||||
let primary = key();
|
||||
let mut backup = primary.clone();
|
||||
backup.id = "key-codex-search-2".to_string();
|
||||
backup.provider_id = "provider-codex-search-2".to_string();
|
||||
backup.name = "oauth-backup".to_string();
|
||||
backup.global_priority_by_format = Some(json!({"openai:search": 2}));
|
||||
vec![primary, backup]
|
||||
},
|
||||
));
|
||||
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::default());
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let data_state =
|
||||
crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_and_request_candidate_repository_for_tests(
|
||||
auth_repository,
|
||||
candidate_repository,
|
||||
catalog_repository,
|
||||
Arc::clone(&request_candidates),
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
)
|
||||
.with_system_config_values_for_tests([(
|
||||
crate::system_features::ENABLE_MODEL_DIRECTIVES_CONFIG_KEY.to_string(),
|
||||
json!(true),
|
||||
)]);
|
||||
let state = build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(data_state);
|
||||
let gateway = build_router_with_state(state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/v1/alpha/search"))
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {client_api_key}"),
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-search-1")
|
||||
.json(&json!({
|
||||
"id": "session-search-1",
|
||||
"model": "gpt-5.6-sol-ultra-fast",
|
||||
"reasoning": {"effort": "low", "summary": "auto"},
|
||||
"input": "find current OpenAI documentation",
|
||||
"commands": {
|
||||
"search_query": [{"q": "OpenAI Codex search"}],
|
||||
"open": [{"ref_id": "turn0search0"}]
|
||||
},
|
||||
"settings": {
|
||||
"search_context_size": "high",
|
||||
"allowed_callers": ["direct"]
|
||||
},
|
||||
"max_output_tokens": 4096,
|
||||
"store": false,
|
||||
"stream": true,
|
||||
"future_request_field": {"enabled": true}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("search request should succeed");
|
||||
|
||||
if response.status() != StatusCode::CREATED {
|
||||
let status = response.status();
|
||||
let body = response.text().await.expect("error response should read");
|
||||
panic!("Search request returned {status}: {body}");
|
||||
}
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-search-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("alpha")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(EXECUTION_PATH_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC)
|
||||
);
|
||||
let response_json: serde_json::Value = response.json().await.expect("response should parse");
|
||||
assert_eq!(response_json["output"], "search result");
|
||||
assert_eq!(response_json["encrypted_output"], "encrypted-search-result");
|
||||
assert_eq!(response_json["future_response_field"]["enabled"], true);
|
||||
|
||||
let plan = seen_plans
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.first()
|
||||
.cloned()
|
||||
.expect("execution plan should be captured");
|
||||
assert_eq!(
|
||||
plan["url"],
|
||||
"https://chatgpt.com/backend-api/codex/alpha/search"
|
||||
);
|
||||
assert_eq!(plan["client_api_format"], "openai:search");
|
||||
assert_eq!(plan["provider_api_format"], "openai:search");
|
||||
assert_eq!(plan["stream"], false);
|
||||
assert_eq!(plan["timeouts"]["total_ms"], 900_000);
|
||||
assert_eq!(
|
||||
plan["headers"]["authorization"],
|
||||
"Bearer codex-search-access-token"
|
||||
);
|
||||
assert_eq!(plan["headers"]["chatgpt-account-id"], "account-search-1");
|
||||
assert_eq!(plan["headers"]["x-openai-fedramp"], "true");
|
||||
assert_eq!(plan["headers"]["originator"], "codex_cli_rs");
|
||||
assert!(plan["headers"]["user-agent"]
|
||||
.as_str()
|
||||
.is_some_and(|value| value.starts_with("codex_cli_rs/")));
|
||||
assert!(plan["headers"].get("openai-beta").is_none());
|
||||
assert!(plan["headers"]
|
||||
.get("x-openai-internal-codex-responses-lite")
|
||||
.is_none());
|
||||
assert_ne!(plan["headers"]["accept"], "text/event-stream");
|
||||
|
||||
let body = &plan["body"]["json_body"];
|
||||
assert_eq!(body["id"], "session-search-1");
|
||||
assert_eq!(body["model"], "gpt-5.6-sol");
|
||||
assert_eq!(body["reasoning"]["effort"], "max");
|
||||
assert_eq!(body["reasoning"]["summary"], "auto");
|
||||
assert_eq!(
|
||||
body["commands"]["search_query"][0]["q"],
|
||||
"OpenAI Codex search"
|
||||
);
|
||||
assert_eq!(body["commands"]["open"][0]["ref_id"], "turn0search0");
|
||||
assert_eq!(body["settings"]["search_context_size"], "high");
|
||||
assert_eq!(body["max_output_tokens"], 4096);
|
||||
assert!(body.get("store").is_none());
|
||||
assert!(body.get("future_request_field").is_none());
|
||||
assert!(body.get("stream").is_none());
|
||||
assert!(body.get("service_tier").is_none());
|
||||
|
||||
let candidates = request_candidates
|
||||
.list_by_request_id("trace-search-1")
|
||||
.await
|
||||
.expect("request candidates should read");
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].status, RequestCandidateStatus::Success);
|
||||
|
||||
let expected_error_body = json!({
|
||||
"error": {
|
||||
"type": "rate_limit_error",
|
||||
"message": "Search capacity reached",
|
||||
"param": null,
|
||||
"code": "rate_limit_exceeded"
|
||||
},
|
||||
"future_error_field": {"retryable": true}
|
||||
});
|
||||
let error_response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/v1/alpha/search"))
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {client_api_key}"),
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-search-error-1")
|
||||
.json(&json!({
|
||||
"id": "session-search-error-1",
|
||||
"model": "gpt-5.6-sol",
|
||||
"input": "find current OpenAI documentation",
|
||||
"commands": {"search_query": [{"q": "OpenAI documentation"}]}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("search error response should return");
|
||||
|
||||
assert_eq!(error_response.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error_response
|
||||
.headers()
|
||||
.get("x-search-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("rate-limited")
|
||||
);
|
||||
assert_eq!(
|
||||
error_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("error response should parse"),
|
||||
expected_error_body
|
||||
);
|
||||
let error_candidates = request_candidates
|
||||
.list_by_request_id("trace-search-error-1")
|
||||
.await
|
||||
.expect("error request candidates should read");
|
||||
assert_eq!(error_candidates.len(), 1);
|
||||
assert_eq!(error_candidates[0].status, RequestCandidateStatus::Failed);
|
||||
|
||||
let failover_response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/v1/alpha/search"))
|
||||
.header(http::header::CONTENT_TYPE, "application/json")
|
||||
.header(
|
||||
http::header::AUTHORIZATION,
|
||||
format!("Bearer {client_api_key}"),
|
||||
)
|
||||
.header(TRACE_ID_HEADER, "trace-search-failover-1")
|
||||
.json(&json!({
|
||||
"id": "session-search-failover-1",
|
||||
"model": "gpt-5.6-sol",
|
||||
"input": "find current OpenAI documentation",
|
||||
"commands": {"search_query": [{"q": "OpenAI documentation"}]}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("search failover response should return");
|
||||
|
||||
assert_eq!(failover_response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
failover_response
|
||||
.headers()
|
||||
.get("x-search-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("backup")
|
||||
);
|
||||
assert_eq!(
|
||||
failover_response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.expect("failover response should parse")["output"],
|
||||
"search fallback result"
|
||||
);
|
||||
let failover_plans = seen_plans
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.iter()
|
||||
.filter(|plan| plan["request_id"] == "trace-search-failover-1")
|
||||
.map(|plan| plan["provider_id"].clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
failover_plans,
|
||||
vec![
|
||||
json!("provider-codex-search-1"),
|
||||
json!("provider-codex-search-2")
|
||||
]
|
||||
);
|
||||
let failover_candidates = request_candidates
|
||||
.list_by_request_id("trace-search-failover-1")
|
||||
.await
|
||||
.expect("failover request candidates should read");
|
||||
assert_eq!(failover_candidates.len(), 2);
|
||||
assert_eq!(
|
||||
failover_candidates[0].status,
|
||||
RequestCandidateStatus::Failed
|
||||
);
|
||||
assert_eq!(failover_candidates[0].status_code, Some(500));
|
||||
assert_eq!(
|
||||
failover_candidates[1].status,
|
||||
RequestCandidateStatus::Success
|
||||
);
|
||||
assert_eq!(failover_candidates[1].status_code, Some(200));
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
@@ -661,7 +661,7 @@ fn admin_monitoring_snapshots_stay_app_local() {
|
||||
"monitoring/resilience/snapshot.rs should define AdminMonitoringResilienceSnapshot locally"
|
||||
);
|
||||
|
||||
let data_system = read_workspace_file("crates/aether-data/src/repository/system.rs");
|
||||
let data_system = read_workspace_file("crates/aether-data/runtime/src/repository/system.rs");
|
||||
assert!(
|
||||
!data_system.contains("AdminMonitoringCacheSnapshot")
|
||||
&& !data_system.contains("AdminMonitoringResilienceSnapshot"),
|
||||
|
||||
@@ -58,7 +58,8 @@ fn admin_provider_root_stays_thin() {
|
||||
|
||||
let ops_mod = read_workspace_file("apps/aether-gateway/src/handlers/admin/provider/ops/mod.rs");
|
||||
assert!(
|
||||
!ops_mod.contains("pub(crate) use self::providers::admin_provider_ops_local_action_response;"),
|
||||
!ops_mod
|
||||
.contains("pub(crate) use self::providers::admin_provider_ops_local_action_response;"),
|
||||
"handlers/admin/provider/ops/mod.rs should not re-export admin_provider_ops_local_action_response"
|
||||
);
|
||||
|
||||
@@ -120,7 +121,7 @@ fn admin_provider_oauth_complete_dispatch_remains_thin() {
|
||||
#[test]
|
||||
fn postgres_provider_cleanup_preserves_usage_history() {
|
||||
let postgres_provider_catalog =
|
||||
read_workspace_file("crates/aether-data/src/repository/provider_catalog/postgres.rs");
|
||||
read_workspace_file("crates/aether-data/adapters/postgres/src/provider_catalog.rs");
|
||||
|
||||
for forbidden in [
|
||||
"UPDATE usage SET provider_id = NULL",
|
||||
@@ -137,9 +138,9 @@ fn postgres_provider_cleanup_preserves_usage_history() {
|
||||
#[test]
|
||||
fn provider_cleanup_keeps_common_backends_in_sync() {
|
||||
for path in [
|
||||
"crates/aether-data/src/repository/provider_catalog/postgres.rs",
|
||||
"crates/aether-data/src/repository/provider_catalog/mysql.rs",
|
||||
"crates/aether-data/src/repository/provider_catalog/sqlite.rs",
|
||||
"crates/aether-data/adapters/postgres/src/provider_catalog.rs",
|
||||
"crates/aether-data/adapters/mysql/src/provider_catalog.rs",
|
||||
"crates/aether-data/adapters/sqlite/src/provider_catalog.rs",
|
||||
] {
|
||||
let source = read_workspace_file(path);
|
||||
for required in [
|
||||
@@ -370,7 +371,9 @@ fn admin_provider_ops_routes_directoryized() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
routes_mod.contains("pub(crate) async fn maybe_build_local_admin_provider_ops_providers_response("),
|
||||
routes_mod.contains(
|
||||
"pub(crate) async fn maybe_build_local_admin_provider_ops_providers_response("
|
||||
),
|
||||
"handlers/admin/provider/ops/providers/routes/mod.rs should keep the provider ops entry seam"
|
||||
);
|
||||
for forbidden in [
|
||||
@@ -386,7 +389,9 @@ fn admin_provider_ops_routes_directoryized() {
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/provider/ops/providers/routes.rs"),
|
||||
!workspace_file_exists(
|
||||
"apps/aether-gateway/src/handlers/admin/provider/ops/providers/routes.rs"
|
||||
),
|
||||
"handlers/admin/provider/ops/providers/routes.rs should be removed once routes are directoryized"
|
||||
);
|
||||
|
||||
@@ -667,8 +672,10 @@ fn admin_provider_query_and_strategy_use_specific_local_owners() {
|
||||
let strategy_routes =
|
||||
read_workspace_file("apps/aether-gateway/src/handlers/admin/provider/strategy/routes.rs");
|
||||
assert!(
|
||||
strategy_routes.contains("state\n .maybe_build_admin_provider_strategy_route_response(")
|
||||
|| strategy_routes.contains("state.maybe_build_admin_provider_strategy_route_response("),
|
||||
strategy_routes
|
||||
.contains("state\n .maybe_build_admin_provider_strategy_route_response(")
|
||||
|| strategy_routes
|
||||
.contains("state.maybe_build_admin_provider_strategy_route_response("),
|
||||
"handlers/admin/provider/strategy/routes.rs should delegate to request/provider route owner"
|
||||
);
|
||||
assert!(
|
||||
@@ -686,11 +693,15 @@ fn admin_provider_query_and_strategy_use_specific_local_owners() {
|
||||
);
|
||||
|
||||
assert!(
|
||||
workspace_file_exists("apps/aether-gateway/src/handlers/admin/provider/strategy/responses.rs"),
|
||||
workspace_file_exists(
|
||||
"apps/aether-gateway/src/handlers/admin/provider/strategy/responses.rs"
|
||||
),
|
||||
"handlers/admin/provider/strategy/responses.rs should own strategy route-level shared responses"
|
||||
);
|
||||
assert!(
|
||||
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/provider/strategy/shared.rs"),
|
||||
!workspace_file_exists(
|
||||
"apps/aether-gateway/src/handlers/admin/provider/strategy/shared.rs"
|
||||
),
|
||||
"handlers/admin/provider/strategy/shared.rs should be removed once the local shared hub is narrowed"
|
||||
);
|
||||
}
|
||||
@@ -1181,7 +1192,7 @@ fn admin_provider_write_uses_specific_local_owners() {
|
||||
] {
|
||||
assert!(
|
||||
endpoint_keys_mutations.contains(pattern),
|
||||
"handlers/admin/provider/endpoint_keys/mutations/mod.rs should expose explicit mutation owner {pattern}"
|
||||
"handlers/admin/provider/endpoint_keys/mutations/mod.rs should expose explicit mutation owner {pattern}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1300,7 +1311,9 @@ fn admin_provider_ops_actions_mod_stays_thin() {
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/provider/ops/providers/actions.rs"),
|
||||
!workspace_file_exists(
|
||||
"apps/aether-gateway/src/handlers/admin/provider/ops/providers/actions.rs"
|
||||
),
|
||||
"handlers/admin/provider/ops/providers/actions.rs should be removed once actions logic is directoryized"
|
||||
);
|
||||
|
||||
@@ -1812,7 +1825,8 @@ fn admin_provider_oauth_quota_mod_stays_thin() {
|
||||
"handlers/admin/provider/oauth/quota/shared.rs should delegate quota metadata provider detection to aether-provider-pool"
|
||||
);
|
||||
assert!(
|
||||
!quota_shared.contains("[\"codex\", \"kiro\", \"antigravity\", \"gemini_cli\", \"chatgpt_web\"]"),
|
||||
!quota_shared
|
||||
.contains("[\"codex\", \"kiro\", \"antigravity\", \"gemini_cli\", \"chatgpt_web\"]"),
|
||||
"handlers/admin/provider/oauth/quota/shared.rs should not hardcode quota metadata provider list"
|
||||
);
|
||||
|
||||
@@ -1874,9 +1888,8 @@ fn admin_provider_oauth_quota_mod_stays_thin() {
|
||||
"apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/invalid.rs",
|
||||
);
|
||||
assert!(
|
||||
quota_codex_invalid.contains(
|
||||
"use crate::handlers::admin::provider::shared::payloads::{"
|
||||
) || quota_codex_invalid.contains("use aether_admin::provider::quota"),
|
||||
quota_codex_invalid.contains("use crate::handlers::admin::provider::shared::payloads::{")
|
||||
|| quota_codex_invalid.contains("use aether_admin::provider::quota"),
|
||||
"handlers/admin/provider/oauth/quota/codex/invalid.rs should either own codex invalid-state helpers locally or delegate to aether-admin"
|
||||
);
|
||||
let quota_codex_plan = read_workspace_file(
|
||||
@@ -1950,7 +1963,8 @@ fn admin_provider_oauth_quota_mod_stays_thin() {
|
||||
"handlers/admin/provider/oauth/quota/antigravity.rs should import common quota helpers from shared.rs"
|
||||
);
|
||||
assert!(
|
||||
quota_antigravity.contains("use aether_provider_pool::build_antigravity_pool_quota_request;"),
|
||||
quota_antigravity
|
||||
.contains("use aether_provider_pool::build_antigravity_pool_quota_request;"),
|
||||
"handlers/admin/provider/oauth/quota/antigravity.rs should delegate antigravity quota request construction to aether-provider-pool"
|
||||
);
|
||||
let quota_chatgpt_web = read_workspace_file(
|
||||
@@ -2131,12 +2145,14 @@ fn admin_provider_oauth_dispatch_batch_mod_stays_thin() {
|
||||
"apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/batch/kiro_import.rs",
|
||||
);
|
||||
assert!(
|
||||
batch_kiro_import.contains("pub(super) async fn execute_admin_provider_oauth_kiro_batch_import("),
|
||||
batch_kiro_import
|
||||
.contains("pub(super) async fn execute_admin_provider_oauth_kiro_batch_import("),
|
||||
"handlers/admin/provider/oauth/dispatch/batch/kiro_import.rs should own the kiro batch execution owner"
|
||||
);
|
||||
assert!(
|
||||
batch_kiro_import.contains("build_kiro_batch_import_key_name(")
|
||||
|| batch_kiro_import.contains("aether_admin::provider::oauth::build_kiro_batch_import_key_name"),
|
||||
|| batch_kiro_import
|
||||
.contains("aether_admin::provider::oauth::build_kiro_batch_import_key_name"),
|
||||
"handlers/admin/provider/oauth/dispatch/batch/kiro_import.rs should either own or delegate the kiro key-name builder"
|
||||
);
|
||||
assert!(
|
||||
@@ -2163,7 +2179,8 @@ fn admin_provider_oauth_dispatch_batch_mod_stays_thin() {
|
||||
"apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/batch/orchestration.rs",
|
||||
);
|
||||
assert!(
|
||||
batch_orchestration.contains("pub(in super::super) async fn handle_admin_provider_oauth_batch_import("),
|
||||
batch_orchestration
|
||||
.contains("pub(in super::super) async fn handle_admin_provider_oauth_batch_import("),
|
||||
"handlers/admin/provider/oauth/dispatch/batch/orchestration.rs should own the direct batch import route"
|
||||
);
|
||||
|
||||
@@ -2334,7 +2351,9 @@ fn admin_provider_oauth_device_mod_stays_thin() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
!workspace_file_exists("apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/device.rs"),
|
||||
!workspace_file_exists(
|
||||
"apps/aether-gateway/src/handlers/admin/provider/oauth/dispatch/device.rs"
|
||||
),
|
||||
"handlers/admin/provider/oauth/dispatch/device.rs should be removed once device dispatch is directoryized"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,12 +103,15 @@ fn admin_wrapped_state_owns_api_key_and_proxy_capabilities() {
|
||||
"{path} should use AdminAppState encryption capability instead of raw state.app() encryption"
|
||||
);
|
||||
assert!(
|
||||
!contents.contains("decrypt_catalog_secret_with_fallbacks(state.app().encryption_key(),"),
|
||||
!contents
|
||||
.contains("decrypt_catalog_secret_with_fallbacks(state.app().encryption_key(),"),
|
||||
"{path} should use AdminAppState decryption capability instead of raw state.app().encryption_key()"
|
||||
);
|
||||
assert!(
|
||||
!contents.contains("resolve_transport_proxy_snapshot_with_tunnel_affinity(\n state.app(),")
|
||||
&& !contents.contains("resolve_transport_proxy_snapshot_with_tunnel_affinity(state.app(),"),
|
||||
!contents.contains(
|
||||
"resolve_transport_proxy_snapshot_with_tunnel_affinity(\n state.app(),"
|
||||
) && !contents
|
||||
.contains("resolve_transport_proxy_snapshot_with_tunnel_affinity(state.app(),"),
|
||||
"{path} should use AdminAppState proxy capability instead of raw state.app() transport proxy resolution"
|
||||
);
|
||||
}
|
||||
@@ -682,7 +685,8 @@ fn crate_root_exposes_real_admin_and_ai_serving_facades() {
|
||||
|
||||
let ai_serving_api_mod = read_workspace_file("apps/aether-gateway/src/ai_serving/api.rs");
|
||||
assert!(
|
||||
ai_serving_api_mod.contains("use crate::ai_serving::{is_json_request, GatewayControlDecision};"),
|
||||
ai_serving_api_mod
|
||||
.contains("use crate::ai_serving::{is_json_request, GatewayControlDecision};"),
|
||||
"ai_serving/api.rs should depend on the crate-facing ai_serving seam instead of deep internal modules"
|
||||
);
|
||||
}
|
||||
@@ -695,7 +699,7 @@ fn gateway_ai_serving_api_module_delegates_pure_ownership_to_format_crate() {
|
||||
"ai_serving/api.rs should re-export pure ownership through aether_ai_formats::api"
|
||||
);
|
||||
|
||||
let format_crate_api = read_workspace_file("crates/aether-ai-formats/src/api.rs");
|
||||
let format_crate_api = read_workspace_file("crates/aether-ai/formats/src/api.rs");
|
||||
for pattern in [
|
||||
"pub use crate::contracts::{",
|
||||
"pub use crate::provider_compat::kiro_stream::{",
|
||||
|
||||
@@ -29,8 +29,8 @@ fn ai_serving_target_structure_removes_legacy_pipeline_boundary() {
|
||||
let mut violations = Vec::new();
|
||||
for root in [
|
||||
"apps/aether-gateway/src",
|
||||
"crates/aether-ai-serving/src",
|
||||
"crates/aether-ai-formats/src",
|
||||
"crates/aether-ai/serving/src",
|
||||
"crates/aether-ai/formats/src",
|
||||
] {
|
||||
for file in collect_workspace_rust_files(root) {
|
||||
let relative = file
|
||||
@@ -75,7 +75,7 @@ fn ai_serving_target_structure_removes_legacy_pipeline_boundary() {
|
||||
|
||||
#[test]
|
||||
fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
let serving_manifest = read_workspace_file("crates/aether-ai-serving/Cargo.toml");
|
||||
let serving_manifest = read_workspace_file("crates/aether-ai/serving/Cargo.toml");
|
||||
for forbidden in ["axum", "sqlx", "redis", "aether-gateway"] {
|
||||
assert!(
|
||||
!serving_manifest.contains(forbidden),
|
||||
@@ -84,7 +84,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let mut violations = Vec::new();
|
||||
for file in collect_workspace_rust_files("crates/aether-ai-serving/src") {
|
||||
for file in collect_workspace_rust_files("crates/aether-ai/serving/src") {
|
||||
let source = std::fs::read_to_string(&file).expect("source file should be readable");
|
||||
let hits = ["AppState", "axum::", "sqlx::", "redis::"]
|
||||
.iter()
|
||||
@@ -101,7 +101,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
violations.join("\n")
|
||||
);
|
||||
|
||||
let serving_attempt_loop = read_workspace_file("crates/aether-ai-serving/src/attempt_loop.rs");
|
||||
let serving_attempt_loop = read_workspace_file("crates/aether-ai/serving/src/attempt_loop.rs");
|
||||
for pattern in [
|
||||
"pub trait AiExecutionAttempt",
|
||||
"pub trait AiAttemptLoopPort",
|
||||
@@ -129,7 +129,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_decision_path =
|
||||
read_workspace_file("crates/aether-ai-serving/src/decision_path.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/decision_path.rs");
|
||||
for pattern in [
|
||||
"pub enum AiSyncDecisionStep",
|
||||
"pub enum AiStreamDecisionStep",
|
||||
@@ -168,7 +168,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
);
|
||||
}
|
||||
|
||||
let serving_plan_payload = read_workspace_file("crates/aether-ai-serving/src/plan_payload.rs");
|
||||
let serving_plan_payload = read_workspace_file("crates/aether-ai/serving/src/plan_payload.rs");
|
||||
for pattern in [
|
||||
"pub fn build_ai_sync_execution_plan_payload",
|
||||
"pub fn build_ai_stream_execution_plan_payload",
|
||||
@@ -204,7 +204,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
);
|
||||
}
|
||||
|
||||
let serving_attempt_plan = read_workspace_file("crates/aether-ai-serving/src/attempt_plan.rs");
|
||||
let serving_attempt_plan = read_workspace_file("crates/aether-ai/serving/src/attempt_plan.rs");
|
||||
for pattern in [
|
||||
"pub fn build_ai_execution_decision_from_plan",
|
||||
"pub fn infer_ai_upstream_base_url",
|
||||
@@ -238,7 +238,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_candidate_materialization =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_materialization.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_materialization.rs");
|
||||
for pattern in [
|
||||
"pub trait AiCandidateMaterializationPort",
|
||||
"pub async fn run_ai_candidate_materialization",
|
||||
@@ -270,7 +270,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_candidate_preselection =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_preselection.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_preselection.rs");
|
||||
for pattern in [
|
||||
"pub trait AiCandidatePreselectionPort",
|
||||
"pub async fn run_ai_candidate_preselection",
|
||||
@@ -290,7 +290,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_candidate_ranking =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_ranking.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_ranking.rs");
|
||||
for pattern in [
|
||||
"pub trait AiCandidateRankingPort",
|
||||
"pub async fn run_ai_candidate_ranking",
|
||||
@@ -312,7 +312,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_candidate_resolution =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_resolution.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_resolution.rs");
|
||||
for pattern in [
|
||||
"pub trait AiCandidateResolutionPort",
|
||||
"pub async fn run_ai_candidate_resolution",
|
||||
@@ -370,7 +370,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let provider_transport_conversion =
|
||||
read_workspace_file("crates/aether-provider-transport/src/conversion.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/conversion.rs");
|
||||
for pattern in [
|
||||
"pub struct CandidateTransportPolicyFacts",
|
||||
"pub fn candidate_common_transport_skip_reason(",
|
||||
@@ -445,7 +445,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let serving_execution_path =
|
||||
read_workspace_file("crates/aether-ai-serving/src/execution_path.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/execution_path.rs");
|
||||
for pattern in [
|
||||
"pub enum AiSyncExecutionStep",
|
||||
"pub enum AiStreamExecutionStep",
|
||||
@@ -488,7 +488,7 @@ fn ai_serving_crate_owns_attempt_loop_without_gateway_runtime_deps() {
|
||||
|
||||
#[test]
|
||||
fn ai_serving_internal_dtos_use_ai_execution_names() {
|
||||
let serving_dto = read_workspace_file("crates/aether-ai-serving/src/dto.rs");
|
||||
let serving_dto = read_workspace_file("crates/aether-ai/serving/src/dto.rs");
|
||||
for expected in [
|
||||
"pub struct AiExecutionDecision",
|
||||
"pub struct AiExecutionPlanPayload",
|
||||
@@ -525,8 +525,8 @@ fn ai_serving_internal_dtos_use_ai_execution_names() {
|
||||
"apps/aether-gateway/src/ai_serving",
|
||||
"apps/aether-gateway/src/executor",
|
||||
"apps/aether-gateway/src/execution_runtime",
|
||||
"crates/aether-ai-serving/src",
|
||||
"crates/aether-ai-formats/src",
|
||||
"crates/aether-ai/serving/src",
|
||||
"crates/aether-ai/formats/src",
|
||||
] {
|
||||
for file in collect_workspace_rust_files(root) {
|
||||
let relative = file
|
||||
@@ -560,7 +560,7 @@ fn ai_serving_internal_dtos_use_ai_execution_names() {
|
||||
|
||||
#[test]
|
||||
fn ai_format_crate_stays_free_of_gateway_runtime_deps() {
|
||||
for manifest_path in ["crates/aether-ai-formats/Cargo.toml"] {
|
||||
for manifest_path in ["crates/aether-ai/formats/Cargo.toml"] {
|
||||
let manifest = read_workspace_file(manifest_path);
|
||||
for forbidden in [
|
||||
"axum",
|
||||
@@ -578,7 +578,7 @@ fn ai_format_crate_stays_free_of_gateway_runtime_deps() {
|
||||
}
|
||||
|
||||
let mut violations = Vec::new();
|
||||
for root in ["crates/aether-ai-formats/src"] {
|
||||
for root in ["crates/aether-ai/formats/src"] {
|
||||
for file in collect_workspace_rust_files(root) {
|
||||
let source = std::fs::read_to_string(&file).expect("source file should be readable");
|
||||
let hits = [
|
||||
@@ -608,7 +608,7 @@ fn ai_format_crate_stays_free_of_gateway_runtime_deps() {
|
||||
|
||||
#[test]
|
||||
fn aether_runtime_stays_free_of_ai_serving_policy() {
|
||||
let runtime_manifest = read_workspace_file("crates/aether-runtime/Cargo.toml");
|
||||
let runtime_manifest = read_workspace_file("crates/aether-runtime/base/Cargo.toml");
|
||||
for forbidden in [
|
||||
"aether-ai-serving",
|
||||
"aether-ai-formats",
|
||||
@@ -622,7 +622,7 @@ fn aether_runtime_stays_free_of_ai_serving_policy() {
|
||||
}
|
||||
|
||||
let mut violations = Vec::new();
|
||||
for file in collect_workspace_rust_files("crates/aether-runtime/src") {
|
||||
for file in collect_workspace_rust_files("crates/aether-runtime/base/src") {
|
||||
let source = std::fs::read_to_string(&file).expect("source file should be readable");
|
||||
let hits = [
|
||||
"aether_ai_serving",
|
||||
@@ -824,7 +824,7 @@ fn ai_serving_routes_control_and_execution_deps_through_facades() {
|
||||
);
|
||||
}
|
||||
|
||||
let serving_attempt_plan = read_workspace_file("crates/aether-ai-serving/src/attempt_plan.rs");
|
||||
let serving_attempt_plan = read_workspace_file("crates/aether-ai/serving/src/attempt_plan.rs");
|
||||
for pattern in [
|
||||
"pub fn take_ai_decision_plan_core(",
|
||||
"pub fn take_ai_upstream_auth_pair(",
|
||||
@@ -871,13 +871,15 @@ fn ai_serving_routes_control_and_execution_deps_through_facades() {
|
||||
let gateway_finalize_common =
|
||||
read_workspace_file("apps/aether-gateway/src/ai_serving/finalize/common.rs");
|
||||
assert!(
|
||||
gateway_finalize_common
|
||||
.contains("prepare_local_success_response_parts as prepare_local_success_response_parts_impl"),
|
||||
gateway_finalize_common.contains(
|
||||
"prepare_local_success_response_parts as prepare_local_success_response_parts_impl"
|
||||
),
|
||||
"finalize/common.rs should delegate success response-part normalization to the format crate"
|
||||
);
|
||||
assert!(
|
||||
gateway_finalize_common
|
||||
.contains("build_local_success_background_report as build_local_success_background_report_impl"),
|
||||
gateway_finalize_common.contains(
|
||||
"build_local_success_background_report as build_local_success_background_report_impl"
|
||||
),
|
||||
"finalize/common.rs should delegate pure success background-report construction to the format crate"
|
||||
);
|
||||
assert!(
|
||||
@@ -1009,7 +1011,7 @@ fn ai_serving_routes_provider_transport_deps_through_facade() {
|
||||
"ai_serving/runtime should stay removed after facade cleanup"
|
||||
);
|
||||
assert!(
|
||||
!workspace_file_exists("crates/aether-ai-formats/src/transport.rs"),
|
||||
!workspace_file_exists("crates/aether-ai/formats/src/transport.rs"),
|
||||
"aether-ai-formats should not expose a provider transport bridge"
|
||||
);
|
||||
|
||||
@@ -1384,7 +1386,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let serving_lib = read_workspace_file("crates/aether-ai-serving/src/lib.rs");
|
||||
let serving_lib = read_workspace_file("crates/aether-ai/serving/src/lib.rs");
|
||||
for forbidden in ["pub mod pool_scheduler;", "pub mod pool_scores;"] {
|
||||
assert!(
|
||||
!serving_lib.contains(forbidden),
|
||||
@@ -1392,7 +1394,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let provider_pool_lib = read_workspace_file("crates/aether-provider-pool/src/lib.rs");
|
||||
let provider_pool_lib = read_workspace_file("crates/aether-provider/pool/src/lib.rs");
|
||||
for pattern in [
|
||||
"mod capability;",
|
||||
"mod plan;",
|
||||
@@ -1410,7 +1412,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let provider_pool_provider = read_workspace_file("crates/aether-provider-pool/src/provider.rs");
|
||||
let provider_pool_provider = read_workspace_file("crates/aether-provider/pool/src/provider.rs");
|
||||
for pattern in [
|
||||
"pub trait ProviderPoolAdapter",
|
||||
"ProviderPoolMemberInput",
|
||||
@@ -1423,7 +1425,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let provider_pool_service = read_workspace_file("crates/aether-provider-pool/src/service.rs");
|
||||
let provider_pool_service = read_workspace_file("crates/aether-provider/pool/src/service.rs");
|
||||
for pattern in [
|
||||
"pub struct ProviderPoolService",
|
||||
"with_builtin_adapters",
|
||||
@@ -1449,7 +1451,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
|
||||
let provider_pool_providers =
|
||||
read_workspace_file("crates/aether-provider-pool/src/providers/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/pool/src/providers/mod.rs");
|
||||
for pattern in [
|
||||
"pub mod default;",
|
||||
"pub mod unsupported;",
|
||||
@@ -1466,15 +1468,15 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
}
|
||||
for (path, patterns) in [
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/default.rs",
|
||||
"crates/aether-provider/pool/src/providers/default.rs",
|
||||
vec!["DefaultProviderPoolAdapter"],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/antigravity.rs",
|
||||
"crates/aether-provider/pool/src/providers/antigravity.rs",
|
||||
vec!["AntigravityProviderPoolAdapter"],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/codex.rs",
|
||||
"crates/aether-provider/pool/src/providers/codex.rs",
|
||||
vec![
|
||||
"CodexProviderPoolAdapter",
|
||||
"recent_refresh",
|
||||
@@ -1482,7 +1484,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/gemini_cli.rs",
|
||||
"crates/aether-provider/pool/src/providers/gemini_cli.rs",
|
||||
vec![
|
||||
"GeminiCliProviderPoolAdapter",
|
||||
"build_gemini_cli_pool_quota_request",
|
||||
@@ -1490,11 +1492,11 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/kiro.rs",
|
||||
"crates/aether-provider/pool/src/providers/kiro.rs",
|
||||
vec!["KiroProviderPoolAdapter", "quota_exhausted_from_bucket"],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/chatgpt_web.rs",
|
||||
"crates/aether-provider/pool/src/providers/chatgpt_web.rs",
|
||||
vec![
|
||||
"ChatGptWebProviderPoolAdapter",
|
||||
"build_chatgpt_web_pool_quota_request",
|
||||
@@ -1504,7 +1506,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
],
|
||||
),
|
||||
(
|
||||
"crates/aether-provider-pool/src/providers/unsupported.rs",
|
||||
"crates/aether-provider/pool/src/providers/unsupported.rs",
|
||||
vec![
|
||||
"UnsupportedQuotaProviderPoolAdapter",
|
||||
"CLAUDE_CODE_PROVIDER_POOL_ADAPTER",
|
||||
@@ -1521,7 +1523,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
}
|
||||
}
|
||||
|
||||
let provider_pool_plan = read_workspace_file("crates/aether-provider-pool/src/plan.rs");
|
||||
let provider_pool_plan = read_workspace_file("crates/aether-provider/pool/src/plan.rs");
|
||||
for pattern in ["normalize_provider_plan_tier", "derive_plan_tier"] {
|
||||
assert!(
|
||||
provider_pool_plan.contains(pattern),
|
||||
@@ -1529,7 +1531,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let provider_pool_quota = read_workspace_file("crates/aether-provider-pool/src/quota.rs");
|
||||
let provider_pool_quota = read_workspace_file("crates/aether-provider/pool/src/quota.rs");
|
||||
for pattern in [
|
||||
"provider_pool_key_account_quota_exhausted",
|
||||
"provider_pool_member_quota_snapshot",
|
||||
@@ -1544,7 +1546,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
);
|
||||
}
|
||||
|
||||
let provider_pool_presets = read_workspace_file("crates/aether-provider-pool/src/presets.rs");
|
||||
let provider_pool_presets = read_workspace_file("crates/aether-provider/pool/src/presets.rs");
|
||||
for pattern in [
|
||||
"normalize_provider_scheduling_presets",
|
||||
"build_admin_pool_scheduling_presets_payload",
|
||||
@@ -1561,7 +1563,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
"plan_priority_score(",
|
||||
] {
|
||||
let mut violations = Vec::new();
|
||||
for file in collect_workspace_rust_files("crates/aether-provider-pool/src") {
|
||||
for file in collect_workspace_rust_files("crates/aether-provider/pool/src") {
|
||||
let source = std::fs::read_to_string(&file).expect("source file should be readable");
|
||||
if source.contains(forbidden) {
|
||||
violations.push(file.display().to_string());
|
||||
@@ -1578,7 +1580,7 @@ fn ai_serving_planner_separates_local_candidate_resolution_from_ranking() {
|
||||
#[test]
|
||||
fn ai_serving_candidate_preparation_owns_shared_auth_and_mapped_model_resolution() {
|
||||
let serving_candidate_preparation =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_preparation.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_preparation.rs");
|
||||
for pattern in [
|
||||
"pub struct AiPreparedHeaderAuthenticatedCandidate",
|
||||
"pub fn prepare_ai_header_authenticated_candidate(",
|
||||
@@ -1592,7 +1594,7 @@ fn ai_serving_candidate_preparation_owns_shared_auth_and_mapped_model_resolution
|
||||
);
|
||||
}
|
||||
|
||||
let serving_lib = read_workspace_file("crates/aether-ai-serving/src/lib.rs");
|
||||
let serving_lib = read_workspace_file("crates/aether-ai/serving/src/lib.rs");
|
||||
for pattern in [
|
||||
"pub mod candidate_preparation;",
|
||||
"prepare_ai_header_authenticated_candidate",
|
||||
@@ -1690,7 +1692,7 @@ fn ai_serving_candidate_materialization_owns_affinity_and_candidate_runtime_pers
|
||||
);
|
||||
|
||||
let serving_candidate_persistence =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_persistence.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_persistence.rs");
|
||||
for pattern in [
|
||||
"pub trait AiAvailableCandidatePersistencePort",
|
||||
"pub async fn run_ai_available_candidate_persistence",
|
||||
@@ -1837,7 +1839,7 @@ fn ai_serving_materialization_policy_owns_local_candidate_persistence_modes() {
|
||||
);
|
||||
|
||||
let serving_candidate_persistence_policy =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_persistence_policy.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_persistence_policy.rs");
|
||||
for pattern in [
|
||||
"pub enum AiCandidatePersistencePolicyKind {",
|
||||
"pub struct AiCandidatePersistencePolicySpec {",
|
||||
@@ -1913,9 +1915,9 @@ fn ai_serving_candidate_metadata_owns_local_execution_candidate_extra_data_shape
|
||||
let candidate_metadata =
|
||||
read_workspace_file("apps/aether-gateway/src/ai_serving/planner/candidate_metadata.rs");
|
||||
let serving_ranking_metadata =
|
||||
read_workspace_file("crates/aether-ai-serving/src/ranking_metadata.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/ranking_metadata.rs");
|
||||
let serving_candidate_metadata =
|
||||
read_workspace_file("crates/aether-ai-serving/src/candidate_metadata.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/candidate_metadata.rs");
|
||||
for pattern in [
|
||||
"pub struct AiCandidateMetadataParts<'a> {",
|
||||
"pub fn build_ai_candidate_metadata(",
|
||||
@@ -2015,7 +2017,7 @@ fn ai_serving_runtime_miss_owns_local_execution_miss_state_machine() {
|
||||
"planner/mod.rs should wire runtime_miss helper module"
|
||||
);
|
||||
|
||||
let serving_runtime_miss = read_workspace_file("crates/aether-ai-serving/src/runtime_miss.rs");
|
||||
let serving_runtime_miss = read_workspace_file("crates/aether-ai/serving/src/runtime_miss.rs");
|
||||
for pattern in [
|
||||
"pub trait AiRuntimeMissDiagnosticPort",
|
||||
"pub trait AiRuntimeMissDiagnosticFields",
|
||||
@@ -2264,7 +2266,7 @@ fn ai_serving_video_routes_request_preparation_through_request_payload_seams() {
|
||||
}
|
||||
|
||||
let provider_transport_video =
|
||||
read_workspace_file("crates/aether-provider-transport/src/video/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/video/mod.rs");
|
||||
for pattern in [
|
||||
"pub enum ProviderVideoCreateFamily",
|
||||
"pub fn video_create_transport_unsupported_reason(",
|
||||
@@ -2339,7 +2341,7 @@ fn ai_serving_files_routes_request_preparation_through_request_payload_seams() {
|
||||
}
|
||||
|
||||
let provider_transport_files =
|
||||
read_workspace_file("crates/aether-provider-transport/src/gemini_files/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/gemini_files/mod.rs");
|
||||
for pattern in [
|
||||
"pub fn gemini_files_transport_unsupported_reason(",
|
||||
"pub fn resolve_gemini_files_auth(",
|
||||
@@ -2409,7 +2411,7 @@ fn ai_serving_image_routes_split_surface_normalization_and_transport_policy() {
|
||||
}
|
||||
|
||||
let surface_image =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/openai/image/request.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/openai/image/request.rs");
|
||||
for pattern in [
|
||||
"pub enum OpenAiImageOperation",
|
||||
"pub fn is_openai_image_stream_request(",
|
||||
@@ -2425,7 +2427,7 @@ fn ai_serving_image_routes_split_surface_normalization_and_transport_policy() {
|
||||
}
|
||||
|
||||
let provider_transport_image =
|
||||
read_workspace_file("crates/aether-provider-transport/src/openai_image/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/openai_image/mod.rs");
|
||||
for pattern in [
|
||||
"pub fn openai_image_transport_unsupported_reason(",
|
||||
"pub fn resolve_openai_image_auth(",
|
||||
@@ -2605,7 +2607,7 @@ fn ai_serving_payload_metadata_owns_local_execution_decision_response_shape() {
|
||||
"gateway payload_metadata.rs should be removed after serving extraction"
|
||||
);
|
||||
|
||||
let decision_payload = read_workspace_file("crates/aether-ai-serving/src/decision_payload.rs");
|
||||
let decision_payload = read_workspace_file("crates/aether-ai/serving/src/decision_payload.rs");
|
||||
for pattern in [
|
||||
"pub struct AiExecutionDecisionResponseParts {",
|
||||
"pub fn build_ai_execution_decision_response(",
|
||||
@@ -2651,7 +2653,7 @@ fn ai_serving_owns_pure_planner_diagnostics_and_execution_labels() {
|
||||
}
|
||||
|
||||
let serving_failure_diagnostic =
|
||||
read_workspace_file("crates/aether-ai-serving/src/failure_diagnostic.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/failure_diagnostic.rs");
|
||||
for pattern in [
|
||||
"pub enum CandidateFailureDiagnosticKind {",
|
||||
"pub struct CandidateFailureDiagnostic {",
|
||||
@@ -2667,7 +2669,7 @@ fn ai_serving_owns_pure_planner_diagnostics_and_execution_labels() {
|
||||
}
|
||||
|
||||
let serving_request_body_diagnostics =
|
||||
read_workspace_file("crates/aether-ai-serving/src/request_body_diagnostics.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/request_body_diagnostics.rs");
|
||||
for pattern in [
|
||||
"pub fn request_body_build_failure_extra_data(",
|
||||
"pub fn same_format_provider_request_body_failure_extra_data(",
|
||||
@@ -2692,7 +2694,7 @@ fn ai_serving_owns_pure_planner_diagnostics_and_execution_labels() {
|
||||
"gateway standard planner should consume request-body diagnostics from aether-ai-serving"
|
||||
);
|
||||
|
||||
let serving_dto = read_workspace_file("crates/aether-ai-serving/src/dto.rs");
|
||||
let serving_dto = read_workspace_file("crates/aether-ai/serving/src/dto.rs");
|
||||
for pattern in ["pub enum ExecutionStrategy", "pub enum ConversionMode"] {
|
||||
assert!(
|
||||
serving_dto.contains(pattern),
|
||||
@@ -2726,7 +2728,7 @@ fn ai_serving_report_context_owns_local_execution_context_shape() {
|
||||
);
|
||||
|
||||
let serving_report_context =
|
||||
read_workspace_file("crates/aether-ai-serving/src/report_context.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/report_context.rs");
|
||||
for pattern in [
|
||||
"pub struct AiExecutionReportContextParts<'a> {",
|
||||
"pub fn build_ai_execution_report_context(",
|
||||
@@ -2878,7 +2880,7 @@ fn ai_serving_standard_attempts_consume_eligible_local_candidates_without_transp
|
||||
}
|
||||
|
||||
let provider_transport_standard =
|
||||
read_workspace_file("crates/aether-provider-transport/src/standard/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/standard/mod.rs");
|
||||
for pattern in [
|
||||
"pub struct StandardProviderRequestHeadersInput",
|
||||
"pub struct StandardProviderRequestHeaders",
|
||||
@@ -2898,7 +2900,7 @@ fn ai_serving_standard_attempts_consume_eligible_local_candidates_without_transp
|
||||
}
|
||||
|
||||
let provider_transport_request_url =
|
||||
read_workspace_file("crates/aether-provider-transport/src/request_url/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/request_url/mod.rs");
|
||||
assert!(
|
||||
provider_transport_request_url.contains("pub fn build_kiro_cross_format_upstream_url("),
|
||||
"provider-transport request_url.rs should own Kiro cross-format URL hook"
|
||||
@@ -2960,7 +2962,7 @@ fn ai_serving_standard_plan_builders_delegate_fallback_transport_policy() {
|
||||
}
|
||||
|
||||
let provider_transport_standard =
|
||||
read_workspace_file("crates/aether-provider-transport/src/standard/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/standard/mod.rs");
|
||||
for pattern in [
|
||||
"pub enum StandardPlanFallbackAcceptPolicy",
|
||||
"pub struct StandardPlanFallbackHeadersInput",
|
||||
@@ -3063,7 +3065,7 @@ fn ai_serving_spec_metadata_owns_family_requested_model_and_plan_builder_routing
|
||||
|
||||
let spec_metadata =
|
||||
read_workspace_file("apps/aether-gateway/src/ai_serving/planner/spec_metadata.rs");
|
||||
let serving_surface_spec = read_workspace_file("crates/aether-ai-serving/src/surface_spec.rs");
|
||||
let serving_surface_spec = read_workspace_file("crates/aether-ai/serving/src/surface_spec.rs");
|
||||
for pattern in [
|
||||
"pub enum AiRequestedModelFamily {",
|
||||
"pub struct AiExecutionSurfaceSpecMetadata {",
|
||||
@@ -3228,7 +3230,7 @@ fn ai_serving_same_format_provider_request_policy_owns_provider_type_behavior()
|
||||
"apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request/policy.rs",
|
||||
);
|
||||
let provider_transport_policy =
|
||||
read_workspace_file("crates/aether-provider-transport/src/same_format_provider/mod.rs");
|
||||
read_workspace_file("crates/aether-provider/transport/src/same_format_provider/mod.rs");
|
||||
for pattern in [
|
||||
"pub struct SameFormatProviderRequestBehavior {",
|
||||
"pub struct SameFormatProviderRequestBodyInput",
|
||||
@@ -3314,7 +3316,7 @@ fn ai_serving_decision_inputs_share_authenticated_input_helper() {
|
||||
);
|
||||
|
||||
let serving_decision_input =
|
||||
read_workspace_file("crates/aether-ai-serving/src/decision_input.rs");
|
||||
read_workspace_file("crates/aether-ai/serving/src/decision_input.rs");
|
||||
for pattern in [
|
||||
"pub trait AiAuthenticatedDecisionInputPort",
|
||||
"pub async fn run_ai_authenticated_decision_input",
|
||||
@@ -3488,7 +3490,8 @@ fn ai_serving_leaf_planner_owners_route_contract_specs_through_gateway_seams() {
|
||||
"apps/aether-gateway/src/ai_serving/planner/specialized/video/support.rs",
|
||||
);
|
||||
assert!(
|
||||
specialized_video_support.contains("use super::{LocalVideoCreateFamily, LocalVideoCreateSpec};"),
|
||||
specialized_video_support
|
||||
.contains("use super::{LocalVideoCreateFamily, LocalVideoCreateSpec};"),
|
||||
"planner/specialized/video/support.rs should use local video seams for LocalVideoCreate* types"
|
||||
);
|
||||
}
|
||||
@@ -3496,10 +3499,10 @@ fn ai_serving_leaf_planner_owners_route_contract_specs_through_gateway_seams() {
|
||||
#[test]
|
||||
fn ai_serving_m5_moves_contracts_and_route_logic_into_format_crate() {
|
||||
for path in [
|
||||
"crates/aether-ai-formats/src/contracts/actions.rs",
|
||||
"crates/aether-ai-formats/src/contracts/plan_kinds.rs",
|
||||
"crates/aether-ai-formats/src/contracts/report_kinds.rs",
|
||||
"crates/aether-ai-formats/src/formats/shared/routing.rs",
|
||||
"crates/aether-ai/formats/src/contracts/actions.rs",
|
||||
"crates/aether-ai/formats/src/contracts/plan_kinds.rs",
|
||||
"crates/aether-ai/formats/src/contracts/report_kinds.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/routing.rs",
|
||||
] {
|
||||
assert!(
|
||||
workspace_file_exists(path),
|
||||
@@ -3561,7 +3564,7 @@ fn ai_serving_m5_moves_contracts_and_route_logic_into_format_crate() {
|
||||
}
|
||||
|
||||
let surface_route =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/shared/routing.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/shared/routing.rs");
|
||||
for pattern in [
|
||||
"pub fn is_matching_stream_http_request(",
|
||||
"is_openai_image_stream_request(parts, body_json, body_base64)",
|
||||
@@ -3614,12 +3617,12 @@ fn ai_serving_m5_moves_contracts_and_route_logic_into_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_m5_moves_kiro_stream_helpers_into_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/provider_compat/kiro_stream.rs"),
|
||||
"crates/aether-ai-formats/src/provider_compat/kiro_stream.rs should exist after kiro helper extraction"
|
||||
workspace_file_exists("crates/aether-ai/formats/src/provider_compat/kiro_stream.rs"),
|
||||
"crates/aether-ai/formats/src/provider_compat/kiro_stream.rs should exist after kiro helper extraction"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/provider_compat/kiro_stream/state.rs"),
|
||||
"crates/aether-ai-formats/src/provider_compat/kiro_stream/state.rs should own the Kiro stream state machine"
|
||||
workspace_file_exists("crates/aether-ai/formats/src/provider_compat/kiro_stream/state.rs"),
|
||||
"crates/aether-ai/formats/src/provider_compat/kiro_stream/state.rs should own the Kiro stream state machine"
|
||||
);
|
||||
|
||||
for path in [
|
||||
@@ -3633,7 +3636,7 @@ fn ai_serving_m5_moves_kiro_stream_helpers_into_format_crate() {
|
||||
);
|
||||
}
|
||||
|
||||
let surface_api = read_workspace_file("crates/aether-ai-formats/src/api.rs");
|
||||
let surface_api = read_workspace_file("crates/aether-ai/formats/src/api.rs");
|
||||
assert!(
|
||||
surface_api.contains("KiroToClaudeCliStreamState"),
|
||||
"aether-ai-formats api should export KiroToClaudeCliStreamState"
|
||||
@@ -3670,7 +3673,7 @@ fn ai_serving_m5_moves_kiro_stream_helpers_into_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_private_envelope_stream_normalizer_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/provider_compat/private_envelope.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/provider_compat/private_envelope.rs"),
|
||||
"surface private envelope adapter should exist"
|
||||
);
|
||||
assert!(
|
||||
@@ -3687,7 +3690,7 @@ fn ai_serving_private_envelope_stream_normalizer_is_owned_by_format_crate() {
|
||||
);
|
||||
|
||||
let surface_private_envelope =
|
||||
read_workspace_file("crates/aether-ai-formats/src/provider_compat/private_envelope.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/provider_compat/private_envelope.rs");
|
||||
for expected in [
|
||||
"pub struct ProviderPrivateStreamNormalizer",
|
||||
"pub fn maybe_build_provider_private_stream_normalizer",
|
||||
@@ -3810,11 +3813,11 @@ fn ai_serving_error_body_is_owned_by_format_finalize_module() {
|
||||
"ai_serving/conversion/error.rs should stay removed"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/error_body.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/error_body.rs"),
|
||||
"format error response-body helpers should live under finalize/error_body.rs"
|
||||
);
|
||||
assert!(
|
||||
!workspace_file_exists("crates/aether-ai-formats/src/formats/conversion/error.rs"),
|
||||
!workspace_file_exists("crates/aether-ai/formats/src/formats/conversion/error.rs"),
|
||||
"aether-ai-formats should not keep error response-body helpers under conversion"
|
||||
);
|
||||
|
||||
@@ -3839,7 +3842,7 @@ fn ai_serving_error_body_is_owned_by_format_finalize_module() {
|
||||
#[test]
|
||||
fn ai_serving_conversion_request_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/conversion/request.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/conversion/request.rs"),
|
||||
"request conversion should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
@@ -3864,7 +3867,7 @@ fn ai_serving_conversion_request_is_owned_by_format_crate() {
|
||||
"gateway ai_serving/mod.rs should not keep request re-export shell after root-seam consolidation"
|
||||
);
|
||||
|
||||
let surface_api = read_workspace_file("crates/aether-ai-formats/src/api.rs");
|
||||
let surface_api = read_workspace_file("crates/aether-ai/formats/src/api.rs");
|
||||
assert!(
|
||||
surface_api.contains("pub use aether_ai_formats::formats::conversion::request::{"),
|
||||
"format API facade should re-export request conversion directly from aether-ai-formats"
|
||||
@@ -3874,7 +3877,7 @@ fn ai_serving_conversion_request_is_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_conversion_response_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/conversion/response.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/conversion/response.rs"),
|
||||
"response conversion should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
@@ -3899,7 +3902,7 @@ fn ai_serving_conversion_response_is_owned_by_format_crate() {
|
||||
"gateway ai_serving/mod.rs should not keep response re-export shell after root-seam consolidation"
|
||||
);
|
||||
|
||||
let surface_api = read_workspace_file("crates/aether-ai-formats/src/api.rs");
|
||||
let surface_api = read_workspace_file("crates/aether-ai/formats/src/api.rs");
|
||||
assert!(
|
||||
surface_api.contains("pub use aether_ai_formats::formats::conversion::response::{"),
|
||||
"format API facade should re-export response conversion directly from aether-ai-formats"
|
||||
@@ -3909,17 +3912,17 @@ fn ai_serving_conversion_response_is_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_format_crate_owns_conversion_and_surface_facade() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/conversion"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/conversion"),
|
||||
"aether-ai-formats should own the conversion directory"
|
||||
);
|
||||
|
||||
let surface_lib = read_workspace_file("crates/aether-ai-formats/src/lib.rs");
|
||||
let surface_lib = read_workspace_file("crates/aether-ai/formats/src/lib.rs");
|
||||
assert!(
|
||||
surface_lib.contains("pub mod protocol;"),
|
||||
"aether-ai-formats lib.rs should expose the protocol module"
|
||||
);
|
||||
|
||||
let surface_api = read_workspace_file("crates/aether-ai-formats/src/api.rs");
|
||||
let surface_api = read_workspace_file("crates/aether-ai/formats/src/api.rs");
|
||||
for pattern in [
|
||||
"pub use aether_ai_formats::{",
|
||||
"pub use aether_ai_formats::formats::conversion::request::{",
|
||||
@@ -4003,12 +4006,12 @@ fn ai_serving_finalize_standard_sync_response_converters_are_owned_by_format_cra
|
||||
#[test]
|
||||
fn ai_serving_finalize_stream_engine_is_owned_by_format_crate() {
|
||||
for path in [
|
||||
"crates/aether-ai-formats/src/formats/shared/sse.rs",
|
||||
"crates/aether-ai-formats/src/formats/shared/stream_core/common.rs",
|
||||
"crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs",
|
||||
"crates/aether-ai-formats/src/formats/openai/chat/stream.rs",
|
||||
"crates/aether-ai-formats/src/formats/claude/messages/stream.rs",
|
||||
"crates/aether-ai-formats/src/formats/gemini/generate_content/stream.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/sse.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/stream_core/common.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/stream_core/format_matrix.rs",
|
||||
"crates/aether-ai/formats/src/formats/openai/chat/stream.rs",
|
||||
"crates/aether-ai/formats/src/formats/claude/messages/stream.rs",
|
||||
"crates/aether-ai/formats/src/formats/gemini/generate_content/stream.rs",
|
||||
] {
|
||||
assert!(
|
||||
workspace_file_exists(path),
|
||||
@@ -4044,7 +4047,7 @@ fn ai_serving_finalize_stream_engine_is_owned_by_format_crate() {
|
||||
}
|
||||
|
||||
let surface_format_matrix = read_workspace_file(
|
||||
"crates/aether-ai-formats/src/formats/shared/stream_core/format_matrix.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/stream_core/format_matrix.rs",
|
||||
);
|
||||
for pattern in [
|
||||
"pub struct StreamingStandardFormatMatrix",
|
||||
@@ -4068,16 +4071,16 @@ fn ai_serving_finalize_stream_engine_is_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_finalize_standard_sync_products_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/sync_products.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/sync_products.rs"),
|
||||
"finalize sync_products should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/sync_to_stream.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/sync_to_stream.rs"),
|
||||
"finalize sync-to-stream bridge should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
let surface_sync_products =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/shared/sync_products.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/shared/sync_products.rs");
|
||||
for expected in [
|
||||
"pub fn maybe_build_standard_cross_format_sync_product_from_normalized_payload(",
|
||||
"pub fn maybe_build_standard_same_format_sync_body_from_normalized_payload(",
|
||||
@@ -4167,9 +4170,8 @@ fn ai_serving_finalize_standard_sync_products_are_owned_by_format_crate() {
|
||||
"apps/aether-gateway/src/ai_serving/finalize/internal/sync_finalize.rs",
|
||||
);
|
||||
assert!(
|
||||
gateway_internal_sync.contains(
|
||||
"maybe_build_standard_sync_finalize_product_from_normalized_payload"
|
||||
),
|
||||
gateway_internal_sync
|
||||
.contains("maybe_build_standard_sync_finalize_product_from_normalized_payload"),
|
||||
"gateway internal/sync_finalize.rs should delegate normalized standard sync finalize dispatch to aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
@@ -4204,7 +4206,7 @@ fn ai_serving_finalize_standard_sync_products_are_owned_by_format_crate() {
|
||||
}
|
||||
|
||||
let surface_openai_image_stream =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/openai/image/stream.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/openai/image/stream.rs");
|
||||
for expected in [
|
||||
"pub fn maybe_build_openai_image_sync_finalize_product(",
|
||||
"pub struct OpenAiImageSyncFinalizeProduct",
|
||||
@@ -4219,7 +4221,7 @@ fn ai_serving_finalize_standard_sync_products_are_owned_by_format_crate() {
|
||||
}
|
||||
|
||||
let surface_sync_to_stream =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/shared/sync_to_stream.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/shared/sync_to_stream.rs");
|
||||
for expected in [
|
||||
"pub fn maybe_bridge_standard_sync_json_to_stream(",
|
||||
"pub struct SyncToStreamBridgeOutcome",
|
||||
@@ -4256,11 +4258,11 @@ fn ai_serving_finalize_standard_sync_products_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_finalize_stream_rewrite_matrix_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/stream_rewrite.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/stream_rewrite.rs"),
|
||||
"finalize stream rewrite matrix should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/openai/image/stream.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/openai/image/stream.rs"),
|
||||
"OpenAI image stream rewrite state should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4309,7 +4311,7 @@ fn ai_serving_finalize_stream_rewrite_matrix_is_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_planner_common_parser_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/request.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/request.rs"),
|
||||
"planner/common pure parser should exist in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4325,8 +4327,9 @@ fn ai_serving_planner_common_parser_is_owned_by_format_crate() {
|
||||
"gateway planner/common.rs should delegate body parsing through the ai_serving root seam"
|
||||
);
|
||||
assert!(
|
||||
gateway_common_runtime
|
||||
.contains("force_upstream_streaming_for_provider as force_upstream_streaming_for_provider_impl"),
|
||||
gateway_common_runtime.contains(
|
||||
"force_upstream_streaming_for_provider as force_upstream_streaming_for_provider_impl"
|
||||
),
|
||||
"gateway planner/common.rs should delegate upstream streaming policy through the ai_serving root seam"
|
||||
);
|
||||
for forbidden in [
|
||||
@@ -4404,7 +4407,8 @@ fn ai_serving_planner_common_parser_is_owned_by_format_crate() {
|
||||
);
|
||||
assert!(
|
||||
openai_chat_diagnostic.contains("set_local_runtime_miss_diagnostic_reason(")
|
||||
|| openai_chat_diagnostic.contains("set_local_runtime_candidate_evaluation_diagnostic("),
|
||||
|| openai_chat_diagnostic
|
||||
.contains("set_local_runtime_candidate_evaluation_diagnostic("),
|
||||
"openai chat diagnostic.rs should delegate miss diagnostic handling through planner/runtime_miss.rs"
|
||||
);
|
||||
assert!(
|
||||
@@ -4415,7 +4419,7 @@ fn ai_serving_planner_common_parser_is_owned_by_format_crate() {
|
||||
|
||||
#[test]
|
||||
fn ai_serving_root_owns_shared_gemini_request_path_parser() {
|
||||
let serving_surface_spec = read_workspace_file("crates/aether-ai-serving/src/surface_spec.rs");
|
||||
let serving_surface_spec = read_workspace_file("crates/aether-ai/serving/src/surface_spec.rs");
|
||||
assert!(
|
||||
serving_surface_spec.contains("pub fn extract_ai_gemini_model_from_path("),
|
||||
"aether-ai-serving should own shared gemini request-path parsing"
|
||||
@@ -4423,9 +4427,8 @@ fn ai_serving_root_owns_shared_gemini_request_path_parser() {
|
||||
|
||||
let ai_serving_mod = read_workspace_file("apps/aether-gateway/src/ai_serving/mod.rs");
|
||||
assert!(
|
||||
ai_serving_mod.contains(
|
||||
"extract_ai_gemini_model_from_path as extract_gemini_model_from_path"
|
||||
),
|
||||
ai_serving_mod
|
||||
.contains("extract_ai_gemini_model_from_path as extract_gemini_model_from_path"),
|
||||
"ai_serving/mod.rs should expose shared gemini request-path parsing through the serving seam"
|
||||
);
|
||||
|
||||
@@ -4452,7 +4455,7 @@ fn ai_serving_root_owns_shared_gemini_request_path_parser() {
|
||||
#[test]
|
||||
fn ai_serving_planner_standard_normalize_is_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/standard_normalize.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/standard_normalize.rs"),
|
||||
"planner/standard/normalize should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4515,7 +4518,7 @@ fn ai_serving_planner_standard_normalize_is_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_openai_helpers_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/openai/shared.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/openai/shared.rs"),
|
||||
"planner/openai helper owner should exist in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4548,17 +4551,17 @@ fn ai_serving_openai_helpers_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_standard_matrix_delegates_format_conversion_to_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/request_matrix.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/request_matrix.rs"),
|
||||
"planner/matrix facade should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/standard_matrix.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/standard_matrix.rs"),
|
||||
"format standard request-body planner should live in aether-ai-formats"
|
||||
);
|
||||
for path in [
|
||||
"crates/aether-ai-formats/src/protocol/canonical.rs",
|
||||
"crates/aether-ai-formats/src/formats/matrix.rs",
|
||||
"crates/aether-ai-formats/src/formats/registry.rs",
|
||||
"crates/aether-ai/formats/src/protocol/canonical.rs",
|
||||
"crates/aether-ai/formats/src/formats/matrix.rs",
|
||||
"crates/aether-ai/formats/src/formats/registry.rs",
|
||||
] {
|
||||
assert!(
|
||||
workspace_file_exists(path),
|
||||
@@ -4566,7 +4569,7 @@ fn ai_serving_standard_matrix_delegates_format_conversion_to_format_crate() {
|
||||
);
|
||||
}
|
||||
let surface_matrix =
|
||||
read_workspace_file("crates/aether-ai-formats/src/formats/shared/standard_matrix.rs");
|
||||
read_workspace_file("crates/aether-ai/formats/src/formats/shared/standard_matrix.rs");
|
||||
assert!(
|
||||
surface_matrix.contains("use aether_ai_formats::formats::registry::{")
|
||||
&& surface_matrix.contains("convert_request")
|
||||
@@ -4618,26 +4621,26 @@ fn ai_serving_standard_matrix_delegates_format_conversion_to_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_standard_family_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/family.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/family.rs"),
|
||||
"planner/standard/family pure spec owner should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/claude/messages/chat_spec.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/claude/messages/chat_spec.rs"),
|
||||
"planner/standard/claude/chat pure spec resolver should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/claude/messages/cli_spec.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/claude/messages/cli_spec.rs"),
|
||||
"planner/standard/claude/cli pure spec resolver should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists(
|
||||
"crates/aether-ai-formats/src/formats/gemini/generate_content/chat_spec.rs"
|
||||
"crates/aether-ai/formats/src/formats/gemini/generate_content/chat_spec.rs"
|
||||
),
|
||||
"planner/standard/gemini/chat pure spec resolver should live in aether-ai-formats"
|
||||
);
|
||||
assert!(
|
||||
workspace_file_exists(
|
||||
"crates/aether-ai-formats/src/formats/gemini/generate_content/cli_spec.rs"
|
||||
"crates/aether-ai/formats/src/formats/gemini/generate_content/cli_spec.rs"
|
||||
),
|
||||
"planner/standard/gemini/cli pure spec resolver should live in aether-ai-formats"
|
||||
);
|
||||
@@ -4705,7 +4708,7 @@ fn ai_serving_standard_family_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_same_format_provider_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/passthrough.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/passthrough.rs"),
|
||||
"planner/passthrough/provider pure spec owner should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4756,7 +4759,7 @@ fn ai_serving_same_format_provider_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_passthrough_provider_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/passthrough.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/passthrough.rs"),
|
||||
"planner/passthrough/provider pure spec owner should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4801,7 +4804,7 @@ fn ai_serving_passthrough_provider_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_specialized_files_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/gemini/files/spec.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/gemini/files/spec.rs"),
|
||||
"planner/specialized/files pure spec owner should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4831,12 +4834,12 @@ fn ai_serving_specialized_files_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_specialized_video_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/shared/video.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/shared/video.rs"),
|
||||
"planner/specialized/video shared spec seam should live in aether-ai-formats"
|
||||
);
|
||||
for path in [
|
||||
"crates/aether-ai-formats/src/formats/openai/video/spec.rs",
|
||||
"crates/aether-ai-formats/src/formats/gemini/video/spec.rs",
|
||||
"crates/aether-ai/formats/src/formats/openai/video/spec.rs",
|
||||
"crates/aether-ai/formats/src/formats/gemini/video/spec.rs",
|
||||
] {
|
||||
assert!(
|
||||
workspace_file_exists(path),
|
||||
@@ -4868,7 +4871,7 @@ fn ai_serving_specialized_video_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_openai_responses_specs_are_owned_by_format_crate() {
|
||||
assert!(
|
||||
workspace_file_exists("crates/aether-ai-formats/src/formats/openai/responses/spec.rs"),
|
||||
workspace_file_exists("crates/aether-ai/formats/src/formats/openai/responses/spec.rs"),
|
||||
"planner/standard/openai_responses pure spec owner should live in aether-ai-formats"
|
||||
);
|
||||
|
||||
@@ -4908,9 +4911,9 @@ fn ai_serving_openai_responses_specs_are_owned_by_format_crate() {
|
||||
#[test]
|
||||
fn ai_serving_legacy_api_format_names_stay_out_of_primary_paths() {
|
||||
for path in [
|
||||
"crates/aether-ai-formats/src/contracts/plan_kinds.rs",
|
||||
"crates/aether-ai-formats/src/formats/shared/routing.rs",
|
||||
"crates/aether-ai-formats/src/formats/openai/responses/spec.rs",
|
||||
"crates/aether-ai/formats/src/contracts/plan_kinds.rs",
|
||||
"crates/aether-ai/formats/src/formats/shared/routing.rs",
|
||||
"crates/aether-ai/formats/src/formats/openai/responses/spec.rs",
|
||||
"apps/aether-gateway/src/ai_serving/planner/decision/control_plan.rs",
|
||||
"apps/aether-gateway/src/execution_runtime/fallback.rs",
|
||||
] {
|
||||
@@ -4934,7 +4937,7 @@ fn ai_serving_legacy_api_format_names_stay_out_of_primary_paths() {
|
||||
}
|
||||
}
|
||||
|
||||
let registry = read_workspace_file("crates/aether-ai-formats/src/formats/registry.rs");
|
||||
let registry = read_workspace_file("crates/aether-ai/formats/src/formats/registry.rs");
|
||||
let implementation = registry
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
@@ -4969,12 +4972,12 @@ fn retired_api_format_occurrences_are_whitelisted() {
|
||||
"apps/aether-gateway/src/handlers/admin/provider/write/normalize.rs",
|
||||
"apps/aether-gateway/src/handlers/admin/request/system/import.rs",
|
||||
"apps/aether-gateway/src/tests/control/admin/system_import.rs",
|
||||
"crates/aether-ai-formats/src/formats/id.rs",
|
||||
"crates/aether-ai-formats/src/formats/matrix.rs",
|
||||
"crates/aether-ai-formats/src/formats/registry.rs",
|
||||
"crates/aether-data/src/migrate.rs",
|
||||
"crates/aether-data/src/lifecycle/migrate/tests.rs",
|
||||
"crates/aether-usage-runtime/src/report.rs",
|
||||
"crates/aether-ai/formats/src/formats/id.rs",
|
||||
"crates/aether-ai/formats/src/formats/matrix.rs",
|
||||
"crates/aether-ai/formats/src/formats/registry.rs",
|
||||
"crates/aether-data/runtime/src/migrate.rs",
|
||||
"crates/aether-data/runtime/src/lifecycle/migrate/tests.rs",
|
||||
"crates/aether-usage/runtime/src/report.rs",
|
||||
"frontend/src/api/endpoints/types/__tests__/api-format.spec.ts",
|
||||
];
|
||||
let allowed = allowed_paths
|
||||
|
||||
@@ -119,6 +119,23 @@ pub(super) fn workspace_file_exists(root_relative_path: &str) -> bool {
|
||||
.exists()
|
||||
}
|
||||
|
||||
pub(super) fn workspace_files_with_extension(
|
||||
root_relative_path: &str,
|
||||
extension: &str,
|
||||
) -> Vec<PathBuf> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
.join(root_relative_path);
|
||||
let mut files = fs::read_dir(root)
|
||||
.expect("workspace directory should be readable")
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().and_then(|value| value.to_str()) == Some(extension))
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
pub(super) fn collect_workspace_rust_files(root_relative_path: &str) -> Vec<PathBuf> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../..")
|
||||
@@ -179,3 +196,4 @@ mod ai_serving;
|
||||
mod runtime_and_security;
|
||||
mod sql_and_data;
|
||||
mod usage;
|
||||
mod workspace_tiers;
|
||||
|
||||
@@ -161,11 +161,11 @@ fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
"crates/aether-admin/src",
|
||||
"crates/aether-billing/src",
|
||||
"crates/aether-model-fetch/src",
|
||||
"crates/aether-provider-pool/src",
|
||||
"crates/aether-runtime/src",
|
||||
"crates/aether-task-runtime/src",
|
||||
"crates/aether-usage-runtime/src",
|
||||
"crates/aether-provider-transport/src",
|
||||
"crates/aether-provider/pool/src",
|
||||
"crates/aether-runtime/base/src",
|
||||
"crates/aether-task/runtime/src",
|
||||
"crates/aether-usage/runtime/src",
|
||||
"crates/aether-provider/transport/src",
|
||||
"crates/aether-wallet/src",
|
||||
] {
|
||||
for path in collect_workspace_rust_files(root) {
|
||||
@@ -199,11 +199,11 @@ fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
"crates/aether-admin/Cargo.toml",
|
||||
"crates/aether-billing/Cargo.toml",
|
||||
"crates/aether-model-fetch/Cargo.toml",
|
||||
"crates/aether-provider-pool/Cargo.toml",
|
||||
"crates/aether-provider-transport/Cargo.toml",
|
||||
"crates/aether-runtime/Cargo.toml",
|
||||
"crates/aether-task-runtime/Cargo.toml",
|
||||
"crates/aether-usage-runtime/Cargo.toml",
|
||||
"crates/aether-provider/pool/Cargo.toml",
|
||||
"crates/aether-provider/transport/Cargo.toml",
|
||||
"crates/aether-runtime/base/Cargo.toml",
|
||||
"crates/aether-task/runtime/Cargo.toml",
|
||||
"crates/aether-usage/runtime/Cargo.toml",
|
||||
"crates/aether-wallet/Cargo.toml",
|
||||
] {
|
||||
let cargo = read_workspace_file(manifest);
|
||||
@@ -220,7 +220,7 @@ fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
);
|
||||
|
||||
let mut runtime_state_violations = Vec::new();
|
||||
for path in collect_workspace_rust_files("crates/aether-runtime-state/src") {
|
||||
for path in collect_workspace_rust_files("crates/aether-runtime/state/src") {
|
||||
if path
|
||||
.components()
|
||||
.any(|component| component.as_os_str() == "redis")
|
||||
@@ -244,13 +244,13 @@ fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
}
|
||||
assert!(
|
||||
runtime_state_violations.is_empty(),
|
||||
"only crates/aether-runtime-state/src/redis may depend on the redis crate directly:\n{}",
|
||||
"only crates/aether-runtime/state/src/redis may depend on the redis crate directly:\n{}",
|
||||
runtime_state_violations.join("\n")
|
||||
);
|
||||
|
||||
let mut runtime_connection_violations = Vec::new();
|
||||
for path in collect_workspace_rust_files("crates/aether-runtime-state/src") {
|
||||
if path.ends_with("crates/aether-runtime-state/src/redis/client.rs") {
|
||||
for path in collect_workspace_rust_files("crates/aether-runtime/state/src") {
|
||||
if path.ends_with("crates/aether-runtime/state/src/redis/client.rs") {
|
||||
continue;
|
||||
}
|
||||
let source = production_workspace_source(&path);
|
||||
@@ -267,17 +267,17 @@ fn runtime_state_owns_redis_runtime_boundaries() {
|
||||
|
||||
#[test]
|
||||
fn aether_data_stays_free_of_redis_runtime_backends() {
|
||||
let cargo = read_workspace_file("crates/aether-data/Cargo.toml");
|
||||
let cargo = read_workspace_file("crates/aether-data/runtime/Cargo.toml");
|
||||
assert!(
|
||||
!cargo.contains("redis.workspace"),
|
||||
"aether-data should not depend on redis; runtime Redis belongs to aether-runtime-state"
|
||||
);
|
||||
|
||||
for removed_path in [
|
||||
"crates/aether-data/src/backend/redis.rs",
|
||||
"crates/aether-data/src/backend/locks.rs",
|
||||
"crates/aether-data/src/backend/workers.rs",
|
||||
"crates/aether-data/src/driver/redis/mod.rs",
|
||||
"crates/aether-data/runtime/src/backend/redis.rs",
|
||||
"crates/aether-data/runtime/src/backend/locks.rs",
|
||||
"crates/aether-data/runtime/src/backend/workers.rs",
|
||||
"crates/aether-data/runtime/src/driver/redis/mod.rs",
|
||||
] {
|
||||
assert!(
|
||||
!workspace_file_exists(removed_path),
|
||||
@@ -285,7 +285,7 @@ fn aether_data_stays_free_of_redis_runtime_backends() {
|
||||
);
|
||||
}
|
||||
|
||||
for path in collect_workspace_rust_files("crates/aether-data/src") {
|
||||
for path in collect_workspace_rust_files("crates/aether-data/runtime/src") {
|
||||
let source = production_workspace_source(&path);
|
||||
for forbidden in [
|
||||
"pub mod redis",
|
||||
@@ -328,7 +328,7 @@ fn gateway_request_candidate_trace_type_is_owned_by_aether_data_contracts() {
|
||||
}
|
||||
|
||||
let candidate_types =
|
||||
read_workspace_file("crates/aether-data-contracts/src/repository/candidates/types.rs");
|
||||
read_workspace_file("crates/aether-data/contracts/src/repository/candidates/types.rs");
|
||||
for pattern in [
|
||||
"pub enum RequestCandidateFinalStatus",
|
||||
"pub struct RequestCandidateTrace",
|
||||
@@ -366,7 +366,7 @@ fn gateway_decision_trace_type_is_owned_by_aether_data_contracts() {
|
||||
}
|
||||
|
||||
let candidate_types =
|
||||
read_workspace_file("crates/aether-data-contracts/src/repository/candidates/types.rs");
|
||||
read_workspace_file("crates/aether-data/contracts/src/repository/candidates/types.rs");
|
||||
for pattern in [
|
||||
"pub struct DecisionTraceCandidate",
|
||||
"pub struct DecisionTrace",
|
||||
@@ -911,7 +911,8 @@ fn gateway_request_audit_bundle_type_is_owned_by_aether_data() {
|
||||
);
|
||||
}
|
||||
|
||||
let audit_types = read_workspace_file("crates/aether-data/src/repository/audit.rs");
|
||||
let audit_types =
|
||||
read_workspace_file("crates/aether-data/runtime/src/repository/audit/types.rs");
|
||||
for pattern in [
|
||||
"pub struct RequestAuditBundle",
|
||||
"pub trait RequestAuditReader",
|
||||
@@ -1434,7 +1435,7 @@ fn provider_transport_cache_helpers_live_in_shared_crate() {
|
||||
"state/mod.rs should import refresh detection from shared provider transport"
|
||||
);
|
||||
|
||||
let transport_cache = read_workspace_file("crates/aether-provider-transport/src/cache.rs");
|
||||
let transport_cache = read_workspace_file("crates/aether-provider/transport/src/cache.rs");
|
||||
for pattern in [
|
||||
"pub struct ProviderTransportSnapshotCacheKey",
|
||||
"pub fn provider_transport_snapshot_looks_refreshed(",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
use super::{collect_workspace_rust_files, read_workspace_file, workspace_file_exists};
|
||||
|
||||
fn assert_manifest_excludes(manifest_path: &str, forbidden: &[&str]) {
|
||||
let manifest = read_workspace_file(manifest_path);
|
||||
let violations = forbidden
|
||||
.iter()
|
||||
.filter(|dependency| manifest.contains(**dependency))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"{manifest_path} crosses its dependency tier through: {}",
|
||||
violations.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_policy_crates_do_not_depend_on_runtime_adapters() {
|
||||
let pure_manifests = [
|
||||
"crates/aether-admission-core/Cargo.toml",
|
||||
"crates/aether-provider/core/Cargo.toml",
|
||||
"crates/aether-task/core/Cargo.toml",
|
||||
"crates/aether-usage/core/Cargo.toml",
|
||||
];
|
||||
let forbidden = [
|
||||
"axum",
|
||||
"sqlx",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"wreq",
|
||||
"tokio",
|
||||
"aether-data =",
|
||||
"aether-gateway",
|
||||
];
|
||||
|
||||
for manifest in pure_manifests {
|
||||
assert_manifest_excludes(manifest, &forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_adapters_are_independent_driver_boundaries() {
|
||||
let adapters = [
|
||||
(
|
||||
"crates/aether-data/adapters/postgres/Cargo.toml",
|
||||
"features = [\"postgres\"",
|
||||
["features = [\"mysql\"", "features = [\"sqlite\""],
|
||||
),
|
||||
(
|
||||
"crates/aether-data/adapters/mysql/Cargo.toml",
|
||||
"features = [\"mysql\"",
|
||||
["features = [\"postgres\"", "features = [\"sqlite\""],
|
||||
),
|
||||
(
|
||||
"crates/aether-data/adapters/sqlite/Cargo.toml",
|
||||
"features = [\"sqlite\"",
|
||||
["features = [\"postgres\"", "features = [\"mysql\""],
|
||||
),
|
||||
];
|
||||
|
||||
for (manifest_path, expected_driver, other_drivers) in adapters {
|
||||
let manifest = read_workspace_file(manifest_path);
|
||||
assert!(manifest.contains("aether-data-contracts.workspace = true"));
|
||||
assert!(manifest.contains(expected_driver));
|
||||
assert_manifest_excludes(
|
||||
manifest_path,
|
||||
&[other_drivers[0], other_drivers[1], "aether-gateway", "axum"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_facade_preserves_legacy_driver_paths_without_owning_driver_code() {
|
||||
for (path, adapter) in [
|
||||
(
|
||||
"crates/aether-data/runtime/src/driver/postgres.rs",
|
||||
"aether_data_postgres",
|
||||
),
|
||||
(
|
||||
"crates/aether-data/runtime/src/driver/mysql.rs",
|
||||
"aether_data_mysql",
|
||||
),
|
||||
(
|
||||
"crates/aether-data/runtime/src/driver/sqlite.rs",
|
||||
"aether_data_sqlite",
|
||||
),
|
||||
] {
|
||||
let source = read_workspace_file(path);
|
||||
assert!(
|
||||
source.contains(&format!("pub use {adapter}::*;")),
|
||||
"{path} should remain a thin compatibility facade"
|
||||
);
|
||||
assert!(!source.contains("sqlx::"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_runtime_components_keep_focused_dependency_surfaces() {
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-gateway/frontdoor/Cargo.toml",
|
||||
&[
|
||||
"aether-data",
|
||||
"aether-provider-transport",
|
||||
"aether-gateway-workers",
|
||||
"sqlx",
|
||||
"redis",
|
||||
],
|
||||
);
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-gateway/workers/Cargo.toml",
|
||||
&[
|
||||
"axum",
|
||||
"aether-gateway-frontdoor",
|
||||
"aether-provider-transport",
|
||||
],
|
||||
);
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-gateway/execution/Cargo.toml",
|
||||
&[
|
||||
"axum",
|
||||
"sqlx",
|
||||
"redis",
|
||||
"aether-data",
|
||||
"aether-gateway-frontdoor",
|
||||
"aether-gateway-workers",
|
||||
],
|
||||
);
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-gateway/control/Cargo.toml",
|
||||
&[
|
||||
"sqlx",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"aether-data",
|
||||
"aether-provider-transport",
|
||||
"aether-gateway-workers",
|
||||
],
|
||||
);
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-gateway/tunnel/Cargo.toml",
|
||||
&[
|
||||
"axum",
|
||||
"sqlx",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"wreq",
|
||||
"aether-data",
|
||||
"aether-provider-transport",
|
||||
"aether-gateway-workers",
|
||||
],
|
||||
);
|
||||
assert_manifest_excludes(
|
||||
"crates/aether-testing/loadtools/Cargo.toml",
|
||||
&[
|
||||
"aether-gateway",
|
||||
"aether-testkit",
|
||||
"aether-data",
|
||||
"axum",
|
||||
"redis",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_binary_uses_shared_tunnel_boundary_without_gateway_runtime_dependency() {
|
||||
let manifest = read_workspace_file("apps/aether-tunnel/Cargo.toml");
|
||||
let dependencies = manifest
|
||||
.split_once("[dependencies]")
|
||||
.expect("tunnel manifest should declare dependencies")
|
||||
.1
|
||||
.split("[dev-dependencies]")
|
||||
.next()
|
||||
.expect("normal dependency section should exist");
|
||||
|
||||
assert!(dependencies.contains("aether-gateway-tunnel.workspace = true"));
|
||||
assert!(!dependencies.contains("aether-gateway.workspace = true"));
|
||||
|
||||
let protocol_facade = read_workspace_file("apps/aether-tunnel/src/tunnel/protocol.rs");
|
||||
assert!(protocol_facade.contains("aether_gateway_tunnel::protocol::*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_facade_defaults_to_postgres_and_gateway_selects_all_drivers_explicitly() {
|
||||
let data_manifest = read_workspace_file("crates/aether-data/runtime/Cargo.toml");
|
||||
assert!(data_manifest.contains("default = [\"postgres\"]"));
|
||||
for dependency in [
|
||||
"aether-data-postgres = { workspace = true, optional = true }",
|
||||
"aether-data-mysql = { workspace = true, optional = true }",
|
||||
"aether-data-sqlite = { workspace = true, optional = true }",
|
||||
] {
|
||||
assert!(
|
||||
data_manifest.contains(dependency),
|
||||
"aether-data should keep {dependency} optional"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!data_manifest.contains("features = [\"postgres\", \"mysql\", \"sqlite\"\"]"),
|
||||
"aether-data must not unconditionally enable every sqlx driver"
|
||||
);
|
||||
|
||||
let gateway_manifest = read_workspace_file("apps/aether-gateway/Cargo.toml");
|
||||
assert!(gateway_manifest
|
||||
.contains("aether-data = { workspace = true, features = [\"all-drivers\"] }"));
|
||||
|
||||
let data_lib = read_workspace_file("crates/aether-data/runtime/src/lib.rs");
|
||||
for backend in ["PostgresBackend", "MysqlBackend", "SqliteBackend"] {
|
||||
assert!(
|
||||
data_lib.contains(&format!("pub use backend::{backend};")),
|
||||
"aether-data should expose enabled backends symmetrically at its facade root"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_query_helpers_belong_to_adapters_not_the_runtime_facade() {
|
||||
let data_manifest = read_workspace_file("crates/aether-data/runtime/Cargo.toml");
|
||||
assert!(
|
||||
!data_manifest.contains("aether-data-query.workspace = true"),
|
||||
"aether-data should not keep a direct query-helper dependency after SQL repositories move to adapters"
|
||||
);
|
||||
|
||||
for adapter_manifest in [
|
||||
"crates/aether-data/adapters/postgres/Cargo.toml",
|
||||
"crates/aether-data/adapters/mysql/Cargo.toml",
|
||||
"crates/aether-data/adapters/sqlite/Cargo.toml",
|
||||
] {
|
||||
let manifest = read_workspace_file(adapter_manifest);
|
||||
assert!(
|
||||
manifest.contains("aether-data-query.workspace = true"),
|
||||
"{adapter_manifest} should own its query-helper dependency"
|
||||
);
|
||||
}
|
||||
|
||||
let query_helpers = read_workspace_file("crates/aether-data/query/src/lib.rs");
|
||||
for dialect in ["Postgres", "MySql", "Sqlite"] {
|
||||
assert!(
|
||||
query_helpers.contains(dialect),
|
||||
"aether-data-query should render the {dialect} dialect"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_adapters_centralize_error_mapping_boilerplate() {
|
||||
for (adapter, driver) in [
|
||||
("aether-data-mysql", "mysql"),
|
||||
("aether-data-sqlite", "sqlite"),
|
||||
] {
|
||||
let root = format!("crates/aether-data/adapters/{driver}/src");
|
||||
let files = collect_workspace_rust_files(&root);
|
||||
let trait_owners = files
|
||||
.iter()
|
||||
.filter(|path| {
|
||||
std::fs::read_to_string(path)
|
||||
.expect("adapter source should be readable")
|
||||
.contains("trait SqlResultExt<T>")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
trait_owners.len(),
|
||||
1,
|
||||
"{adapter} should have exactly one SqlResultExt owner, found: {trait_owners:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
trait_owners[0].file_name().and_then(|name| name.to_str()),
|
||||
Some("error.rs"),
|
||||
"{adapter} should keep SQL error mapping in src/error.rs"
|
||||
);
|
||||
|
||||
let lib = read_workspace_file(&format!("crates/aether-data/adapters/{driver}/src/lib.rs"));
|
||||
assert!(lib.contains("mod error;"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_tunnel_protocol_path_is_a_thin_compatibility_facade() {
|
||||
let source = read_workspace_file("apps/aether-gateway/src/tunnel/embedded/protocol.rs");
|
||||
assert_eq!(
|
||||
source.trim(),
|
||||
"pub use aether_gateway_tunnel::embedded::protocol::*;"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontdoor_owns_bounded_request_body_buffering() {
|
||||
let frontdoor = read_workspace_file("crates/aether-gateway/frontdoor/src/body.rs");
|
||||
assert!(frontdoor.contains("acquire_many_owned"));
|
||||
assert!(frontdoor.contains("to_bytes(body, body_limit)"));
|
||||
assert!(frontdoor.contains("BodyBufferReservation"));
|
||||
|
||||
let gateway = read_workspace_file("apps/aether-gateway/src/handlers/proxy/body_buffer.rs");
|
||||
assert!(gateway.contains("FrontdoorBodyBufferPolicy"));
|
||||
assert!(!gateway.contains("acquire_many_owned"));
|
||||
assert!(!gateway.contains("request_body_collection_exceeded_limit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn benchmark_binaries_are_outside_the_reusable_testkit() {
|
||||
let testkit_bin = "crates/aether-testing/testkit/src/bin";
|
||||
assert!(
|
||||
!workspace_file_exists(testkit_bin) || collect_workspace_rust_files(testkit_bin).is_empty(),
|
||||
"aether-testkit must not own benchmark binaries"
|
||||
);
|
||||
assert!(
|
||||
!collect_workspace_rust_files("crates/aether-testing/loadtools/src/bin").is_empty(),
|
||||
"standalone load tools should live in aether-loadtools"
|
||||
);
|
||||
assert!(
|
||||
!collect_workspace_rust_files("crates/aether-testing/integration/src/bin").is_empty(),
|
||||
"gateway-backed scenarios should live in aether-integration-tests"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn testkit_gateway_harness_is_opt_in() {
|
||||
let testkit_manifest = read_workspace_file("crates/aether-testing/testkit/Cargo.toml");
|
||||
assert!(
|
||||
testkit_manifest.contains("default = []"),
|
||||
"aether-testkit should keep the default feature set dependency-light"
|
||||
);
|
||||
assert!(
|
||||
testkit_manifest
|
||||
.contains("gateway = [\"dep:aether-gateway\", \"dep:aether-runtime-state\"]"),
|
||||
"gateway harnesses should be behind the explicit gateway feature"
|
||||
);
|
||||
assert!(
|
||||
testkit_manifest.contains("postgres = [\"dep:aether-data\", \"dep:sqlx\"]"),
|
||||
"Postgres schema helpers should be behind the explicit postgres feature"
|
||||
);
|
||||
assert!(testkit_manifest.contains(
|
||||
"aether-gateway = { workspace = true, features = [\"testkit\"], optional = true }"
|
||||
));
|
||||
|
||||
let testkit_lib = read_workspace_file("crates/aether-testing/testkit/src/lib.rs");
|
||||
for module in ["execution_runtime", "gateway", "tunnel"] {
|
||||
assert!(
|
||||
testkit_lib.contains(&format!("#[cfg(feature = \"gateway\")]\nmod {module};")),
|
||||
"aether-testkit::{module} should be feature-gated"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
testkit_lib.contains("#[cfg(feature = \"postgres\")]\nmod postgres;"),
|
||||
"the Postgres helper should be feature-gated"
|
||||
);
|
||||
|
||||
let integration_manifest = read_workspace_file("crates/aether-testing/integration/Cargo.toml");
|
||||
assert!(integration_manifest
|
||||
.contains("aether-testkit = { workspace = true, features = [\"gateway\", \"postgres\"] }"));
|
||||
}
|
||||
@@ -112,6 +112,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -66,6 +66,7 @@ fn sample_decision() -> crate::control::GatewayControlDecision {
|
||||
auth_context: None,
|
||||
admin_principal: None,
|
||||
local_auth_rejection: None,
|
||||
model_directive_policy: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,15 +310,15 @@ fn gateway_exposes_request_concurrency_metrics() {
|
||||
}
|
||||
|
||||
async fn gateway_exposes_request_concurrency_metrics_impl() {
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway state should build")
|
||||
.with_request_concurrency_limit(3)
|
||||
.with_distributed_request_concurrency_gate(memory_runtime_semaphore(
|
||||
"gateway_requests_distributed",
|
||||
5,
|
||||
)),
|
||||
);
|
||||
let state = AppState::new()
|
||||
.expect("gateway state should build")
|
||||
.with_request_concurrency_limit(3)
|
||||
.with_distributed_request_concurrency_gate(memory_runtime_semaphore(
|
||||
"gateway_requests_distributed",
|
||||
5,
|
||||
));
|
||||
assert!(state.prewarm_metric_snapshot().await);
|
||||
let gateway = build_router_with_state(state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
@@ -436,6 +437,7 @@ async fn gateway_exposes_fallback_metrics_impl() {
|
||||
Some(EXECUTION_PATH_LOCAL_EXECUTION_RUNTIME_MISS),
|
||||
GatewayFallbackReason::LocalExecutionPathRequired,
|
||||
);
|
||||
assert!(state.prewarm_metric_snapshot().await);
|
||||
let gateway = build_router_with_state(state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_a
|
||||
}),
|
||||
);
|
||||
|
||||
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeRequest>));
|
||||
let seen_execution_runtime = Arc::new(Mutex::new(Vec::<SeenExecutionRuntimeRequest>::new()));
|
||||
let seen_execution_runtime_clone = Arc::clone(&seen_execution_runtime);
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
@@ -83,21 +83,22 @@ async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_a
|
||||
.expect("body should read"),
|
||||
)
|
||||
.expect("plan should parse");
|
||||
*seen_execution_runtime_inner
|
||||
seen_execution_runtime_inner
|
||||
.lock()
|
||||
.expect("mutex should lock") = Some(SeenExecutionRuntimeRequest {
|
||||
url: plan.url.clone(),
|
||||
authorization: plan
|
||||
.headers
|
||||
.get("authorization")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
provider_api_format: plan.provider_api_format.clone(),
|
||||
total_ms: plan
|
||||
.timeouts
|
||||
.as_ref()
|
||||
.and_then(|timeouts| timeouts.total_ms),
|
||||
});
|
||||
.expect("mutex should lock")
|
||||
.push(SeenExecutionRuntimeRequest {
|
||||
url: plan.url.clone(),
|
||||
authorization: plan
|
||||
.headers
|
||||
.get("authorization")
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
provider_api_format: plan.provider_api_format.clone(),
|
||||
total_ms: plan
|
||||
.timeouts
|
||||
.as_ref()
|
||||
.and_then(|timeouts| timeouts.total_ms),
|
||||
});
|
||||
let result = aether_contracts::ExecutionResult {
|
||||
request_id: plan.request_id,
|
||||
candidate_id: None,
|
||||
@@ -223,24 +224,24 @@ async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_a
|
||||
);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
let seen_execution_runtime_request = seen_execution_runtime
|
||||
let seen_execution_runtime_requests = seen_execution_runtime
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.clone()
|
||||
.expect("execution runtime request should be captured");
|
||||
.clone();
|
||||
assert_eq!(seen_execution_runtime_requests.len(), 2);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.url,
|
||||
seen_execution_runtime_requests[0].url,
|
||||
"https://chatgpt.com/backend-api/wham/usage"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.authorization,
|
||||
"Bearer sk-codex-123"
|
||||
seen_execution_runtime_requests[1].url,
|
||||
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"
|
||||
);
|
||||
assert_eq!(
|
||||
seen_execution_runtime_request.provider_api_format,
|
||||
"openai:responses"
|
||||
);
|
||||
assert_eq!(seen_execution_runtime_request.total_ms, Some(30_000));
|
||||
for request in seen_execution_runtime_requests {
|
||||
assert_eq!(request.authorization, "Bearer sk-codex-123");
|
||||
assert_eq!(request.provider_api_format, "openai:responses");
|
||||
assert_eq!(request.total_ms, Some(30_000));
|
||||
}
|
||||
|
||||
let reloaded = provider_catalog_repository
|
||||
.list_keys_by_ids(&["key-codex-a".to_string()])
|
||||
@@ -679,7 +680,10 @@ async fn gateway_refreshes_admin_provider_quota_locally_for_requested_codex_keys
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.clone(),
|
||||
vec!["Bearer sk-codex-a".to_string()]
|
||||
vec![
|
||||
"Bearer sk-codex-a".to_string(),
|
||||
"Bearer sk-codex-a".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
let reloaded = provider_catalog_repository
|
||||
|
||||
@@ -559,6 +559,103 @@ async fn gateway_creates_admin_provider_endpoint_locally_with_trusted_admin_prin
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_streaming_policy_for_search_endpoint_before_catalog_write() {
|
||||
let mut create_provider = sample_provider("provider-search-create", "search-create", 10);
|
||||
create_provider.provider_type = "custom".to_string();
|
||||
let mut update_provider = sample_provider("provider-search-update", "search-update", 20);
|
||||
update_provider.provider_type = "custom".to_string();
|
||||
let mut existing_endpoint = sample_endpoint(
|
||||
"endpoint-search-update",
|
||||
"provider-search-update",
|
||||
"openai:search",
|
||||
"https://search.example/v1",
|
||||
);
|
||||
existing_endpoint.config = Some(json!({"marker": "kept"}));
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![create_provider, update_provider],
|
||||
vec![existing_endpoint],
|
||||
vec![],
|
||||
));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(
|
||||
GatewayDataState::with_provider_catalog_repository_for_tests(
|
||||
provider_catalog_repository.clone(),
|
||||
),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let create_response = client
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/endpoints/providers/provider-search-create/endpoints"
|
||||
))
|
||||
.header(GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"provider_id": "provider-search-create",
|
||||
"api_format": "openai:search",
|
||||
"base_url": "https://search.example/v1",
|
||||
"config": {"upstream_stream_policy": "force_stream"}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(create_response.status(), StatusCode::BAD_REQUEST);
|
||||
let create_payload: serde_json::Value = create_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(
|
||||
create_payload["detail"],
|
||||
"OpenAI Search 端点仅支持非流式上游请求"
|
||||
);
|
||||
|
||||
let update_response = client
|
||||
.put(format!(
|
||||
"{gateway_url}/api/admin/endpoints/endpoint-search-update"
|
||||
))
|
||||
.header(GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"config": {"upstreamStreamPolicy": true}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(update_response.status(), StatusCode::BAD_REQUEST);
|
||||
let update_payload: serde_json::Value = update_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body should parse");
|
||||
assert_eq!(
|
||||
update_payload["detail"],
|
||||
"OpenAI Search 端点仅支持非流式上游请求"
|
||||
);
|
||||
|
||||
let created = provider_catalog_repository
|
||||
.list_endpoints_by_provider_ids(&["provider-search-create".to_string()])
|
||||
.await
|
||||
.expect("endpoints should read");
|
||||
assert!(created.is_empty());
|
||||
let unchanged = provider_catalog_repository
|
||||
.list_endpoints_by_ids(&["endpoint-search-update".to_string()])
|
||||
.await
|
||||
.expect("endpoint should read");
|
||||
assert_eq!(unchanged.len(), 1);
|
||||
assert_eq!(unchanged[0].config, Some(json!({"marker": "kept"})));
|
||||
|
||||
gateway_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_updates_admin_provider_endpoint_locally_with_trusted_admin_principal() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
|
||||
@@ -6086,6 +6086,9 @@ async fn gateway_manual_codex_oauth_refresh_reconciles_missing_fixed_endpoint_im
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:responses")
|
||||
.expect("openai responses endpoint should be reconciled");
|
||||
assert!(endpoints
|
||||
.iter()
|
||||
.any(|endpoint| endpoint.api_format == "openai:search"));
|
||||
assert_eq!(
|
||||
responses_endpoint.base_url,
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
|
||||
@@ -10,7 +10,7 @@ use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadReposi
|
||||
use aether_data::repository::proxy_nodes::InMemoryProxyNodeRepository;
|
||||
use aether_data_contracts::repository::provider_catalog::ProviderCatalogReadRepository;
|
||||
use aether_runtime_state::{RedisClientConfig, RuntimeState};
|
||||
use aether_testkit::ManagedRedisServer;
|
||||
use aether_test_support::ManagedRedisServer;
|
||||
use axum::body::to_bytes;
|
||||
use axum::body::Body;
|
||||
use axum::routing::{any, get, post};
|
||||
|
||||
@@ -559,7 +559,7 @@ async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_
|
||||
.expect("mutex should lock") += 1;
|
||||
assert_eq!(
|
||||
plan.url,
|
||||
"https://chatgpt.com/backend-api/codex/models?client_version=0.128.0-alpha.1"
|
||||
"https://chatgpt.com/backend-api/codex/models?client_version=0.144.1"
|
||||
);
|
||||
Json(json!({
|
||||
"request_id": "req-provider-query-codex-invalidated",
|
||||
@@ -625,6 +625,11 @@ async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["success"], json!(true));
|
||||
assert_eq!(payload["data"]["error"], serde_json::Value::Null);
|
||||
let warning = payload["data"]["warning"]
|
||||
.as_str()
|
||||
.expect("Codex fallback warning should be present");
|
||||
assert!(warning.contains("Codex 动态模型目录不可用"));
|
||||
assert!(warning.contains("invalidated"));
|
||||
let model_ids = payload["data"]["models"]
|
||||
.as_array()
|
||||
.expect("models should be an array")
|
||||
@@ -634,11 +639,14 @@ async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_
|
||||
assert_eq!(
|
||||
model_ids,
|
||||
vec![
|
||||
"gpt-5.3-codex",
|
||||
"gpt-5.3-codex-spark",
|
||||
"codex-auto-review",
|
||||
"gpt-5.2",
|
||||
"gpt-5.4",
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.5",
|
||||
"gpt-5.6-luna",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2391,6 +2399,188 @@ async fn gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_te
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_executes_codex_search_admin_pool_model_test_with_search_contract() {
|
||||
run_provider_query_test(
|
||||
"gateway_executes_codex_search_admin_pool_model_test_with_search_contract",
|
||||
gateway_executes_codex_search_admin_pool_model_test_with_search_contract_impl,
|
||||
);
|
||||
}
|
||||
|
||||
async fn gateway_executes_codex_search_admin_pool_model_test_with_search_contract_impl() {
|
||||
let execution_runtime = Router::new().route(
|
||||
"/v1/execute/sync",
|
||||
any(move |Json(plan): Json<ExecutionPlan>| async move {
|
||||
assert_eq!(plan.provider_id, "provider-codex-search");
|
||||
assert_eq!(plan.endpoint_id, "endpoint-codex-search");
|
||||
assert_eq!(plan.key_id, "key-codex-search");
|
||||
assert_eq!(plan.client_api_format, "openai:search");
|
||||
assert_eq!(plan.provider_api_format, "openai:search");
|
||||
assert_eq!(
|
||||
plan.url,
|
||||
"https://chatgpt.com/backend-api/codex/alpha/search"
|
||||
);
|
||||
assert_eq!(plan.model_name.as_deref(), Some("gpt-5.6-sol"));
|
||||
assert!(!plan.stream, "Codex Search is a synchronous JSON protocol");
|
||||
assert_eq!(
|
||||
plan.timeouts
|
||||
.as_ref()
|
||||
.and_then(|timeouts| timeouts.total_ms),
|
||||
Some(900_000)
|
||||
);
|
||||
assert_eq!(
|
||||
plan.headers.get("authorization").map(String::as_str),
|
||||
Some("Bearer codex-search-access-token")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.headers.get("chatgpt-account-id").map(String::as_str),
|
||||
Some("account-search-admin")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.headers.get("x-openai-fedramp").map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
plan.headers.get("originator").map(String::as_str),
|
||||
Some("codex_cli_rs")
|
||||
);
|
||||
assert!(plan
|
||||
.headers
|
||||
.get("user-agent")
|
||||
.is_some_and(|value| value.starts_with("codex_cli_rs/")));
|
||||
assert!(!plan.headers.contains_key("openai-beta"));
|
||||
assert!(!plan
|
||||
.headers
|
||||
.contains_key("x-openai-internal-codex-responses-lite"));
|
||||
assert_ne!(
|
||||
plan.headers.get("accept").map(String::as_str),
|
||||
Some("text/event-stream")
|
||||
);
|
||||
|
||||
let body = plan.body.json_body.as_ref().expect("search json body");
|
||||
assert_eq!(
|
||||
body["id"],
|
||||
json!("aether-model-test-provider-query-search-trace")
|
||||
);
|
||||
assert_eq!(body["model"], json!("gpt-5.6-sol"));
|
||||
assert_eq!(body["input"], json!("find current OpenAI documentation"));
|
||||
assert_eq!(
|
||||
body["commands"]["search_query"][0]["q"],
|
||||
json!("OpenAI Codex Search")
|
||||
);
|
||||
assert!(body.get("stream").is_none());
|
||||
assert!(body.get("store").is_none());
|
||||
assert!(body.get("service_tier").is_none());
|
||||
assert!(body.get("unknown_field").is_none());
|
||||
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"candidate_id": plan.candidate_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"json_body": {
|
||||
"output": "search result"
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"elapsed_ms": 21
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let mut provider = sample_provider("provider-codex-search", "Codex Search", 10);
|
||||
provider.provider_type = "codex".to_string();
|
||||
provider.request_timeout_secs = Some(900.0);
|
||||
let mut endpoint = sample_endpoint(
|
||||
"endpoint-codex-search",
|
||||
"provider-codex-search",
|
||||
"openai:search",
|
||||
"https://chatgpt.com/backend-api/codex",
|
||||
);
|
||||
endpoint.config = Some(json!({"upstream_stream_policy": "force_stream"}));
|
||||
let mut key = sample_key(
|
||||
"key-codex-search",
|
||||
"provider-codex-search",
|
||||
"openai:search",
|
||||
"codex-search-access-token",
|
||||
);
|
||||
key.auth_type = "oauth".to_string();
|
||||
key.encrypted_auth_config = Some(
|
||||
aether_crypto::encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"provider_type":"codex","account_id":"account-search-admin","is_fedramp":true}"#,
|
||||
)
|
||||
.expect("auth config should encrypt"),
|
||||
);
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
vec![endpoint],
|
||||
vec![key],
|
||||
));
|
||||
|
||||
let gateway = build_router_with_state(
|
||||
build_state_with_execution_runtime_override(execution_runtime_url)
|
||||
.with_data_state_for_tests(GatewayDataState::with_provider_transport_reader_for_tests(
|
||||
provider_catalog_repository,
|
||||
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{gateway_url}/api/admin/provider-query/test-model-failover"
|
||||
))
|
||||
.header(GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"provider_id": "provider-codex-search",
|
||||
"mode": "pool",
|
||||
"model": "gpt-5.6-sol",
|
||||
"failover_models": ["gpt-5.6-sol"],
|
||||
"api_format": "openai:search",
|
||||
"endpoint_id": "endpoint-codex-search",
|
||||
"request_id": "provider-query-search-trace",
|
||||
"request_body": {
|
||||
"model": "gpt-5.6-sol",
|
||||
"input": "find current OpenAI documentation",
|
||||
"commands": {
|
||||
"search_query": [{"q": "OpenAI Codex Search"}]
|
||||
},
|
||||
"max_output_tokens": 256,
|
||||
"stream": true,
|
||||
"store": false,
|
||||
"service_tier": "priority",
|
||||
"unknown_field": true
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["success"], json!(true), "payload={payload}");
|
||||
assert_eq!(
|
||||
payload["attempts"][0]["request_body"]["id"],
|
||||
json!("aether-model-test-provider-query-search-trace")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["attempts"][0]["response_body"]["output"],
|
||||
json!("search result")
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime() {
|
||||
run_provider_query_test(
|
||||
@@ -3837,13 +4027,12 @@ async fn gateway_handles_openai_responses_test_model_locally_impl() {
|
||||
.and_then(|value| value.as_str()),
|
||||
Some(prompt)
|
||||
);
|
||||
assert_eq!(
|
||||
plan.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("instructions")),
|
||||
Some(&json!(""))
|
||||
);
|
||||
assert!(plan
|
||||
.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("instructions"))
|
||||
.is_none());
|
||||
assert_eq!(
|
||||
plan.body
|
||||
.json_body
|
||||
@@ -3856,7 +4045,7 @@ async fn gateway_handles_openai_responses_test_model_locally_impl() {
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("prompt_cache_key"))
|
||||
.is_some());
|
||||
.is_none());
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"candidate_id": plan.candidate_id,
|
||||
@@ -3960,8 +4149,8 @@ async fn gateway_handles_openai_image_test_model_locally_impl() {
|
||||
assert_eq!(plan.client_api_format, "openai:image");
|
||||
assert_eq!(plan.provider_api_format, "openai:image");
|
||||
assert_eq!(plan.model_name.as_deref(), Some("gpt-image-1"));
|
||||
assert_eq!(plan.url, "https://api.openai.example/v1/responses");
|
||||
assert!(plan.stream);
|
||||
assert_eq!(plan.url, "https://api.openai.example/v1/images/generations");
|
||||
assert!(!plan.stream);
|
||||
assert_eq!(
|
||||
plan.headers.get("authorization").map(String::as_str),
|
||||
Some("Bearer sk-test-image")
|
||||
@@ -3971,38 +4160,42 @@ async fn gateway_handles_openai_image_test_model_locally_impl() {
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("model")),
|
||||
Some(&json!(crate::ai_serving::CODEX_OPENAI_IMAGE_INTERNAL_MODEL))
|
||||
Some(&json!("gpt-image-1"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.and_then(|body| body.get("input"))
|
||||
.and_then(|input| input.as_array())
|
||||
.and_then(|items| items.first())
|
||||
.and_then(|item| item.get("content"))
|
||||
.and_then(|body| body.get("prompt"))
|
||||
.and_then(|value| value.as_str()),
|
||||
Some("Draw a small blue square")
|
||||
);
|
||||
assert!(plan
|
||||
.body
|
||||
.json_body
|
||||
.as_ref()
|
||||
.is_some_and(|body| body.get("stream").is_none()));
|
||||
Json(json!({
|
||||
"request_id": plan.request_id,
|
||||
"candidate_id": plan.candidate_id,
|
||||
"status_code": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": {
|
||||
"body_bytes_b64": base64::engine::general_purpose::STANDARD.encode(
|
||||
concat!(
|
||||
"event: response.created\n",
|
||||
"data: {\"type\":\"response.created\",\"response\":{\"created_at\":1776839946}}\n\n",
|
||||
"event: response.output_item.done\n",
|
||||
"data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"image_generation_call\",\"output_format\":\"png\",\"revised_prompt\":\"revised prompt\",\"result\":\"aGVsbG8=\"}}\n\n",
|
||||
"event: response.completed\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_img_123\",\"model\":\"gpt-image-1\",\"status\":\"completed\",\"tool_usage\":{\"image_gen\":{\"input_tokens\":171,\"output_tokens\":1372,\"total_tokens\":1543}}}}\n\n"
|
||||
)
|
||||
.as_bytes()
|
||||
)
|
||||
"json_body": {
|
||||
"created": 1776839946,
|
||||
"model": "gpt-image-1",
|
||||
"data": [{
|
||||
"b64_json": "aGVsbG8=",
|
||||
"revised_prompt": "revised prompt"
|
||||
}],
|
||||
"usage": {
|
||||
"input_tokens": 171,
|
||||
"output_tokens": 1372,
|
||||
"total_tokens": 1543
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"elapsed_ms": 19
|
||||
|
||||
@@ -832,7 +832,7 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
"is_active": false,
|
||||
"concurrent_limit": 8,
|
||||
"max_retries": 6,
|
||||
"request_timeout": 55.0,
|
||||
"request_timeout": aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS,
|
||||
"stream_first_byte_timeout": 11.0,
|
||||
"enable_format_conversion": false,
|
||||
"config": {
|
||||
@@ -860,7 +860,10 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
assert_eq!(payload["enable_format_conversion"], false);
|
||||
assert_eq!(payload["is_active"], false);
|
||||
assert_eq!(payload["max_retries"], 6);
|
||||
assert_eq!(payload["request_timeout"], 55.0);
|
||||
assert_eq!(
|
||||
payload["request_timeout"].as_f64(),
|
||||
Some(aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS as f64)
|
||||
);
|
||||
assert_eq!(payload["stream_first_byte_timeout"], 11.0);
|
||||
assert_eq!(payload["proxy"], json!({"url": "https://proxy.example"}));
|
||||
assert_eq!(payload["claude_code_advanced"], json!({"pool_size": 2}));
|
||||
@@ -870,6 +873,21 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
assert_eq!(payload["ops_configured"], true);
|
||||
assert_eq!(payload["ops_architecture_id"], "cubence");
|
||||
|
||||
let invalid_timeout_response = reqwest::Client::new()
|
||||
.patch(format!("{gateway_url}/api/admin/providers/provider-openai"))
|
||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"request_timeout":
|
||||
aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS + 1
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(invalid_timeout_response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let disable_response = reqwest::Client::new()
|
||||
.patch(format!("{gateway_url}/api/admin/providers/provider-openai"))
|
||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||
@@ -924,6 +942,10 @@ async fn gateway_updates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
.iter()
|
||||
.find(|provider| provider.id == "provider-openai")
|
||||
.expect("provider should exist");
|
||||
assert_eq!(
|
||||
updated_provider.request_timeout_secs,
|
||||
Some(aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS as f64)
|
||||
);
|
||||
assert_eq!(
|
||||
updated_provider
|
||||
.config
|
||||
@@ -1000,6 +1022,7 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
"website": "codex.example",
|
||||
"keep_priority_on_conversion": true,
|
||||
"max_retries": 7,
|
||||
"request_timeout": aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS,
|
||||
"config": {"chat_pii_redaction": {"enabled": true}},
|
||||
"pool_advanced": {},
|
||||
"failover_rules": {"strategy": "ordered"},
|
||||
@@ -1034,6 +1057,10 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
assert_eq!(created.website.as_deref(), Some("https://codex.example"));
|
||||
assert!(created.enable_format_conversion);
|
||||
assert_eq!(created.max_retries, Some(7));
|
||||
assert_eq!(
|
||||
created.request_timeout_secs,
|
||||
Some(aether_contracts::MAX_EXECUTION_REQUEST_TIMEOUT_SECS as f64)
|
||||
);
|
||||
assert_eq!(created.keep_priority_on_conversion, true);
|
||||
assert_eq!(
|
||||
created
|
||||
@@ -1080,7 +1107,7 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
.list_endpoints_by_provider_ids(std::slice::from_ref(&created.id))
|
||||
.await
|
||||
.expect("endpoints should list");
|
||||
assert_eq!(endpoints.len(), 3);
|
||||
assert_eq!(endpoints.len(), 4);
|
||||
let responses_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:responses")
|
||||
@@ -1089,6 +1116,10 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:responses:compact")
|
||||
.expect("compact endpoint should exist");
|
||||
let search_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:search")
|
||||
.expect("search endpoint should exist");
|
||||
let image_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:image")
|
||||
@@ -1101,12 +1132,17 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
compact_endpoint.base_url,
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
);
|
||||
assert_eq!(
|
||||
search_endpoint.base_url,
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
);
|
||||
assert_eq!(
|
||||
image_endpoint.base_url,
|
||||
"https://chatgpt.com/backend-api/codex"
|
||||
);
|
||||
assert_eq!(responses_endpoint.max_retries, Some(7));
|
||||
assert_eq!(compact_endpoint.max_retries, Some(7));
|
||||
assert_eq!(search_endpoint.max_retries, Some(7));
|
||||
assert_eq!(image_endpoint.max_retries, Some(7));
|
||||
assert_eq!(
|
||||
responses_endpoint
|
||||
@@ -1116,16 +1152,25 @@ async fn gateway_creates_admin_provider_locally_with_trusted_admin_principal() {
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("force_stream")
|
||||
);
|
||||
assert_eq!(
|
||||
search_endpoint
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("upstream_stream_policy"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
image_endpoint
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("upstream_stream_policy"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("force_stream")
|
||||
None
|
||||
);
|
||||
assert!(responses_endpoint.body_rules.is_none());
|
||||
assert!(compact_endpoint.body_rules.is_none());
|
||||
assert!(search_endpoint.body_rules.is_none());
|
||||
assert!(image_endpoint.body_rules.is_none());
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
@@ -1216,7 +1261,7 @@ async fn gateway_updates_fixed_provider_and_reconciles_template_managed_endpoint
|
||||
.list_endpoints_by_provider_ids(&["provider-codex".to_string()])
|
||||
.await
|
||||
.expect("endpoints should list");
|
||||
assert_eq!(endpoints.len(), 3);
|
||||
assert_eq!(endpoints.len(), 4);
|
||||
let responses_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:responses")
|
||||
@@ -1225,6 +1270,10 @@ async fn gateway_updates_fixed_provider_and_reconciles_template_managed_endpoint
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:responses:compact")
|
||||
.expect("compact endpoint should exist");
|
||||
let search_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:search")
|
||||
.expect("search endpoint should exist");
|
||||
let image_endpoint = endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.api_format == "openai:image")
|
||||
@@ -1232,6 +1281,7 @@ async fn gateway_updates_fixed_provider_and_reconciles_template_managed_endpoint
|
||||
|
||||
assert_eq!(responses_endpoint.max_retries, Some(9));
|
||||
assert_eq!(compact_endpoint.max_retries, Some(9));
|
||||
assert_eq!(search_endpoint.max_retries, Some(9));
|
||||
assert_eq!(image_endpoint.max_retries, Some(9));
|
||||
assert_eq!(
|
||||
responses_endpoint
|
||||
@@ -1242,20 +1292,37 @@ async fn gateway_updates_fixed_provider_and_reconciles_template_managed_endpoint
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
search_endpoint
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("_aether_fixed_provider_template"))
|
||||
.and_then(|value| value.get("managed"))
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
search_endpoint
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("upstream_stream_policy"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
image_endpoint
|
||||
.config
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("upstream_stream_policy"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("force_stream")
|
||||
None
|
||||
);
|
||||
let keys = provider_catalog_repository
|
||||
.list_keys_by_provider_ids(&["provider-codex".to_string()])
|
||||
.await
|
||||
.expect("keys should list");
|
||||
assert_eq!(keys.len(), 1);
|
||||
assert!(keys[0].api_formats.is_none());
|
||||
assert_eq!(keys[0].api_formats, Some(json!(["openai:responses"])));
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
|
||||
@@ -1009,6 +1009,26 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
||||
payload["timeline"][1]["avg_first_byte_time_ms"],
|
||||
serde_json::Value::Null
|
||||
);
|
||||
|
||||
let without_timeline_response = admin_request(reqwest::Client::new().get(format!(
|
||||
"{gateway_url}/api/admin/stats/performance/providers?start_date=2024-03-21&end_date=2024-03-21&granularity=hour&limit=2&tz_offset_minutes=0&include_timeline=false"
|
||||
)))
|
||||
.send()
|
||||
.await
|
||||
.expect("request without timeline should succeed");
|
||||
assert_eq!(without_timeline_response.status(), StatusCode::OK);
|
||||
let without_timeline_payload: serde_json::Value = without_timeline_response
|
||||
.json()
|
||||
.await
|
||||
.expect("json body without timeline should parse");
|
||||
assert_eq!(without_timeline_payload["summary"], payload["summary"]);
|
||||
assert_eq!(without_timeline_payload["providers"], payload["providers"]);
|
||||
assert_eq!(
|
||||
without_timeline_payload["timeline"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
@@ -1379,7 +1399,7 @@ async fn gateway_handles_admin_stats_leaderboard_models_locally_with_trusted_adm
|
||||
assert_eq!(payload["metric"], "tokens");
|
||||
assert_eq!(payload["items"][0]["rank"], 1);
|
||||
assert_eq!(payload["items"][0]["id"], "gpt-5");
|
||||
assert_eq!(payload["items"][0]["value"], 160);
|
||||
assert_eq!(payload["items"][0]["value"], 150);
|
||||
assert_eq!(payload["items"][1]["id"], "claude-3-5-sonnet");
|
||||
assert_eq!(payload["items"][1]["value"], 100);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
@@ -641,7 +641,7 @@ async fn gateway_handles_admin_usage_aggregation_stats_locally_with_trusted_admi
|
||||
assert_eq!(items[0]["model"], "gpt-5");
|
||||
assert_eq!(items[0]["request_count"], 2);
|
||||
assert_eq!(items[0]["output_tokens"], 40);
|
||||
assert_eq!(items[0]["effective_input_tokens"], 150);
|
||||
assert_eq!(items[0]["effective_input_tokens"], 120);
|
||||
assert_eq!(items[0]["total_input_context"], 160);
|
||||
assert_eq!(items[0]["cache_creation_tokens"], 30);
|
||||
assert_eq!(items[0]["cache_creation_ephemeral_5m_tokens"], 12);
|
||||
@@ -1026,7 +1026,7 @@ async fn gateway_handles_admin_usage_active_locally_with_trusted_admin_principal
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["requests"].as_array().expect("array").len(), 1);
|
||||
assert_eq!(payload["requests"][0]["id"], "usage-pending");
|
||||
assert_eq!(payload["requests"][0]["effective_input_tokens"], 5);
|
||||
assert_eq!(payload["requests"][0]["effective_input_tokens"], 0);
|
||||
assert_eq!(payload["requests"][0]["provider"], "OpenAI");
|
||||
assert_eq!(payload["requests"][0]["api_key_name"], "fresh-primary");
|
||||
assert_eq!(payload["requests"][0]["has_fallback"], true);
|
||||
@@ -1326,7 +1326,7 @@ async fn gateway_handles_admin_usage_records_locally_with_trusted_admin_principa
|
||||
payload["records"][0]["provider_key_name"],
|
||||
"upstream-primary"
|
||||
);
|
||||
assert_eq!(payload["records"][0]["effective_input_tokens"], 35);
|
||||
assert_eq!(payload["records"][0]["effective_input_tokens"], 20);
|
||||
assert_eq!(payload["records"][0]["first_byte_time_ms"], 120);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
@@ -2053,8 +2053,8 @@ async fn gateway_handles_admin_usage_detail_locally_with_trusted_admin_principal
|
||||
assert_eq!(payload["api_key"]["name"], "primary");
|
||||
assert_eq!(payload["provider"], "OpenAI");
|
||||
assert_eq!(payload["model"], "gpt-5");
|
||||
assert_eq!(payload["effective_input_tokens"], 115);
|
||||
assert_eq!(payload["total_tokens"], 165);
|
||||
assert_eq!(payload["effective_input_tokens"], 100);
|
||||
assert_eq!(payload["total_tokens"], 150);
|
||||
assert_eq!(payload["cache_creation_cost"], 0.0);
|
||||
assert_eq!(payload["cache_read_cost"], 0.0);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::io;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aether_contracts::tunnel::RequestMeta;
|
||||
use aether_data::repository::proxy_nodes::ProxyNodeReadRepository;
|
||||
use axum::body::Body;
|
||||
use axum::routing::{any, post};
|
||||
@@ -10,12 +11,45 @@ use futures_util::stream;
|
||||
use http::header::HeaderValue;
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{
|
||||
build_router_with_state, sample_proxy_node, start_server, AppState, GatewayDataState,
|
||||
InMemoryProxyNodeRepository, TRACE_ID_HEADER,
|
||||
};
|
||||
|
||||
fn relay_request_meta(
|
||||
stream: bool,
|
||||
request_timeout_ms: Option<u64>,
|
||||
stream_first_byte_timeout_ms: Option<u64>,
|
||||
) -> RequestMeta {
|
||||
RequestMeta {
|
||||
provider_id: Some("provider-1".to_string()),
|
||||
endpoint_id: Some("endpoint-1".to_string()),
|
||||
key_id: Some("key-1".to_string()),
|
||||
method: "POST".to_string(),
|
||||
url: "https://example.com/responses".to_string(),
|
||||
headers: HashMap::new(),
|
||||
stream,
|
||||
request_timeout_ms,
|
||||
stream_first_byte_timeout_ms,
|
||||
timeout: 60,
|
||||
follow_redirects: None,
|
||||
http1_only: false,
|
||||
transport_profile: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn relay_envelope(meta: &RequestMeta, body: &[u8]) -> Vec<u8> {
|
||||
let encoded_meta = serde_json::to_vec(meta).expect("metadata should encode");
|
||||
let mut envelope = Vec::with_capacity(4 + encoded_meta.len() + body.len());
|
||||
envelope.extend_from_slice(&(encoded_meta.len() as u32).to_be_bytes());
|
||||
envelope.extend_from_slice(&encoded_meta);
|
||||
envelope.extend_from_slice(body);
|
||||
envelope
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_internal_tunnel_heartbeat_locally_with_loopback() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -292,10 +326,13 @@ async fn gateway_forwards_tunnel_relay_to_attachment_owner() {
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let envelope = relay_envelope(&relay_request_meta(false, None, None), b"relay-envelope");
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/api/internal/tunnel/relay/node-123"))
|
||||
.header(TRACE_ID_HEADER, "trace-owner-forward")
|
||||
.body("relay-envelope")
|
||||
.header(http::header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(envelope.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
@@ -309,8 +346,8 @@ async fn gateway_forwards_tunnel_relay_to_attachment_owner() {
|
||||
Some("trace-owner-forward")
|
||||
);
|
||||
assert_eq!(
|
||||
response.text().await.expect("body should read"),
|
||||
"relay-envelope"
|
||||
response.bytes().await.expect("body should read"),
|
||||
Bytes::from(envelope)
|
||||
);
|
||||
assert_eq!(*owner_hits.lock().expect("mutex should lock"), 1);
|
||||
|
||||
@@ -318,6 +355,71 @@ async fn gateway_forwards_tunnel_relay_to_attachment_owner() {
|
||||
owner_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_owner_relay_uses_non_stream_timeout_from_envelope() {
|
||||
let owner = Router::new().route(
|
||||
"/api/internal/tunnel/relay/node-123",
|
||||
post(|body: Body| async move {
|
||||
let body = axum::body::to_bytes(body, usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
(StatusCode::OK, Body::from(body))
|
||||
}),
|
||||
);
|
||||
|
||||
let (owner_url, owner_handle) = start_server(owner).await;
|
||||
let data_state = GatewayDataState::disabled().with_system_config_values_for_tests(vec![(
|
||||
"tunnel.attachments.node-123".to_string(),
|
||||
json!({
|
||||
"gateway_instance_id": "gateway-b",
|
||||
"relay_base_url": owner_url,
|
||||
"conn_count": 1,
|
||||
"observed_at_unix_secs": 4_102_444_800u64,
|
||||
}),
|
||||
)]);
|
||||
let mut state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_tunnel_identity_for_tests("gateway-a", Some("http://gateway-a.internal"));
|
||||
let short_timeout_client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(10))
|
||||
.build()
|
||||
.expect("test client should build");
|
||||
state.client = short_timeout_client.clone();
|
||||
state.owner_forward_client = short_timeout_client;
|
||||
let gateway = build_router_with_state(state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let meta = relay_request_meta(false, Some(100), None);
|
||||
let envelope = relay_envelope(&meta, b"relay-body");
|
||||
let encoded_meta = serde_json::to_vec(&meta).expect("metadata should encode");
|
||||
let split_at = 4 + encoded_meta.len() / 2;
|
||||
let request_body = reqwest::Body::wrap_stream(stream::iter(vec![
|
||||
Ok::<Bytes, io::Error>(Bytes::copy_from_slice(&envelope[..split_at])),
|
||||
Ok::<Bytes, io::Error>(Bytes::copy_from_slice(&envelope[split_at..])),
|
||||
]));
|
||||
|
||||
let response = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(1))
|
||||
.build()
|
||||
.expect("request client should build")
|
||||
.post(format!("{gateway_url}/api/internal/tunnel/relay/node-123"))
|
||||
.body(request_body)
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.bytes().await.expect("response body should read"),
|
||||
Bytes::from(envelope)
|
||||
);
|
||||
|
||||
gateway_handle.abort();
|
||||
owner_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_streams_tunnel_relay_body_to_attachment_owner() {
|
||||
let owner_hits = Arc::new(Mutex::new(0usize));
|
||||
@@ -331,8 +433,12 @@ async fn gateway_streams_tunnel_relay_body_to_attachment_owner() {
|
||||
let body = axum::body::to_bytes(body, usize::MAX)
|
||||
.await
|
||||
.expect("body should read");
|
||||
assert_eq!(body, Bytes::from_static(b"relay-stream-envelope"));
|
||||
(StatusCode::OK, Body::from("stream-ok"))
|
||||
let response_body = Body::from_stream(async_stream::stream! {
|
||||
yield Ok::<_, io::Error>(Bytes::from_static(b"stream-"));
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
yield Ok::<_, io::Error>(Bytes::from_static(b"ok"));
|
||||
});
|
||||
(StatusCode::OK, response_body)
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -347,19 +453,23 @@ async fn gateway_streams_tunnel_relay_body_to_attachment_owner() {
|
||||
"observed_at_unix_secs": 4_102_444_800u64,
|
||||
}),
|
||||
)]);
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_tunnel_identity_for_tests("gateway-a", Some("http://gateway-a.internal")),
|
||||
);
|
||||
let mut state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_tunnel_identity_for_tests("gateway-a", Some("http://gateway-a.internal"));
|
||||
state.client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(10))
|
||||
.build()
|
||||
.expect("short shared client should build");
|
||||
let gateway = build_router_with_state(state);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let request_body = reqwest::Body::wrap_stream(stream::iter(vec![
|
||||
Ok::<Bytes, io::Error>(Bytes::from_static(b"relay-")),
|
||||
Ok::<Bytes, io::Error>(Bytes::from_static(b"stream-")),
|
||||
Ok::<Bytes, io::Error>(Bytes::from_static(b"envelope")),
|
||||
]));
|
||||
let meta = relay_request_meta(true, Some(900_000), Some(100));
|
||||
let envelope = relay_envelope(&meta, b"relay-stream-envelope");
|
||||
let expected_envelope = Bytes::copy_from_slice(&envelope);
|
||||
let request_body = reqwest::Body::wrap_stream(stream::iter(vec![Ok::<Bytes, io::Error>(
|
||||
expected_envelope.clone(),
|
||||
)]));
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/api/internal/tunnel/relay/node-123"))
|
||||
.body(request_body)
|
||||
@@ -369,8 +479,8 @@ async fn gateway_streams_tunnel_relay_body_to_attachment_owner() {
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.text().await.expect("body should read"),
|
||||
"stream-ok"
|
||||
response.bytes().await.expect("body should read"),
|
||||
Bytes::from_static(b"stream-ok")
|
||||
);
|
||||
assert_eq!(*owner_hits.lock().expect("mutex should lock"), 1);
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ fn sample_files_candidate_row() -> StoredMinimalCandidateSelectionRow {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:files".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -35,6 +35,27 @@ use aether_data_contracts::repository::video_tasks::{
|
||||
use base64::Engine as _;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn run_frontdoor_async_test<F>(name: &'static str, future: F)
|
||||
where
|
||||
F: std::future::Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
let handle = std::thread::Builder::new()
|
||||
.name(name.to_string())
|
||||
.stack_size(16 * 1024 * 1024)
|
||||
.spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("frontdoor test runtime should build")
|
||||
.block_on(future);
|
||||
})
|
||||
.expect("large-stack frontdoor test thread should spawn");
|
||||
|
||||
if let Err(payload) = handle.join() {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_api_key(value: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(value.as_bytes());
|
||||
@@ -270,6 +291,7 @@ fn sample_models_candidate_row(
|
||||
priority: 1,
|
||||
api_formats: Some(vec![api_format.to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -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,
|
||||
operations: 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,
|
||||
"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(
|
||||
|
||||
@@ -70,6 +70,7 @@ async fn gateway_exposes_frontdoor_manifest_without_proxying_upstream() {
|
||||
assert!(owned_routes
|
||||
.iter()
|
||||
.any(|value| value == "/v1/responses/compact"));
|
||||
assert!(owned_routes.iter().any(|value| value == "/v1/alpha/search"));
|
||||
assert!(owned_routes.iter().any(|value| value == "/health"));
|
||||
assert!(owned_routes.iter().any(|value| value == "/v1/health"));
|
||||
assert!(owned_routes.iter().any(|value| value == "/v1/providers"));
|
||||
|
||||
@@ -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");
|
||||
@@ -8243,7 +8240,9 @@ async fn gateway_returns_service_unavailable_for_users_me_management_token_write
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_users_me_providers_locally_without_proxying_upstream() {
|
||||
let now = Utc::now();
|
||||
let user = sample_auth_user(now);
|
||||
let mut user = sample_auth_user(now);
|
||||
user.allowed_providers = Some(vec!["claude".to_string()]);
|
||||
user.allowed_providers_mode = "specific".to_string();
|
||||
let access_token = build_test_auth_token(
|
||||
"access",
|
||||
serde_json::Map::from_iter([
|
||||
@@ -8303,6 +8302,27 @@ async fn gateway_handles_users_me_providers_locally_without_proxying_upstream()
|
||||
]),
|
||||
);
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![user]));
|
||||
let group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
name: "OpenAI only".to_string(),
|
||||
description: None,
|
||||
priority: 0,
|
||||
allowed_providers: Some(vec!["openai".to_string()]),
|
||||
allowed_providers_mode: "specific".to_string(),
|
||||
allowed_api_formats: None,
|
||||
allowed_api_formats_mode: "unrestricted".to_string(),
|
||||
allowed_models: None,
|
||||
allowed_models_mode: "unrestricted".to_string(),
|
||||
rate_limit: None,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("group should create")
|
||||
.expect("group should exist");
|
||||
user_repository
|
||||
.add_user_to_group(&group.id, "user-auth-1")
|
||||
.await
|
||||
.expect("group membership should create");
|
||||
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
@@ -9700,13 +9720,13 @@ async fn gateway_handles_users_me_available_models_locally_without_proxying_upst
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_filters_users_me_available_models_by_group_policy_and_hides_model_mappings() {
|
||||
async fn gateway_refreshes_users_me_available_models_after_group_assignment() {
|
||||
let now = Utc::now();
|
||||
let mut user = sample_auth_user(now);
|
||||
user.allowed_providers = None;
|
||||
user.allowed_providers_mode = "unrestricted".to_string();
|
||||
user.allowed_models = None;
|
||||
user.allowed_models_mode = "unrestricted".to_string();
|
||||
// Keep the legacy gpt-5 personal policy from sample_auth_user. Group policies are the
|
||||
// authority now, so this stale field must not narrow the user catalog.
|
||||
let access_token = build_test_auth_token(
|
||||
"access",
|
||||
serde_json::Map::from_iter([
|
||||
@@ -9757,31 +9777,25 @@ async fn gateway_filters_users_me_available_models_by_group_policy_and_hides_mod
|
||||
.await
|
||||
.expect("group should create")
|
||||
.expect("group should exist");
|
||||
user_repository
|
||||
.add_user_to_group(&group.id, "user-auth-1")
|
||||
.await
|
||||
.expect("group membership should create");
|
||||
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
let data_state = crate::data::GatewayDataState::with_global_model_reader_for_tests(
|
||||
global_model_repository,
|
||||
)
|
||||
let data_state =
|
||||
crate::data::GatewayDataState::with_global_model_reader_for_tests(global_model_repository)
|
||||
.with_user_reader(Arc::clone(&user_repository));
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_auth_sessions_for_tests([sample_auth_session(
|
||||
"user-auth-1",
|
||||
"session-users-me-group-models",
|
||||
"device-users-me-group-models",
|
||||
"refresh-token-placeholder",
|
||||
now,
|
||||
)])
|
||||
})
|
||||
.await;
|
||||
let state = AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_auth_sessions_for_tests([sample_auth_session(
|
||||
"user-auth-1",
|
||||
"session-users-me-group-models",
|
||||
"device-users-me-group-models",
|
||||
"refresh-token-placeholder",
|
||||
now,
|
||||
)]);
|
||||
let mutation_state = state.clone();
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| state).await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!("{gateway_url}/api/users/me/available-models"))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-users-me-group-models")
|
||||
@@ -9790,6 +9804,24 @@ async fn gateway_filters_users_me_available_models_by_group_policy_and_hides_mod
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["total"], 2);
|
||||
|
||||
mutation_state
|
||||
.replace_user_groups_for_user("user-auth-1", std::slice::from_ref(&group.id))
|
||||
.await
|
||||
.expect("group membership should create");
|
||||
|
||||
let response = client
|
||||
.get(format!("{gateway_url}/api/users/me/available-models"))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-users-me-group-models")
|
||||
.header("user-agent", "AetherTest/1.0")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed after group assignment");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
let models = payload["models"]
|
||||
@@ -9929,6 +9961,27 @@ async fn gateway_returns_service_unavailable_for_users_me_available_models_witho
|
||||
.expect("active global model ref should build")]),
|
||||
);
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![user]));
|
||||
let group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
name: "OpenAI provider only".to_string(),
|
||||
description: None,
|
||||
priority: 0,
|
||||
allowed_providers: Some(vec!["openai".to_string()]),
|
||||
allowed_providers_mode: "specific".to_string(),
|
||||
allowed_api_formats: None,
|
||||
allowed_api_formats_mode: "unrestricted".to_string(),
|
||||
allowed_models: None,
|
||||
allowed_models_mode: "unrestricted".to_string(),
|
||||
rate_limit: None,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("group should create")
|
||||
.expect("group should exist");
|
||||
user_repository
|
||||
.add_user_to_group(&group.id, "user-auth-1")
|
||||
.await
|
||||
.expect("group membership should create");
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
let data_state = crate::data::GatewayDataState::with_global_model_reader_for_tests(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -89,6 +89,13 @@ fn sample_cli_auth_snapshot(
|
||||
}
|
||||
|
||||
fn sample_provider(provider_id: &str) -> StoredProviderCatalogProvider {
|
||||
sample_provider_with_request_timeout(provider_id, None)
|
||||
}
|
||||
|
||||
fn sample_provider_with_request_timeout(
|
||||
provider_id: &str,
|
||||
request_timeout_secs: Option<f64>,
|
||||
) -> StoredProviderCatalogProvider {
|
||||
StoredProviderCatalogProvider::new(
|
||||
provider_id.to_string(),
|
||||
provider_id.to_string(),
|
||||
@@ -96,7 +103,17 @@ fn sample_provider(provider_id: &str) -> StoredProviderCatalogProvider {
|
||||
"custom".to_string(),
|
||||
)
|
||||
.expect("provider should build")
|
||||
.with_transport_fields(true, false, false, None, None, None, None, None, None)
|
||||
.with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
request_timeout_secs,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn sample_endpoint(endpoint_id: &str, provider_id: &str) -> StoredProviderCatalogEndpoint {
|
||||
@@ -435,6 +452,7 @@ async fn gateway_forwards_public_request_to_remote_tunnel_owner_before_fallback_
|
||||
async move {
|
||||
let (parts, body) = request.into_parts();
|
||||
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
*seen_owner_inner.lock().expect("mutex should lock") = Some(SeenOwnerRequest {
|
||||
path: parts
|
||||
.uri
|
||||
@@ -511,7 +529,10 @@ async fn gateway_forwards_public_request_to_remote_tunnel_owner_before_fallback_
|
||||
let (owner_url, owner_handle) = start_server(owner).await;
|
||||
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider("provider-owner")],
|
||||
vec![sample_provider_with_request_timeout(
|
||||
"provider-owner",
|
||||
Some(0.1),
|
||||
)],
|
||||
vec![sample_endpoint("endpoint-owner", "provider-owner")],
|
||||
vec![sample_key("key-owner", "provider-owner", "node-owner")],
|
||||
));
|
||||
@@ -540,6 +561,12 @@ async fn gateway_forwards_public_request_to_remote_tunnel_owner_before_fallback_
|
||||
state = state
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_tunnel_identity_for_tests("gateway-a", Some("http://gateway-a:8080"));
|
||||
let short_timeout_client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(10))
|
||||
.build()
|
||||
.expect("test client should build");
|
||||
state.client = short_timeout_client.clone();
|
||||
state.owner_forward_client = short_timeout_client;
|
||||
state.remember_scheduler_affinity_target(
|
||||
"scheduler_affinity:api-key-affinity-1:openai:chat:gpt-4.1",
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
@@ -922,32 +949,39 @@ async fn gateway_streamifies_sync_json_from_remote_tunnel_owner_before_returning
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
});
|
||||
let encoded_response = serde_json::to_vec(&json!({
|
||||
"id": "resp-codex-affinity-stream-123",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"status": "completed",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "msg-codex-affinity-stream-123",
|
||||
"role": "assistant",
|
||||
"content": [{
|
||||
"type": "output_text",
|
||||
"text": "Hello from affinity sync json",
|
||||
"annotations": []
|
||||
}]
|
||||
}],
|
||||
"usage": {
|
||||
"input_tokens": 1,
|
||||
"output_tokens": 2,
|
||||
"total_tokens": 3
|
||||
}
|
||||
}))
|
||||
.expect("body should encode");
|
||||
let split_at = encoded_response.len() / 2;
|
||||
let first = axum::body::Bytes::copy_from_slice(&encoded_response[..split_at]);
|
||||
let second = axum::body::Bytes::copy_from_slice(&encoded_response[split_at..]);
|
||||
let response_body = Body::from_stream(async_stream::stream! {
|
||||
yield Ok::<_, std::io::Error>(first);
|
||||
tokio::time::sleep(Duration::from_millis(40)).await;
|
||||
yield Ok::<_, std::io::Error>(second);
|
||||
});
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::from(
|
||||
serde_json::to_vec(&json!({
|
||||
"id": "resp-codex-affinity-stream-123",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"status": "completed",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "msg-codex-affinity-stream-123",
|
||||
"role": "assistant",
|
||||
"content": [{
|
||||
"type": "output_text",
|
||||
"text": "Hello from affinity sync json",
|
||||
"annotations": []
|
||||
}]
|
||||
}],
|
||||
"usage": {
|
||||
"input_tokens": 1,
|
||||
"output_tokens": 2,
|
||||
"total_tokens": 3
|
||||
}
|
||||
}))
|
||||
.expect("body should encode"),
|
||||
))
|
||||
.body(response_body)
|
||||
.expect("response should build");
|
||||
response.headers_mut().insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
@@ -1001,6 +1035,10 @@ async fn gateway_streamifies_sync_json_from_remote_tunnel_owner_before_returning
|
||||
state = state
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_tunnel_identity_for_tests("gateway-a", Some("http://gateway-a:8080"));
|
||||
state.client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(10))
|
||||
.build()
|
||||
.expect("short shared client should build");
|
||||
state.remember_scheduler_affinity_target(
|
||||
"scheduler_affinity:api-key-affinity-cli-1:openai:responses:gpt-5.4",
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
|
||||
@@ -101,6 +101,7 @@ pub(super) fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelec
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -1611,6 +1611,7 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -1926,6 +1927,7 @@ fn gateway_keeps_failed_usage_request_capture_lightweight_for_large_local_claude
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -268,6 +268,7 @@ fn sample_candidate_row(spec: ProviderSpec) -> StoredMinimalCandidateSelectionRo
|
||||
priority: 1,
|
||||
api_formats: Some(vec![spec.api_format.to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(true),
|
||||
model_is_active: true,
|
||||
@@ -804,7 +805,7 @@ fn gateway_records_openai_sync_usage_and_pricing_with_cache_tokens() {
|
||||
async fn gateway_records_openai_sync_usage_and_pricing_with_cache_tokens_impl() {
|
||||
let expected = ExpectedUsagePricing {
|
||||
input_tokens: 120,
|
||||
billed_input_tokens: 100,
|
||||
billed_input_tokens: 20,
|
||||
output_tokens: 40,
|
||||
cache_creation_tokens: 80,
|
||||
cache_creation_ephemeral_5m_tokens: 0,
|
||||
@@ -898,7 +899,7 @@ fn gateway_records_openai_stream_usage_and_pricing_with_cache_tokens() {
|
||||
async fn gateway_records_openai_stream_usage_and_pricing_with_cache_tokens_impl() {
|
||||
let expected = ExpectedUsagePricing {
|
||||
input_tokens: 240,
|
||||
billed_input_tokens: 200,
|
||||
billed_input_tokens: 120,
|
||||
output_tokens: 60,
|
||||
cache_creation_tokens: 80,
|
||||
cache_creation_ephemeral_5m_tokens: 0,
|
||||
|
||||
@@ -110,6 +110,7 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["gemini:video".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(false),
|
||||
model_is_active: true,
|
||||
|
||||
@@ -115,6 +115,7 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:video".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: Some(false),
|
||||
model_is_active: true,
|
||||
|
||||
Reference in New Issue
Block a user