mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(gateway): OpenAI 图像 finalize 统一返回 b64_json 并对齐 codex CLI 候选流式上游
- finalize 移除 response_format=url 的 data URL 分支,统一输出 b64_json - image planner decision 依据请求体中的 stream 字段决定 upstream_is_stream - openai chat sync plan 对 codex+openai:cli 候选强制上游流式 - 扩充 conversion registry 与 standard matrix 的全量 surface 对端测试
This commit is contained in:
@@ -396,6 +396,25 @@ mod tests {
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
const STANDARD_SURFACES: &[&str] = &[
|
||||
"openai:chat",
|
||||
"openai:cli",
|
||||
"claude:chat",
|
||||
"claude:cli",
|
||||
"gemini:chat",
|
||||
"gemini:cli",
|
||||
];
|
||||
|
||||
fn expected_request_conversion_kind(provider_api_format: &str) -> RequestConversionKind {
|
||||
match provider_api_format {
|
||||
"openai:chat" => RequestConversionKind::ToOpenAIChat,
|
||||
"openai:cli" => RequestConversionKind::ToOpenAIFamilyCli,
|
||||
"claude:chat" | "claude:cli" => RequestConversionKind::ToClaudeStandard,
|
||||
"gemini:chat" | "gemini:cli" => RequestConversionKind::ToGeminiStandard,
|
||||
other => panic!("unexpected provider api format: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_conversion_registry_supports_bidirectional_standard_matrix() {
|
||||
assert_eq!(
|
||||
@@ -433,6 +452,27 @@ mod tests {
|
||||
assert_eq!(request_conversion_kind("claude:chat", "claude:chat"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_conversion_registry_covers_all_standard_surface_pairs() {
|
||||
for client_api_format in STANDARD_SURFACES {
|
||||
for provider_api_format in STANDARD_SURFACES {
|
||||
let actual = request_conversion_kind(client_api_format, provider_api_format);
|
||||
if client_api_format == provider_api_format {
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"{client_api_format} -> {provider_api_format} should be same-format"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(expected_request_conversion_kind(provider_api_format)),
|
||||
"{client_api_format} -> {provider_api_format} should be routable"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
|
||||
assert_eq!(
|
||||
@@ -469,6 +509,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_response_conversion_registry_covers_all_standard_surface_pairs() {
|
||||
for provider_api_format in STANDARD_SURFACES {
|
||||
for client_api_format in ["openai:chat", "claude:chat", "gemini:chat"] {
|
||||
let actual =
|
||||
sync_chat_response_conversion_kind(provider_api_format, client_api_format);
|
||||
if *provider_api_format == client_api_format {
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"{provider_api_format} -> {client_api_format} should be same-format"
|
||||
);
|
||||
} else {
|
||||
let expected = match client_api_format {
|
||||
"openai:chat" => SyncChatResponseConversionKind::ToOpenAIChat,
|
||||
"claude:chat" => SyncChatResponseConversionKind::ToClaudeChat,
|
||||
"gemini:chat" => SyncChatResponseConversionKind::ToGeminiChat,
|
||||
other => panic!("unexpected chat client api format: {other}"),
|
||||
};
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(expected),
|
||||
"{provider_api_format} -> {client_api_format} should finalize to chat"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for client_api_format in ["openai:cli", "claude:cli", "gemini:cli"] {
|
||||
let actual =
|
||||
sync_cli_response_conversion_kind(provider_api_format, client_api_format);
|
||||
if *provider_api_format == client_api_format {
|
||||
assert_eq!(
|
||||
actual, None,
|
||||
"{provider_api_format} -> {client_api_format} should be same-format"
|
||||
);
|
||||
} else {
|
||||
let expected = match client_api_format {
|
||||
"openai:cli" => SyncCliResponseConversionKind::ToOpenAIFamilyCli,
|
||||
"claude:cli" => SyncCliResponseConversionKind::ToClaudeCli,
|
||||
"gemini:cli" => SyncCliResponseConversionKind::ToGeminiCli,
|
||||
other => panic!("unexpected cli client api format: {other}"),
|
||||
};
|
||||
assert_eq!(
|
||||
actual,
|
||||
Some(expected),
|
||||
"{provider_api_format} -> {client_api_format} should finalize to cli"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_candidate_registry_excludes_compact_as_cross_format_target() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -150,7 +150,124 @@ pub fn build_standard_upstream_url(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_standard_request_body;
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const STANDARD_SURFACES: &[&str] = &[
|
||||
"openai:chat",
|
||||
"openai:cli",
|
||||
"claude:chat",
|
||||
"claude:cli",
|
||||
"gemini:chat",
|
||||
"gemini:cli",
|
||||
];
|
||||
|
||||
fn sample_request_for(api_format: &str) -> (Value, &'static str) {
|
||||
match api_format {
|
||||
"openai:chat" => (
|
||||
json!({
|
||||
"model": "source-model",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Be concise."},
|
||||
{"role": "user", "content": "Hello matrix"}
|
||||
],
|
||||
"max_tokens": 32
|
||||
}),
|
||||
"/v1/chat/completions",
|
||||
),
|
||||
"openai:cli" => (
|
||||
json!({
|
||||
"model": "source-model",
|
||||
"instructions": "Be concise.",
|
||||
"input": "Hello matrix",
|
||||
"max_output_tokens": 32
|
||||
}),
|
||||
"/v1/responses",
|
||||
),
|
||||
"claude:chat" | "claude:cli" => (
|
||||
json!({
|
||||
"model": "source-model",
|
||||
"system": "Be concise.",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "Hello matrix"}]
|
||||
}],
|
||||
"max_tokens": 32
|
||||
}),
|
||||
"/v1/messages",
|
||||
),
|
||||
"gemini:chat" | "gemini:cli" => (
|
||||
json!({
|
||||
"systemInstruction": {
|
||||
"parts": [{"text": "Be concise."}]
|
||||
},
|
||||
"contents": [{
|
||||
"role": "user",
|
||||
"parts": [{"text": "Hello matrix"}]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 32
|
||||
}
|
||||
}),
|
||||
"/v1beta/models/source-model:generateContent",
|
||||
),
|
||||
other => panic!("unexpected api format: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_stream_flag(provider_api_format: &str, upstream_is_stream: bool, converted: &Value) {
|
||||
match provider_api_format {
|
||||
"openai:chat" | "openai:cli" | "claude:chat" | "claude:cli" => {
|
||||
assert_eq!(
|
||||
converted
|
||||
.get("stream")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
upstream_is_stream,
|
||||
"{provider_api_format} stream flag should follow upstream_is_stream"
|
||||
);
|
||||
}
|
||||
"gemini:chat" | "gemini:cli" => {
|
||||
assert!(
|
||||
converted.get("stream").is_none(),
|
||||
"gemini streaming is represented by endpoint URL, not request body"
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected provider api format: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_request_body_for_all_standard_surface_pairs_in_sync_and_stream_modes() {
|
||||
for client_api_format in STANDARD_SURFACES {
|
||||
let (request, request_path) = sample_request_for(client_api_format);
|
||||
for provider_api_format in STANDARD_SURFACES {
|
||||
for upstream_is_stream in [false, true] {
|
||||
let converted = build_standard_request_body(
|
||||
&request,
|
||||
client_api_format,
|
||||
"mapped-model",
|
||||
"custom",
|
||||
provider_api_format,
|
||||
request_path,
|
||||
upstream_is_stream,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{client_api_format} -> {provider_api_format} should build with upstream_is_stream={upstream_is_stream}"
|
||||
)
|
||||
});
|
||||
|
||||
assert_stream_flag(provider_api_format, upstream_is_stream, &converted);
|
||||
assert!(
|
||||
converted.to_string().contains("Hello matrix"),
|
||||
"{client_api_format} -> {provider_api_format} should retain user content"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_openai_chat_request_from_claude_chat_source() {
|
||||
|
||||
Reference in New Issue
Block a user