mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix: preserve passthrough for same-format stream rewrites
This commit is contained in:
@@ -58,6 +58,14 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if needs_conversion {
|
if needs_conversion {
|
||||||
|
// CPA strategy: when provider and client share the same wire format
|
||||||
|
// (exact match or same family), pass through the stream verbatim.
|
||||||
|
// Parsing→rebuilding only adds overhead and may lose information
|
||||||
|
// (encrypted_content, original item IDs, etc.).
|
||||||
|
if is_same_format_family(provider_api_format.as_str(), client_api_format.as_str()) {
|
||||||
|
return model_directive_display_model_from_report_context(report_context)
|
||||||
|
.map(|_| FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||||
|
}
|
||||||
return supports_standard_stream_rewrite(
|
return supports_standard_stream_rewrite(
|
||||||
provider_api_format.as_str(),
|
provider_api_format.as_str(),
|
||||||
client_api_format.as_str(),
|
client_api_format.as_str(),
|
||||||
@@ -336,6 +344,31 @@ fn supports_standard_stream_rewrite(provider_api_format: &str, client_api_format
|
|||||||
|| is_standard_cli_client_api_format(client_api_format))
|
|| is_standard_cli_client_api_format(client_api_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true for OpenAI Responses family formats that share the same SSE
|
||||||
|
/// wire format and can be passed through without parsing→rebuilding.
|
||||||
|
fn is_openai_responses_family(api_format: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
aether_ai_formats::normalize_api_format_alias(api_format).as_str(),
|
||||||
|
"openai:responses" | "openai:responses:compact"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true when two API formats share the same SSE wire format and
|
||||||
|
/// can be passed through without parsing→rebuilding. This covers:
|
||||||
|
///
|
||||||
|
/// - Exact matches after normalisation (e.g. `claude:messages` ↔ `claude:messages`)
|
||||||
|
/// - OpenAI Responses family (`openai:responses` ↔ `openai:responses:compact`)
|
||||||
|
fn is_same_format_family(provider_format: &str, client_format: &str) -> bool {
|
||||||
|
let provider = aether_ai_formats::normalize_api_format_alias(provider_format);
|
||||||
|
let client = aether_ai_formats::normalize_api_format_alias(client_format);
|
||||||
|
if provider == client {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// OpenAI Responses family shares the same wire format despite having
|
||||||
|
// distinct format IDs.
|
||||||
|
is_openai_responses_family(provider_format) && is_openai_responses_family(client_format)
|
||||||
|
}
|
||||||
|
|
||||||
fn is_standard_provider_api_format(api_format: &str) -> bool {
|
fn is_standard_provider_api_format(api_format: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
aether_ai_formats::normalize_api_format_alias(api_format).as_str(),
|
aether_ai_formats::normalize_api_format_alias(api_format).as_str(),
|
||||||
@@ -526,6 +559,121 @@ data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp
|
|||||||
assert!(!output.contains("data: [DONE]"));
|
assert!(!output.contains("data: [DONE]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_family_responses_passthrough_preserves_encrypted_content() {
|
||||||
|
// When provider and client are both OpenAI Responses family,
|
||||||
|
// the stream should pass through verbatim (only model name rewrite).
|
||||||
|
// This preserves encrypted_content, original item IDs, etc.
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "openai:responses:compact",
|
||||||
|
"needs_conversion": true,
|
||||||
|
"model": "gpt-5.5-xhigh",
|
||||||
|
"mapped_model": "gpt-5.5",
|
||||||
|
});
|
||||||
|
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||||
|
.expect("rewriter should exist");
|
||||||
|
let output = rewriter
|
||||||
|
.push_chunk(
|
||||||
|
b"event: response.output_item.added\n\
|
||||||
|
data: {\"type\":\"response.output_item.added\",\"response_id\":\"resp_123\",\"output_index\":0,\"item\":{\"type\":\"reasoning\",\"id\":\"rs_abc\",\"summary\":[],\"encrypted_content\":\"EWxvY2tlZENvbnRlbnQ=\"}}\n\n",
|
||||||
|
)
|
||||||
|
.expect("rewrite should succeed");
|
||||||
|
let output = String::from_utf8(output).expect("output should be utf8");
|
||||||
|
|
||||||
|
// Passthrough preserves the full payload structure
|
||||||
|
assert!(output.contains("event: response.output_item.added"));
|
||||||
|
assert!(output.contains("\"encrypted_content\":\"EWxvY2tlZENvbnRlbnQ=\""));
|
||||||
|
assert!(output.contains("\"id\":\"rs_abc\""));
|
||||||
|
assert!(output.contains("\"type\":\"reasoning\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_family_responses_without_display_model_passes_through_verbatim() {
|
||||||
|
// When provider and client are both OpenAI Responses family but
|
||||||
|
// there is no display model override, the rewriter returns None
|
||||||
|
// (complete passthrough, no interception at all).
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "openai:responses:compact",
|
||||||
|
"needs_conversion": true,
|
||||||
|
});
|
||||||
|
assert!(maybe_build_ai_surface_stream_rewriter(Some(&report_context)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_claude_passthrough_with_display_model() {
|
||||||
|
// Claude→Claude with needs_conversion=true should pass through
|
||||||
|
// (only model name rewrite), not parse→rebuild.
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "claude:messages",
|
||||||
|
"client_api_format": "claude:messages",
|
||||||
|
"needs_conversion": true,
|
||||||
|
"model": "claude-sonnet-4.5-high",
|
||||||
|
"mapped_model": "claude-sonnet-4.5",
|
||||||
|
});
|
||||||
|
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||||
|
.expect("rewriter should exist");
|
||||||
|
let output = rewriter
|
||||||
|
.push_chunk(
|
||||||
|
b"event: content_block_delta\n\
|
||||||
|
data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Let me reason...\"}}\n\n",
|
||||||
|
)
|
||||||
|
.expect("rewrite should succeed");
|
||||||
|
let output = String::from_utf8(output).expect("output should be utf8");
|
||||||
|
|
||||||
|
// Passthrough preserves the exact wire format
|
||||||
|
assert!(output.contains("event: content_block_delta"));
|
||||||
|
assert!(output.contains("\"thinking\":\"Let me reason...\""));
|
||||||
|
assert!(output.contains("\"type\":\"thinking_delta\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_claude_without_display_model_passes_through_verbatim() {
|
||||||
|
// Claude→Claude without display model: no rewriter needed at all.
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "claude:messages",
|
||||||
|
"client_api_format": "claude:messages",
|
||||||
|
"needs_conversion": true,
|
||||||
|
});
|
||||||
|
assert!(maybe_build_ai_surface_stream_rewriter(Some(&report_context)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_gemini_passthrough_with_display_model() {
|
||||||
|
// Gemini→Gemini with needs_conversion=true should pass through.
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "gemini:generate_content",
|
||||||
|
"client_api_format": "gemini:generate_content",
|
||||||
|
"needs_conversion": true,
|
||||||
|
"model": "gemini-2.5-pro-high",
|
||||||
|
"mapped_model": "gemini-2.5-pro",
|
||||||
|
});
|
||||||
|
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||||
|
.expect("rewriter should exist");
|
||||||
|
let output = rewriter
|
||||||
|
.push_chunk(
|
||||||
|
b"data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello\"}],\"role\":\"model\"}}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
||||||
|
)
|
||||||
|
.expect("rewrite should succeed");
|
||||||
|
let output = String::from_utf8(output).expect("output should be utf8");
|
||||||
|
|
||||||
|
// Model version should be rewritten
|
||||||
|
assert!(output.contains("\"modelVersion\":\"gemini-2.5-pro-high\""));
|
||||||
|
assert!(!output.contains("\"modelVersion\":\"gemini-2.5-pro\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_format_gemini_without_display_model_passes_through_verbatim() {
|
||||||
|
// Gemini→Gemini without display model: no rewriter needed.
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "gemini:generate_content",
|
||||||
|
"client_api_format": "gemini:generate_content",
|
||||||
|
"needs_conversion": true,
|
||||||
|
});
|
||||||
|
assert!(maybe_build_ai_surface_stream_rewriter(Some(&report_context)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolves_openai_image_mode_for_same_format_image_streams() {
|
fn resolves_openai_image_mode_for_same_format_image_streams() {
|
||||||
let report_context = json!({
|
let report_context = json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user