Add Chat Completions routing for Codex providers

- Add a Codex API format selector and routing badge for Chat Completions providers.
- Convert Codex Responses requests to upstream Chat Completions when routing is required.
- Convert Chat Completions JSON and SSE responses back to Responses format.
- Keep generated Codex wire_api values on Responses for Codex compatibility.
- Add i18n labels, provider metadata handling, and focused conversion tests.
This commit is contained in:
Jason
2026-05-19 10:23:48 +08:00
parent 61e68d754c
commit 1c82b8a3fa
19 changed files with 2230 additions and 35 deletions
+50 -17
View File
@@ -1108,20 +1108,29 @@ impl RequestForwarder {
Some(api_format) => super::providers::claude_api_format_needs_transform(api_format),
None => adapter.needs_transform(provider),
};
let (effective_endpoint, passthrough_query) =
if needs_transform && adapter.name() == "Claude" {
let api_format = resolved_claude_api_format
.as_deref()
.unwrap_or_else(|| super::providers::get_claude_api_format(provider));
rewrite_claude_transform_endpoint(endpoint, api_format, is_copilot, &mapped_body)
} else {
(
endpoint.to_string(),
split_endpoint_and_query(endpoint)
.1
.map(ToString::to_string),
)
};
let codex_responses_to_chat = matches!(app_type, AppType::Codex)
&& super::providers::should_convert_codex_responses_to_chat(provider, endpoint);
let (effective_endpoint, passthrough_query) = if codex_responses_to_chat {
rewrite_codex_responses_endpoint_to_chat(endpoint)
} else if needs_transform && adapter.name() == "Claude" {
let api_format = resolved_claude_api_format
.as_deref()
.unwrap_or_else(|| super::providers::get_claude_api_format(provider));
rewrite_claude_transform_endpoint(endpoint, api_format, is_copilot, &mapped_body)
} else {
(
endpoint.to_string(),
split_endpoint_and_query(endpoint)
.1
.map(ToString::to_string),
)
};
let codex_chat_base_is_full_endpoint = codex_responses_to_chat
&& base_url
.trim_end_matches('/')
.to_ascii_lowercase()
.ends_with("/chat/completions");
let url = if matches!(resolved_claude_api_format.as_deref(), Some("gemini_native")) {
super::gemini_url::resolve_gemini_native_url(
@@ -1129,14 +1138,16 @@ impl RequestForwarder {
&effective_endpoint,
is_full_url,
)
} else if is_full_url {
} else if is_full_url || codex_chat_base_is_full_endpoint {
append_query_to_full_url(&base_url, passthrough_query.as_deref())
} else {
adapter.build_url(&base_url, &effective_endpoint)
};
// 转换请求体(如果需要)
let request_body = if needs_transform {
let request_body = if codex_responses_to_chat {
super::providers::transform_codex_chat::responses_to_chat_completions(mapped_body)?
} else if needs_transform {
if adapter.name() == "Claude" {
let api_format = resolved_claude_api_format
.as_deref()
@@ -1169,7 +1180,8 @@ impl RequestForwarder {
);
let request_is_streaming =
is_streaming_request(&effective_endpoint, &filtered_body, headers);
let force_identity_encoding = needs_transform || request_is_streaming;
let force_identity_encoding =
needs_transform || codex_responses_to_chat || request_is_streaming;
// Codex OAuth 需要注入的 ChatGPT-Account-Id(在动态 token 获取期间填充)
let mut codex_oauth_account_id: Option<String> = None;
@@ -2037,6 +2049,18 @@ fn is_claude_messages_path(path: &str) -> bool {
matches!(path, "/v1/messages" | "/claude/v1/messages")
}
fn rewrite_codex_responses_endpoint_to_chat(endpoint: &str) -> (String, Option<String>) {
let (_path, query) = split_endpoint_and_query(endpoint);
let passthrough_query = query.map(ToString::to_string);
let target_path = "/chat/completions";
let rewritten = match passthrough_query.as_deref() {
Some(query) if !query.is_empty() => format!("{target_path}?{query}"),
_ => target_path.to_string(),
};
(rewritten, passthrough_query)
}
fn rewrite_claude_transform_endpoint(
endpoint: &str,
api_format: &str,
@@ -2760,6 +2784,15 @@ mod tests {
assert_eq!(passthrough_query.as_deref(), Some("x-id=1"));
}
#[test]
fn rewrite_codex_responses_endpoint_to_chat_preserves_query() {
let (endpoint, passthrough_query) =
rewrite_codex_responses_endpoint_to_chat("/v1/responses?foo=bar");
assert_eq!(endpoint, "/chat/completions?foo=bar");
assert_eq!(passthrough_query.as_deref(), Some("foo=bar"));
}
#[test]
fn rewrite_claude_transform_endpoint_uses_copilot_path() {
let (endpoint, passthrough_query) = rewrite_claude_transform_endpoint(