mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
feat(gateway): add Codex Live and OpenAI Realtime
Implement preflighted Live/Realtime WebSocket transports, protocol-aware authentication, usage auditing, UI filtering, and legacy Codex permission migration.
This commit is contained in:
@@ -9,12 +9,12 @@ pub use crate::contracts::{
|
||||
CLAUDE_CLI_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CLI_SYNC_ERROR_REPORT_KIND,
|
||||
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND, CLAUDE_CLI_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_SUCCESS_REPORT_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND, EXECUTION_RUNTIME_STREAM_ACTION,
|
||||
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_ACTION,
|
||||
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_ERROR_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND, CODEX_LIVE_STREAM_PLAN_KIND,
|
||||
EXECUTION_RUNTIME_STREAM_ACTION, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
|
||||
EXECUTION_RUNTIME_SYNC_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_ERROR_REPORT_KIND, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND,
|
||||
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
@@ -33,8 +33,8 @@ pub use crate::contracts::{
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_ERROR_REPORT_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
|
||||
@@ -14,20 +14,20 @@ pub use control_payloads::{build_ai_control_plan_request, AiControlPlanRequest};
|
||||
pub use plan_kinds::{
|
||||
is_openai_responses_stream_plan_kind, is_openai_responses_sync_plan_kind,
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND, CODEX_LIVE_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
|
||||
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
|
||||
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
|
||||
};
|
||||
pub use report_kinds::{
|
||||
core_error_background_report_kind, core_error_default_client_api_format,
|
||||
|
||||
@@ -17,6 +17,8 @@ pub const CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND: &str = "claude_count_tokens_sync";
|
||||
pub const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream";
|
||||
pub const GEMINI_INTERACTIONS_STREAM_PLAN_KIND: &str = "gemini_interactions_stream";
|
||||
pub const OPENAI_RESPONSES_STREAM_PLAN_KIND: &str = "openai_responses_stream";
|
||||
pub const OPENAI_REALTIME_STREAM_PLAN_KIND: &str = "openai_realtime_stream";
|
||||
pub const CODEX_LIVE_STREAM_PLAN_KIND: &str = "codex_live_stream";
|
||||
pub const OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND: &str = "openai_responses_compact_stream";
|
||||
pub const CLAUDE_CLI_STREAM_PLAN_KIND: &str = "claude_cli_stream";
|
||||
pub const GEMINI_CLI_STREAM_PLAN_KIND: &str = "gemini_cli_stream";
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{fmt, str::FromStr};
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum FormatFamily {
|
||||
OpenAi,
|
||||
Codex,
|
||||
Claude,
|
||||
Gemini,
|
||||
Jina,
|
||||
@@ -23,9 +24,11 @@ pub enum FormatId {
|
||||
OpenAiChat,
|
||||
OpenAiResponses,
|
||||
OpenAiResponsesCompact,
|
||||
OpenAiRealtime,
|
||||
OpenAiSearch,
|
||||
OpenAiEmbedding,
|
||||
OpenAiRerank,
|
||||
CodexLive,
|
||||
ClaudeMessages,
|
||||
GeminiGenerateContent,
|
||||
GeminiInteractions,
|
||||
@@ -50,9 +53,11 @@ impl FormatId {
|
||||
Self::OpenAiChat
|
||||
| Self::OpenAiResponses
|
||||
| Self::OpenAiResponsesCompact
|
||||
| Self::OpenAiRealtime
|
||||
| Self::OpenAiSearch
|
||||
| Self::OpenAiEmbedding
|
||||
| Self::OpenAiRerank => FormatFamily::OpenAi,
|
||||
Self::CodexLive => FormatFamily::Codex,
|
||||
Self::ClaudeMessages => FormatFamily::Claude,
|
||||
Self::GeminiGenerateContent | Self::GeminiInteractions | Self::GeminiEmbedding => {
|
||||
FormatFamily::Gemini
|
||||
@@ -75,9 +80,11 @@ impl FormatId {
|
||||
Self::OpenAiChat => "openai:chat",
|
||||
Self::OpenAiResponses => "openai:responses",
|
||||
Self::OpenAiResponsesCompact => "openai:responses:compact",
|
||||
Self::OpenAiRealtime => "openai:realtime",
|
||||
Self::OpenAiSearch => "openai:search",
|
||||
Self::OpenAiEmbedding => "openai:embedding",
|
||||
Self::OpenAiRerank => "openai:rerank",
|
||||
Self::CodexLive => "codex:live",
|
||||
Self::ClaudeMessages => "claude:messages",
|
||||
Self::GeminiGenerateContent => "gemini:generate_content",
|
||||
Self::GeminiInteractions => "gemini:interactions",
|
||||
@@ -106,11 +113,15 @@ impl FromStr for FormatId {
|
||||
"openai:responses:compact" | "/v1/responses/compact" => {
|
||||
Ok(Self::OpenAiResponsesCompact)
|
||||
}
|
||||
"openai:realtime" | "openai_realtime" | "realtime" | "/v1/realtime" => {
|
||||
Ok(Self::OpenAiRealtime)
|
||||
}
|
||||
"openai:search" | "openai_search" | "search" | "/v1/alpha/search" => {
|
||||
Ok(Self::OpenAiSearch)
|
||||
}
|
||||
"openai:embedding" | "/v1/embeddings" => Ok(Self::OpenAiEmbedding),
|
||||
"openai:rerank" | "/v1/rerank" => Ok(Self::OpenAiRerank),
|
||||
"codex:live" | "codex_live" | "live" | "/v1/live" => Ok(Self::CodexLive),
|
||||
"claude:messages" | "/v1/messages" => Ok(Self::ClaudeMessages),
|
||||
"gemini:generate_content" => Ok(Self::GeminiGenerateContent),
|
||||
"gemini:interactions"
|
||||
@@ -272,6 +283,59 @@ mod tests {
|
||||
assert_eq!(FormatId::parse("gemini:cli"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_and_codex_live_are_distinct_first_class_formats() {
|
||||
use super::{FormatFamily, FormatProfile};
|
||||
|
||||
for alias in [
|
||||
"openai:realtime",
|
||||
"OPENAI_REALTIME",
|
||||
"realtime",
|
||||
"/v1/realtime",
|
||||
] {
|
||||
assert_eq!(FormatId::parse(alias), Some(FormatId::OpenAiRealtime));
|
||||
assert_eq!(normalize_api_format_alias(alias), "openai:realtime");
|
||||
}
|
||||
for alias in ["codex:live", "CODEX_LIVE", "live", "/v1/live"] {
|
||||
assert_eq!(FormatId::parse(alias), Some(FormatId::CodexLive));
|
||||
assert_eq!(normalize_api_format_alias(alias), "codex:live");
|
||||
}
|
||||
|
||||
assert_eq!(FormatId::OpenAiRealtime.family(), FormatFamily::OpenAi);
|
||||
assert_eq!(FormatId::CodexLive.family(), FormatFamily::Codex);
|
||||
assert_eq!(FormatId::OpenAiRealtime.profile(), FormatProfile::Default);
|
||||
assert_eq!(FormatId::CodexLive.profile(), FormatProfile::Default);
|
||||
assert!(!api_format_uses_body_stream_field("openai:realtime"));
|
||||
assert!(!api_format_uses_body_stream_field("codex:live"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn realtime_live_and_responses_permissions_do_not_cover_each_other() {
|
||||
for (allowed, requested) in [
|
||||
("openai:responses", "openai:realtime"),
|
||||
("openai:responses", "codex:live"),
|
||||
("openai:realtime", "openai:responses"),
|
||||
("openai:realtime", "codex:live"),
|
||||
("codex:live", "openai:responses"),
|
||||
("codex:live", "openai:realtime"),
|
||||
] {
|
||||
assert!(!api_format_permission_covers(allowed, requested));
|
||||
}
|
||||
assert!(api_format_permission_covers(
|
||||
"OPENAI_REALTIME",
|
||||
"/v1/realtime"
|
||||
));
|
||||
assert!(api_format_permission_covers("CODEX_LIVE", "/v1/live"));
|
||||
assert_eq!(
|
||||
api_format_permission_storage_aliases("openai:realtime"),
|
||||
vec!["openai:realtime".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
api_format_permission_storage_aliases("codex:live"),
|
||||
vec!["codex:live".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_permission_covers_its_companion_endpoints() {
|
||||
assert!(api_format_permission_covers(
|
||||
|
||||
@@ -57,6 +57,9 @@ pub fn request_candidate_api_format_preference(
|
||||
if client_api_format == "openai:responses:compact" {
|
||||
return (provider_api_format == "openai:responses:compact").then_some((0, 0));
|
||||
}
|
||||
if matches!(client_api_format.as_str(), "openai:realtime" | "codex:live") {
|
||||
return (provider_api_format == client_api_format).then_some((0, 0));
|
||||
}
|
||||
if client_api_format == "openai:search" {
|
||||
return (provider_api_format == "openai:search").then_some((0, 0));
|
||||
}
|
||||
@@ -112,6 +115,12 @@ pub fn request_candidate_api_formats(
|
||||
if client_api_format == "openai:responses:compact" {
|
||||
return vec!["openai:responses:compact"];
|
||||
}
|
||||
if client_api_format == "openai:realtime" {
|
||||
return vec!["openai:realtime"];
|
||||
}
|
||||
if client_api_format == "codex:live" {
|
||||
return vec!["codex:live"];
|
||||
}
|
||||
if client_api_format == "openai:search" {
|
||||
return vec!["openai:search"];
|
||||
}
|
||||
@@ -746,6 +755,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_transports_only_select_their_exact_wire_format() {
|
||||
assert_eq!(
|
||||
request_candidate_api_formats("/v1/realtime", true),
|
||||
vec!["openai:realtime"]
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_formats("/v1/live", true),
|
||||
vec!["codex:live"]
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_format_preference("openai:realtime", "openai:realtime"),
|
||||
Some((0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_format_preference("codex:live", "codex:live"),
|
||||
Some((0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_format_preference("openai:realtime", "openai:responses"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_format_preference("codex:live", "openai:responses"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_conversion_enable_flag_only_applies_to_real_data_format_conversions() {
|
||||
assert!(!request_conversion_requires_enable_flag(
|
||||
|
||||
@@ -69,6 +69,7 @@ enum CodexOpenAiEndpointKind {
|
||||
Compact,
|
||||
Search,
|
||||
Images,
|
||||
Live,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -652,6 +653,8 @@ fn codex_openai_endpoint_kind(
|
||||
Some(CodexOpenAiEndpointKind::Search)
|
||||
} else if is_openai_image_request(provider_api_format) {
|
||||
Some(CodexOpenAiEndpointKind::Images)
|
||||
} else if aether_ai_formats::api_format_alias_matches(provider_api_format, "codex:live") {
|
||||
Some(CodexOpenAiEndpointKind::Live)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2079,7 +2082,13 @@ pub fn apply_codex_openai_special_headers(
|
||||
}
|
||||
return;
|
||||
}
|
||||
if endpoint_kind == CodexOpenAiEndpointKind::Images {
|
||||
if matches!(
|
||||
endpoint_kind,
|
||||
CodexOpenAiEndpointKind::Images | CodexOpenAiEndpointKind::Live
|
||||
) {
|
||||
if endpoint_kind == CodexOpenAiEndpointKind::Live {
|
||||
remove_btree_header(provider_request_headers, CODEX_RESPONSES_LITE_HEADER);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2873,6 +2882,42 @@ mod tests {
|
||||
assert!(!headers.contains_key("accept"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_live_uses_account_identity_without_responses_lite_headers() {
|
||||
let mut headers = std::collections::BTreeMap::from([(
|
||||
CODEX_RESPONSES_LITE_HEADER.to_string(),
|
||||
"true".to_string(),
|
||||
)]);
|
||||
|
||||
apply_codex_openai_special_headers(
|
||||
&mut headers,
|
||||
&json!({"model": "gpt-live"}),
|
||||
&http::HeaderMap::new(),
|
||||
"codex",
|
||||
"codex:live",
|
||||
Some("request-live"),
|
||||
Some(r#"{"account_id":"account-live","is_fedramp":true}"#),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get("chatgpt-account-id").map(String::as_str),
|
||||
Some("account-live")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("x-openai-fedramp").map(String::as_str),
|
||||
Some("true")
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("user-agent").map(String::as_str),
|
||||
Some(CODEX_CLIENT_USER_AGENT)
|
||||
);
|
||||
assert_eq!(
|
||||
headers.get("originator").map(String::as_str),
|
||||
Some(CODEX_CLIENT_ORIGINATOR)
|
||||
);
|
||||
assert!(!headers.contains_key(CODEX_RESPONSES_LITE_HEADER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_codex_models_do_not_send_the_responses_lite_header() {
|
||||
let mut headers = std::collections::BTreeMap::from([(
|
||||
|
||||
@@ -38,7 +38,10 @@ pub fn parse_request(
|
||||
FormatId::GeminiEmbedding => gemini::embedding::request::from(body, ctx),
|
||||
FormatId::DoubaoEmbedding => doubao::embedding::request::from(body, ctx),
|
||||
FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::from(body, ctx),
|
||||
FormatId::OpenAiSearch | FormatId::GeminiInteractions => None,
|
||||
FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::CodexLive
|
||||
| FormatId::GeminiInteractions => None,
|
||||
}
|
||||
.ok_or_else(|| FormatError::RequestParseFailed {
|
||||
format: source.as_str().to_string(),
|
||||
@@ -72,7 +75,10 @@ fn emit_request_inner(
|
||||
FormatId::GeminiEmbedding => gemini::embedding::request::to(request, ctx),
|
||||
FormatId::DoubaoEmbedding => doubao::embedding::request::to(request, ctx),
|
||||
FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::to(request, ctx),
|
||||
FormatId::OpenAiSearch | FormatId::GeminiInteractions => None,
|
||||
FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::CodexLive
|
||||
| FormatId::GeminiInteractions => None,
|
||||
}
|
||||
.ok_or_else(|| FormatError::RequestEmitFailed {
|
||||
format: target.as_str().to_string(),
|
||||
@@ -296,6 +302,7 @@ pub fn parse_response(
|
||||
FormatId::ClaudeMessages => claude_messages::response::from(body, ctx),
|
||||
FormatId::GeminiGenerateContent => gemini_generate_content::response::from(body, ctx),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::OpenAiRerank
|
||||
@@ -303,6 +310,7 @@ pub fn parse_response(
|
||||
| FormatId::GeminiEmbedding
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive
|
||||
| FormatId::GeminiInteractions => None,
|
||||
}
|
||||
.ok_or_else(|| FormatError::ResponseParseFailed {
|
||||
@@ -335,6 +343,7 @@ fn emit_response_inner(
|
||||
FormatId::ClaudeMessages => claude_messages::response::to(response, ctx),
|
||||
FormatId::GeminiGenerateContent => gemini_generate_content::response::to(response, ctx),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::OpenAiRerank
|
||||
@@ -342,6 +351,7 @@ fn emit_response_inner(
|
||||
| FormatId::GeminiEmbedding
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive
|
||||
| FormatId::GeminiInteractions => None,
|
||||
}
|
||||
.ok_or_else(|| FormatError::ResponseEmitFailed {
|
||||
@@ -1086,13 +1096,15 @@ fn standard_request_root_field_is_audited(source: FormatId, key: &str) -> bool {
|
||||
| "tools"
|
||||
),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::OpenAiRerank
|
||||
| FormatId::GeminiEmbedding
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::JinaRerank
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding => true,
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive => true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1575,13 +1587,15 @@ fn validate_source_response_stop_enums(
|
||||
FormatId::GeminiGenerateContent => validate_gemini_response_finish_reasons(body, target),
|
||||
FormatId::GeminiInteractions => Ok(()),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::OpenAiRerank
|
||||
| FormatId::GeminiEmbedding
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::JinaRerank
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding => Ok(()),
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::contracts::{
|
||||
ApiOperation, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
CLAUDE_COUNT_TOKENS_SYNC_SUCCESS_REPORT_KIND, CODEX_LIVE_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_REALTIME_STREAM_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_SYNC_SUCCESS_REPORT_KIND,
|
||||
};
|
||||
|
||||
@@ -114,6 +115,22 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
|
||||
|
||||
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
|
||||
match plan_kind {
|
||||
CODEX_LIVE_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "codex:live",
|
||||
decision_kind: CODEX_LIVE_STREAM_PLAN_KIND,
|
||||
report_kind: "codex_live_websocket_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
operation: None,
|
||||
}),
|
||||
OPENAI_REALTIME_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "openai:realtime",
|
||||
decision_kind: OPENAI_REALTIME_STREAM_PLAN_KIND,
|
||||
report_kind: "openai_realtime_websocket_success",
|
||||
family: LocalSameFormatProviderFamily::Standard,
|
||||
require_streaming: true,
|
||||
operation: None,
|
||||
}),
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
|
||||
api_format: "claude:messages",
|
||||
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
|
||||
@@ -178,6 +195,24 @@ mod tests {
|
||||
assert!(spec.require_streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_realtime_websocket_same_format_spec() {
|
||||
let spec = resolve_stream_spec("openai_realtime_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "openai:realtime");
|
||||
assert_eq!(spec.report_kind, "openai_realtime_websocket_success");
|
||||
assert!(spec.require_streaming);
|
||||
assert_eq!(spec.family, super::LocalSameFormatProviderFamily::Standard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_codex_live_websocket_same_format_spec() {
|
||||
let spec = resolve_stream_spec("codex_live_stream").expect("spec");
|
||||
assert_eq!(spec.api_format, "codex:live");
|
||||
assert_eq!(spec.report_kind, "codex_live_websocket_success");
|
||||
assert!(spec.require_streaming);
|
||||
assert_eq!(spec.family, super::LocalSameFormatProviderFamily::Standard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_embedding_sync_same_format_spec() {
|
||||
let spec = resolve_sync_spec("openai_embedding_sync").expect("spec");
|
||||
|
||||
@@ -4,13 +4,14 @@ use url::form_urlencoded;
|
||||
use crate::contracts::{
|
||||
ClientSurface, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, CLAUDE_COUNT_TOKENS_SYNC_PLAN_KIND,
|
||||
GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
|
||||
GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
|
||||
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
|
||||
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
|
||||
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
|
||||
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
|
||||
CODEX_LIVE_STREAM_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
|
||||
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
|
||||
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_REALTIME_STREAM_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
@@ -116,6 +117,22 @@ pub fn resolve_execution_runtime_stream_plan_kind_with_client_surface(
|
||||
return Some(OPENAI_RESPONSES_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("realtime")
|
||||
&& *method == Method::GET
|
||||
&& path == "/v1/realtime"
|
||||
{
|
||||
return Some(OPENAI_REALTIME_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("codex")
|
||||
&& route_kind == Some("live")
|
||||
&& ((*method == Method::GET && (path == "/v1/live" || path.starts_with("/v1/live/")))
|
||||
|| (*method == Method::POST && path == "/v1/live"))
|
||||
{
|
||||
return Some(CODEX_LIVE_STREAM_PLAN_KIND);
|
||||
}
|
||||
|
||||
if route_family == Some("openai")
|
||||
&& route_kind == Some("image")
|
||||
&& *method == Method::POST
|
||||
@@ -465,7 +482,7 @@ pub fn sanitize_request_path_and_query(path: &str, query: Option<&str>) -> Optio
|
||||
fn request_query_key_is_safe_to_trace(key: &str) -> bool {
|
||||
matches!(
|
||||
key.to_ascii_lowercase().as_str(),
|
||||
"alt" | "view" | "pagesize" | "page_size" | "limit" | "offset"
|
||||
"alt" | "view" | "pagesize" | "page_size" | "limit" | "offset" | "model"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -541,6 +558,8 @@ pub fn supports_stream_execution_decision_kind(plan_kind: &str) -> bool {
|
||||
matches!(
|
||||
plan_kind,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND
|
||||
| CODEX_LIVE_STREAM_PLAN_KIND
|
||||
| OPENAI_REALTIME_STREAM_PLAN_KIND
|
||||
| CLAUDE_CHAT_STREAM_PLAN_KIND
|
||||
| GEMINI_CHAT_STREAM_PLAN_KIND
|
||||
| OPENAI_RESPONSES_STREAM_PLAN_KIND
|
||||
@@ -567,14 +586,15 @@ mod tests {
|
||||
};
|
||||
use crate::contracts::{
|
||||
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
|
||||
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
|
||||
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
|
||||
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
|
||||
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
|
||||
OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
CLAUDE_CLI_SYNC_PLAN_KIND, CODEX_LIVE_STREAM_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND,
|
||||
GEMINI_CHAT_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND,
|
||||
GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
|
||||
GEMINI_INTERACTIONS_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND,
|
||||
OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
|
||||
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_REALTIME_STREAM_PLAN_KIND,
|
||||
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
|
||||
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_SEARCH_SYNC_PLAN_KIND,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -646,6 +666,59 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_realtime_as_websocket_stream_only() {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("realtime"),
|
||||
None,
|
||||
&Method::GET,
|
||||
"/v1/realtime",
|
||||
),
|
||||
Some(OPENAI_REALTIME_STREAM_PLAN_KIND)
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_sync_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("openai"),
|
||||
Some("realtime"),
|
||||
None,
|
||||
&Method::GET,
|
||||
"/v1/realtime",
|
||||
),
|
||||
None
|
||||
);
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
OPENAI_REALTIME_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_codex_live_call_create_and_websocket_routes() {
|
||||
for (method, path) in [
|
||||
(Method::POST, "/v1/live"),
|
||||
(Method::GET, "/v1/live"),
|
||||
(Method::GET, "/v1/live/rtc_opaque"),
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_execution_runtime_stream_plan_kind(
|
||||
Some("ai_public"),
|
||||
Some("codex"),
|
||||
Some("live"),
|
||||
None,
|
||||
&method,
|
||||
path,
|
||||
),
|
||||
Some(CODEX_LIVE_STREAM_PLAN_KIND)
|
||||
);
|
||||
}
|
||||
assert!(supports_stream_execution_decision_kind(
|
||||
CODEX_LIVE_STREAM_PLAN_KIND
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_openai_responses_compact_plan_kinds() {
|
||||
assert_eq!(
|
||||
@@ -892,6 +965,14 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some("/v1/live/{call_id}?alt=sse")
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_request_path_and_query(
|
||||
"/v1/realtime?model=gpt-realtime-2.1&api_key=secret&token=hidden",
|
||||
None
|
||||
)
|
||||
.as_deref(),
|
||||
Some("/v1/realtime?model=gpt-realtime-2.1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -418,6 +418,7 @@ impl ProviderStreamParser {
|
||||
FormatId::ClaudeMessages => Self::Claude(ClaudeProviderState::default()),
|
||||
FormatId::GeminiGenerateContent => Self::Gemini(GeminiProviderState::default()),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::OpenAiRerank
|
||||
| FormatId::GeminiEmbedding
|
||||
@@ -425,7 +426,8 @@ impl ProviderStreamParser {
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::JinaRerank
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding => return None,
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -537,6 +539,7 @@ impl ClientStreamEmitter {
|
||||
FormatId::ClaudeMessages => Self::Claude(ClaudeClientEmitter::default()),
|
||||
FormatId::GeminiGenerateContent => Self::Gemini(GeminiClientEmitter::default()),
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::OpenAiRerank
|
||||
| FormatId::GeminiEmbedding
|
||||
@@ -544,7 +547,8 @@ impl ClientStreamEmitter {
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::JinaRerank
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding => return None,
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -660,13 +664,15 @@ fn parse_provider_error(
|
||||
parse_gemini_error(payload)
|
||||
}
|
||||
FormatId::OpenAiEmbedding
|
||||
| FormatId::OpenAiRealtime
|
||||
| FormatId::OpenAiSearch
|
||||
| FormatId::OpenAiRerank
|
||||
| FormatId::GeminiEmbedding
|
||||
| FormatId::JinaEmbedding
|
||||
| FormatId::JinaRerank
|
||||
| FormatId::DoubaoEmbedding
|
||||
| FormatId::AliyunMultimodalEmbedding => None,
|
||||
| FormatId::AliyunMultimodalEmbedding
|
||||
| FormatId::CodexLive => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user