mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
Preserve exact request payloads and model client surface and API operation explicitly. Add Anthropic compatibility profiles, bounded stream commitment, and scoped OAuth retry behavior across provider transports.
1165 lines
53 KiB
Rust
1165 lines
53 KiB
Rust
use serde_json::json;
|
|
|
|
use crate::ai_serving::maybe_bridge_standard_sync_json_to_stream;
|
|
|
|
use super::maybe_build_local_stream_rewriter;
|
|
|
|
fn utf8(bytes: Vec<u8>) -> String {
|
|
String::from_utf8(bytes).expect("utf8 should decode")
|
|
}
|
|
|
|
#[test]
|
|
fn same_format_claude_local_stream_rewriter_sanitizes_read_input_json_delta() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "claude:messages",
|
|
"anthropic_compatibility_profile": "claude_code_legacy",
|
|
"needs_conversion": false,
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let mut output = rewriter
|
|
.push_chunk(
|
|
b"event: content_block_start\n\
|
|
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_read_1\",\"name\":\"Read\",\"input\":{}}}\n\n",
|
|
)
|
|
.expect("start should be accepted");
|
|
output.extend(
|
|
rewriter
|
|
.push_chunk(
|
|
b"event: content_block_delta\n\
|
|
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"file_path\\\":\\\"/tmp/a.txt\\\",\\\"pages\\\":\\\"\\\"}\"}}\n\n",
|
|
)
|
|
.expect("delta should be accepted"),
|
|
);
|
|
output.extend(
|
|
rewriter
|
|
.push_chunk(
|
|
b"event: content_block_stop\n\
|
|
data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
|
)
|
|
.expect("stop should flush sanitized delta"),
|
|
);
|
|
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("\"name\":\"Read\""));
|
|
assert!(output_text.contains("\\\"file_path\\\":\\\"/tmp/a.txt\\\""));
|
|
assert!(!output_text.contains("\\\"pages\\\":\\\"\\\""));
|
|
}
|
|
|
|
#[test]
|
|
fn standard_sync_bridge_converts_openai_chat_sync_json_to_openai_chat_sse() {
|
|
let outcome = maybe_bridge_standard_sync_json_to_stream(
|
|
&json!({
|
|
"id": "chatcmpl_sync_123",
|
|
"object": "chat.completion",
|
|
"model": "gpt-5.4",
|
|
"choices": [{
|
|
"index": 0,
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": "Hello from sync bridge"
|
|
},
|
|
"finish_reason": "stop"
|
|
}],
|
|
"usage": {
|
|
"prompt_tokens": 1,
|
|
"completion_tokens": 2,
|
|
"total_tokens": 3
|
|
}
|
|
}),
|
|
"openai:chat",
|
|
"openai:chat",
|
|
None,
|
|
)
|
|
.expect("bridge should succeed")
|
|
.expect("bridge should produce sse");
|
|
|
|
let output_text = utf8(outcome.sse_body);
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"role\":\"assistant\""));
|
|
assert!(output_text.contains("\"content\":\"Hello from sync bridge\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
let summary = outcome
|
|
.terminal_summary
|
|
.expect("terminal summary should exist");
|
|
assert_eq!(summary.response_id.as_deref(), Some("resp_sync_123"));
|
|
assert_eq!(summary.model.as_deref(), Some("gpt-5.4"));
|
|
assert_eq!(summary.finish_reason.as_deref(), Some("stop"));
|
|
assert_eq!(
|
|
summary
|
|
.standardized_usage
|
|
.as_ref()
|
|
.and_then(|usage| usage.dimensions.get("total_tokens"))
|
|
.cloned(),
|
|
Some(json!(3))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_sync_bridge_converts_claude_sync_json_to_openai_responses_sse() {
|
|
let report_context = json!({
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let outcome = maybe_bridge_standard_sync_json_to_stream(
|
|
&json!({
|
|
"id": "msg_sync_123",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": "claude-sonnet-4-5",
|
|
"content": [{
|
|
"type": "text",
|
|
"text": "Hello from Claude sync"
|
|
}],
|
|
"stop_reason": "end_turn",
|
|
"usage": {
|
|
"input_tokens": 2,
|
|
"output_tokens": 3
|
|
}
|
|
}),
|
|
"claude:messages",
|
|
"openai:responses",
|
|
Some(&report_context),
|
|
)
|
|
.expect("bridge should succeed")
|
|
.expect("bridge should produce sse");
|
|
|
|
let output_text = utf8(outcome.sse_body);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_text.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"text\":\"Hello from Claude sync\""));
|
|
let summary = outcome
|
|
.terminal_summary
|
|
.expect("terminal summary should exist");
|
|
assert_eq!(summary.response_id.as_deref(), Some("msg_sync_123"));
|
|
assert_eq!(summary.model.as_deref(), Some("claude-sonnet-4-5"));
|
|
assert_eq!(summary.finish_reason.as_deref(), Some("stop"));
|
|
assert_eq!(
|
|
summary
|
|
.standardized_usage
|
|
.as_ref()
|
|
.and_then(|usage| usage.dimensions.get("total_tokens"))
|
|
.cloned(),
|
|
Some(json!(5))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn antigravity_stream_rewriter_unwraps_and_injects_tool_ids() {
|
|
let report_context = json!({
|
|
"has_envelope": true,
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "gemini:generate_content",
|
|
"envelope_name": "antigravity:v1internal",
|
|
"needs_conversion": false,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\"},\"responseId\":\"resp_123\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("text should be utf8");
|
|
assert!(output_text.contains("\"_v1internal_response_id\":\"resp_123\""));
|
|
assert!(output_text.contains("\"id\":\"call_get_weather_0\""));
|
|
assert!(output_text.contains("\"modelVersion\":\"claude-sonnet-4-5\""));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_emits_completed_event_for_generate() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "generate"
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let first = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.output_item.done\n",
|
|
"data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"ig_123\",\"type\":\"image_generation_call\",\"result\":\"aGVsbG8=\"}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
assert!(first.is_empty());
|
|
|
|
let second = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"tool_usage\":{\"image_gen\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.contains("event: image_generation.completed"));
|
|
assert!(output_text.contains("\"type\":\"image_generation.completed\""));
|
|
assert!(output_text.contains("\"b64_json\":\"aGVsbG8=\""));
|
|
assert!(output_text.contains("\"input_tokens\":1"));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_maps_responses_partial_image_events() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "generate",
|
|
"partial_images": 1
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let partial = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.image_generation_call.partial_image\n",
|
|
"data: {\"type\":\"response.image_generation_call.partial_image\",\"partial_image_index\":0,\"partial_image_b64\":\"cGFydGlhbA==\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let partial_text = utf8(partial);
|
|
assert!(partial_text.contains("event: image_generation.partial_image"));
|
|
assert!(partial_text.contains("\"type\":\"image_generation.partial_image\""));
|
|
assert!(partial_text.contains("\"b64_json\":\"cGFydGlhbA==\""));
|
|
assert!(partial_text.contains("\"partial_image_index\":0"));
|
|
assert!(!partial_text.contains("response.image_generation_call.partial_image"));
|
|
|
|
let done = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.output_item.done\n",
|
|
"data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"ig_123\",\"type\":\"image_generation_call\",\"result\":\"ZmluYWw=\"}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
assert!(done.is_empty());
|
|
|
|
let completed = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":4,\"output_tokens\":5,\"total_tokens\":9}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let completed_text = utf8(completed);
|
|
assert!(completed_text.contains("event: image_generation.completed"));
|
|
assert!(completed_text.contains("\"type\":\"image_generation.completed\""));
|
|
assert!(completed_text.contains("\"b64_json\":\"ZmluYWw=\""));
|
|
assert!(completed_text.contains("\"total_tokens\":9"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_reads_final_image_from_completed_response_output() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "generate"
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let completed = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"output\":[{\"type\":\"message\"},{\"type\":\"image_generation_call\",\"result\":\"ZnJvbV9vdXRwdXQ=\"}],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let completed_text = utf8(completed);
|
|
assert!(completed_text.contains("event: image_generation.completed"));
|
|
assert!(completed_text.contains("\"b64_json\":\"ZnJvbV9vdXRwdXQ=\""));
|
|
assert!(completed_text.contains("\"total_tokens\":3"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_maps_upstream_error_to_generation_failed() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "generate"
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: error\n",
|
|
"data: {\"type\":\"error\",\"error\":{\"type\":\"input-images\",\"code\":\"rate_limit_exceeded\",\"message\":\"Rate limit reached for gpt-image-2\",\"param\":null}}\n\n",
|
|
"event: response.failed\n",
|
|
"data: {\"type\":\"response.failed\",\"response\":{\"status\":\"failed\",\"error\":{\"code\":\"rate_limit_exceeded\",\"message\":\"Rate limit reached for gpt-image-2\"}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: image_generation.failed"));
|
|
assert_eq!(
|
|
output_text
|
|
.matches("event: image_generation.failed")
|
|
.count(),
|
|
1
|
|
);
|
|
assert!(output_text.contains("\"type\":\"image_generation.failed\""));
|
|
assert!(output_text.contains("\"type\":\"input-images\""));
|
|
assert!(output_text.contains("\"code\":\"rate_limit_exceeded\""));
|
|
assert!(output_text.contains("\"message\":\"Rate limit reached for gpt-image-2\""));
|
|
assert!(!output_text.contains("response.failed"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_maps_response_failed_to_edit_failed() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "edit"
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.failed\n",
|
|
"data: {\"type\":\"response.failed\",\"response\":{\"status\":\"failed\",\"error\":{\"code\":\"rate_limit_exceeded\",\"message\":\"slow down\"}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: image_edit.failed"));
|
|
assert!(output_text.contains("\"type\":\"image_edit.failed\""));
|
|
assert!(output_text.contains("\"type\":\"rate_limit_exceeded\""));
|
|
assert!(output_text.contains("\"code\":\"rate_limit_exceeded\""));
|
|
assert!(output_text.contains("\"message\":\"slow down\""));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_image_stream_rewriter_emits_partial_and_completed_events_for_edit() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:image",
|
|
"client_api_format": "openai:image",
|
|
"needs_conversion": false,
|
|
"image_request": {
|
|
"operation": "edit",
|
|
"partial_images": 2
|
|
}
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
|
|
let partial = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.output_item.done\n",
|
|
"data: {\"type\":\"response.output_item.done\",\"output_index\":1,\"item\":{\"id\":\"ig_edit_123\",\"type\":\"image_generation_call\",\"result\":\"d29ybGQ=\"}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let partial_text = utf8(partial);
|
|
assert!(partial_text.contains("event: image_edit.partial_image"));
|
|
assert!(partial_text.contains("\"type\":\"image_edit.partial_image\""));
|
|
assert!(partial_text.contains("\"b64_json\":\"d29ybGQ=\""));
|
|
assert!(partial_text.contains("\"partial_image_index\":1"));
|
|
|
|
let completed = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":4,\"output_tokens\":5,\"total_tokens\":9}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let completed_text = utf8(completed);
|
|
assert!(completed_text.contains("event: image_edit.completed"));
|
|
assert!(completed_text.contains("\"type\":\"image_edit.completed\""));
|
|
assert!(completed_text.contains("\"b64_json\":\"d29ybGQ=\""));
|
|
assert!(completed_text.contains("\"total_tokens\":9"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_cli_v1internal_stream_rewriter_unwraps_response_object() {
|
|
let report_context = json!({
|
|
"has_envelope": true,
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "gemini:generate_content",
|
|
"envelope_name": "gemini_cli:v1internal",
|
|
"needs_conversion": false,
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Gemini CLI\"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-cli-2.5\"}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("text should be utf8");
|
|
assert_eq!(
|
|
output_text,
|
|
"data: {\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Gemini CLI\"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-cli-2.5\"}\n\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"error\":{\"message\":\"bad request\",\"type\":\"invalid_request_error\",\"code\":\"invalid_request\"}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.failed"));
|
|
assert!(output_text.contains("\"sequence_number\":1"));
|
|
assert!(output_text.contains("\"message\":\"bad request\""));
|
|
assert!(output_text.contains("\"type\":\"invalid_request_error\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn claude_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: error\n",
|
|
"data: {\"type\":\"error\",\"error\":{\"type\":\"rate_limit_error\",\"message\":\"slow down\",\"code\":\"rate_limit\"}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.failed"));
|
|
assert!(output_text.contains("\"sequence_number\":1"));
|
|
assert!(output_text.contains("\"message\":\"slow down\""));
|
|
assert!(output_text.contains("\"type\":\"rate_limit_error\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_error_to_openai_responses_stream_rewriter_converts_to_response_failed() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"error\":{\"code\":429,\"message\":\"quota exceeded\",\"status\":\"RESOURCE_EXHAUSTED\"}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.failed"));
|
|
assert!(output_text.contains("\"sequence_number\":1"));
|
|
assert!(output_text.contains("\"message\":\"quota exceeded\""));
|
|
assert!(output_text.contains("\"type\":\"rate_limit_error\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_error_to_claude_chat_stream_rewriter_uses_error_event_line() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "claude:messages",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"error\":{\"message\":\"bad request\",\"type\":\"invalid_request_error\",\"code\":\"invalid_request\"}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: error"));
|
|
assert!(output_text.contains("\"type\":\"error\""));
|
|
assert!(output_text.contains("\"message\":\"bad request\""));
|
|
assert!(output_text.contains("\"code\":\"invalid_request\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_error_to_gemini_chat_stream_rewriter_keeps_data_only_error() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "gemini:generate_content",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"error\":{\"message\":\"rate limited\",\"type\":\"rate_limit_error\",\"code\":\"rate_limit\"}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.starts_with("data: {\"error\":"));
|
|
assert!(!output_text.contains("event: "));
|
|
assert!(output_text.contains("\"message\":\"rate limited\""));
|
|
assert!(output_text.contains("\"status\":\"RESOURCE_EXHAUSTED\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn claude_to_openai_chat_stream_rewriter_converts_text_deltas() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: message_start\n",
|
|
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"model\":\"claude-sonnet-4-5\"}}\n\n",
|
|
"event: content_block_delta\n",
|
|
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\n",
|
|
"event: message_delta\n",
|
|
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"}}\n\n",
|
|
"event: message_stop\n",
|
|
"data: {\"type\":\"message_stop\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("utf8 should decode");
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"role\":\"assistant\""));
|
|
assert!(output_text.contains("\"content\":\"Hello\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
}
|
|
|
|
#[test]
|
|
fn claude_to_openai_chat_stream_rewriter_converts_tool_use_to_tool_calls() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: message_start\n",
|
|
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_tool_claude_chat_stream_123\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null}}\n\n",
|
|
"event: content_block_start\n",
|
|
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Need a tool.\"}}\n\n",
|
|
"event: content_block_stop\n",
|
|
"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
|
"event: content_block_start\n",
|
|
"data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"tool_123\",\"name\":\"get_weather\",\"input\":{\"location\":\"Tokyo\"}}}\n\n",
|
|
"event: message_delta\n",
|
|
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"}}\n\n",
|
|
"event: message_stop\n",
|
|
"data: {\"type\":\"message_stop\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("utf8 should decode");
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"role\":\"assistant\""));
|
|
assert!(output_text.contains("\"tool_calls\":[{"));
|
|
assert!(output_text.contains("\"id\":\"tool_123\""));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(output_text.contains("\\\"location\\\":\\\"Tokyo\\\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"tool_calls\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_text() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let first = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello \"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let first_text = utf8(first);
|
|
assert!(first_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(first_text.contains("\"role\":\"assistant\""));
|
|
assert!(first_text.contains("\"content\":\"Hello \""));
|
|
assert!(!first_text.contains("data: [DONE]"));
|
|
let second = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Gemini\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"content\":\"Gemini\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_to_openai_chat_stream_rewriter_buffers_and_converts_function_call() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"role\":\"assistant\""));
|
|
assert!(output_text.contains("\"content\":\"Need a tool.\""));
|
|
assert!(output_text.contains("\"tool_calls\":[{"));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(output_text.contains("\\\"city\\\":\\\"SF\\\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"tool_calls\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_to_openai_chat_stream_rewriter_converts_text_deltas_immediately() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:responses",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let created = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.created\n",
|
|
"data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_cli_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"in_progress\"}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let created_text = String::from_utf8(created).expect("utf8 should decode");
|
|
assert!(created_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(created_text.contains("\"role\":\"assistant\""));
|
|
assert!(!created_text.contains("data: [DONE]"));
|
|
|
|
let delta = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.output_text.delta\n",
|
|
"data: {\"type\":\"response.output_text.delta\",\"delta\":\"Hello Codex\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let delta_text = String::from_utf8(delta).expect("utf8 should decode");
|
|
assert!(delta_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(delta_text.contains("\"content\":\"Hello Codex\""));
|
|
assert!(!delta_text.contains("data: [DONE]"));
|
|
|
|
let completed = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_cli_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_cli_stream_123\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello Codex\",\"annotations\":[]}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let completed_text = String::from_utf8(completed).expect("utf8 should decode");
|
|
assert!(completed_text.contains("\"finish_reason\":\"stop\""));
|
|
assert!(
|
|
completed_text.contains("\"choices\":[]"),
|
|
"{completed_text}"
|
|
);
|
|
assert!(
|
|
completed_text.contains("\"prompt_tokens\":1"),
|
|
"{completed_text}"
|
|
);
|
|
assert!(
|
|
completed_text.contains("\"completion_tokens\":2"),
|
|
"{completed_text}"
|
|
);
|
|
assert!(
|
|
completed_text.contains("\"total_tokens\":3"),
|
|
"{completed_text}"
|
|
);
|
|
assert!(completed_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_to_openai_chat_stream_rewriter_converts_reasoning_deltas_immediately() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:responses",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.reasoning_summary_text.delta\n",
|
|
"data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp_reasoning_stream_123\",\"item_id\":\"rs_123\",\"output_index\":0,\"summary_index\":0,\"delta\":\"Need to inspect first.\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("utf8 should decode");
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"reasoning_content\":\"Need to inspect first.\""));
|
|
assert!(!output_text.contains("\"content\""));
|
|
assert!(!output_text.contains("data: [DONE]"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_responses_to_openai_chat_stream_rewriter_converts_completed_event_without_buffering() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:responses",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: response.completed\n",
|
|
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_cli_stream_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"id\":\"msg_cli_stream_123\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello Codex\",\"annotations\":[]}]}],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = String::from_utf8(output).expect("utf8 should decode");
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"role\":\"assistant\""));
|
|
assert!(output_text.contains("\"content\":\"Hello Codex\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"stop\""));
|
|
assert!(output_text.contains("\"choices\":[]"), "{output_text}");
|
|
assert!(output_text.contains("\"prompt_tokens\":1"), "{output_text}");
|
|
assert!(
|
|
output_text.contains("\"completion_tokens\":2"),
|
|
"{output_text}"
|
|
);
|
|
assert!(output_text.contains("\"total_tokens\":3"), "{output_text}");
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn antigravity_gemini_to_openai_chat_stream_rewriter_unwraps_and_converts_function_call() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:chat",
|
|
"needs_conversion": true,
|
|
"has_envelope": true,
|
|
"envelope_name": "antigravity:v1internal",
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"response\":{\"responseId\":\"resp_antigravity_chat_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\"},\"responseId\":\"resp_antigravity_chat_tool_123\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("\"object\":\"chat.completion.chunk\""));
|
|
assert!(output_text.contains("\"content\":\"Need a tool.\""));
|
|
assert!(output_text.contains("\"tool_calls\""));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(output_text.contains("\"finish_reason\":\"tool_calls\""));
|
|
assert!(output_text.contains("data: [DONE]"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn antigravity_gemini_to_openai_responses_stream_rewriter_unwraps_and_converts_function_call() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"envelope_name": "antigravity:v1internal",
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"response\":{\"responseId\":\"resp_antigravity_cli_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"city\":\"SF\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}},\"responseId\":\"resp_antigravity_cli_tool_123\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_text.delta"));
|
|
assert!(output_text.contains("event: response.output_item.added"));
|
|
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"function_call\""));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let first = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello \"}],\"role\":\"model\"},\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\"}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let first_text = utf8(first);
|
|
assert!(first_text.contains("event: response.created"));
|
|
assert!(first_text.contains("event: response.output_text.delta"));
|
|
assert!(first_text.contains("\"delta\":\"Hello \""));
|
|
let second = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Gemini CLI\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.contains("event: response.output_text.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"response.completed\""));
|
|
assert!(output_text.contains("\"object\":\"response\""));
|
|
assert!(output_text.contains("\"delta\":\"Gemini CLI\""));
|
|
assert!(output_text.contains("\"text\":\"Hello Gemini CLI\""));
|
|
assert!(output_text.contains("\"total_tokens\":5"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn claude_to_openai_responses_stream_rewriter_buffers_and_converts_to_completed_event() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: message_start\n",
|
|
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"model\":\"claude-sonnet-4-5\"}}\n\n",
|
|
"event: content_block_start\n",
|
|
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n",
|
|
"event: content_block_delta\n",
|
|
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello Claude CLI\"}}\n\n",
|
|
"event: message_delta\n",
|
|
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":2,\"output_tokens\":3}}\n\n",
|
|
"event: message_stop\n",
|
|
"data: {\"type\":\"message_stop\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_text.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"response.completed\""));
|
|
assert!(output_text.contains("\"object\":\"response\""));
|
|
assert!(output_text.contains("\"text\":\"Hello Claude CLI\""));
|
|
assert!(output_text.contains("\"total_tokens\":5"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn claude_to_openai_responses_stream_rewriter_converts_tool_use_to_function_call() {
|
|
let report_context = json!({
|
|
"provider_api_format": "claude:messages",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "claude-sonnet-4-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"event: message_start\n",
|
|
"data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_tool_123\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null}}\n\n",
|
|
"event: content_block_start\n",
|
|
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Running tool.\"}}\n\n",
|
|
"event: content_block_stop\n",
|
|
"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n",
|
|
"event: content_block_start\n",
|
|
"data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"tool_use\",\"id\":\"tool_123\",\"name\":\"read_file\",\"input\":{\"path\":\"/tmp/test.txt\"}}}\n\n",
|
|
"event: content_block_stop\n",
|
|
"data: {\"type\":\"content_block_stop\",\"index\":1}\n\n",
|
|
"event: message_delta\n",
|
|
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":4,\"output_tokens\":6}}\n\n",
|
|
"event: message_stop\n",
|
|
"data: {\"type\":\"message_stop\"}\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_text.delta"));
|
|
assert!(output_text.contains("\"delta\":\"Running tool.\""));
|
|
assert!(output_text.contains("event: response.output_item.added"));
|
|
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"response.completed\""));
|
|
assert!(output_text.contains("\"type\":\"function_call\""));
|
|
assert!(output_text.contains("\"call_id\":\"tool_123\""));
|
|
assert!(output_text.contains("\"name\":\"read_file\""));
|
|
assert!(output_text.contains("\\\"path\\\":\\\"/tmp/test.txt\\\""));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_to_openai_responses_stream_rewriter_converts_function_call_to_completed_event() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:responses",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_tool_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"location\":\"Tokyo\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_item.added"));
|
|
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"response.completed\""));
|
|
assert!(output_text.contains("\"type\":\"function_call\""));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(output_text.contains("\\\"location\\\":\\\"Tokyo\\\""));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn gemini_to_openai_responses_compact_stream_rewriter_converts_function_call_to_completed_event() {
|
|
let report_context = json!({
|
|
"provider_api_format": "gemini:generate_content",
|
|
"client_api_format": "openai:responses:compact",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gemini-2.5-pro",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let output = rewriter
|
|
.push_chunk(
|
|
b"data: {\"responseId\":\"resp_tool_compact_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Need a tool.\"},{\"functionCall\":{\"name\":\"get_weather\",\"args\":{\"location\":\"Tokyo\"}}}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(output);
|
|
assert!(output_text.contains("event: response.created"));
|
|
assert!(output_text.contains("event: response.output_item.added"));
|
|
assert!(output_text.contains("event: response.function_call_arguments.delta"));
|
|
assert!(output_text.contains("event: response.completed"));
|
|
assert!(output_text.contains("\"type\":\"response.completed\""));
|
|
assert!(output_text.contains("\"type\":\"function_call\""));
|
|
assert!(output_text.contains("\"name\":\"get_weather\""));
|
|
assert!(output_text.contains("\\\"location\\\":\\\"Tokyo\\\""));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_to_claude_chat_stream_rewriter_converts_via_standard_matrix() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "claude:messages",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let first = rewriter
|
|
.push_chunk(
|
|
"data: {\"id\":\"chatcmpl_std_claude_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Claude\"},\"finish_reason\":null}]}\n\n"
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let first_text = utf8(first);
|
|
assert!(first_text.contains("event: message_start"));
|
|
assert!(first_text.contains("event: content_block_start"));
|
|
assert!(first_text.contains("event: content_block_delta"));
|
|
assert!(first_text.contains("\"text\":\"Hello Claude\""));
|
|
assert!(first_text.contains("\"usage\":{\"input_tokens\":0,\"output_tokens\":0}"));
|
|
|
|
let second = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"data: {\"id\":\"chatcmpl_std_claude_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n",
|
|
"data: [DONE]\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.contains("event: content_block_stop"));
|
|
assert!(output_text.contains("event: message_delta"));
|
|
assert!(output_text.contains("event: message_stop"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_to_claude_chat_stream_rewriter_injects_default_usage_when_finish_chunk_lacks_usage()
|
|
{
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "claude:messages",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5.4",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let first = rewriter
|
|
.push_chunk(
|
|
"data: {\"id\":\"chatcmpl_usage_missing_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5.4\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello\"},\"finish_reason\":null}]}\n\n"
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let first_text = utf8(first);
|
|
assert!(first_text.contains("event: message_start"));
|
|
|
|
let second = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"data: {\"id\":\"chatcmpl_usage_missing_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5.4\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n",
|
|
"data: [DONE]\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.is_empty(), "{output_text}");
|
|
let final_text = utf8(rewriter.finish().expect("finish should succeed"));
|
|
assert!(final_text.contains("event: message_delta"));
|
|
assert!(final_text.contains("\"stop_reason\":\"end_turn\""));
|
|
assert!(final_text.contains("\"usage\":{\"input_tokens\":0,\"output_tokens\":0}"));
|
|
assert!(final_text.contains("event: message_stop"));
|
|
}
|
|
|
|
#[test]
|
|
fn openai_chat_to_gemini_cli_stream_rewriter_converts_via_standard_matrix() {
|
|
let report_context = json!({
|
|
"provider_api_format": "openai:chat",
|
|
"client_api_format": "gemini:generate_content",
|
|
"needs_conversion": true,
|
|
"mapped_model": "gpt-5",
|
|
});
|
|
let mut rewriter =
|
|
maybe_build_local_stream_rewriter(Some(&report_context)).expect("rewriter should exist");
|
|
let first = rewriter
|
|
.push_chunk(
|
|
"data: {\"id\":\"chatcmpl_std_gemini_cli_123\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"Hello Gemini CLI\"},\"finish_reason\":null}]}\n\n"
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let first_text = utf8(first);
|
|
assert!(first_text.contains("\"responseId\":\"chatcmpl_std_gemini_cli_123\""));
|
|
assert!(first_text.contains("\"candidates\""));
|
|
assert!(first_text.contains("\"text\":\"Hello Gemini CLI\""));
|
|
|
|
let second = rewriter
|
|
.push_chunk(
|
|
concat!(
|
|
"data: {\"id\":\"chatcmpl_std_gemini_cli_123\",\"object\":\"chat.completion.chunk\",\"model\":\"gpt-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":2,\"completion_tokens\":3,\"total_tokens\":5}}\n\n",
|
|
"data: [DONE]\n\n"
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.expect("rewrite should succeed");
|
|
let output_text = utf8(second);
|
|
assert!(output_text.contains("\"finishReason\":\"STOP\""));
|
|
assert!(output_text.contains("\"totalTokenCount\":5"));
|
|
assert!(rewriter.finish().expect("finish should succeed").is_empty());
|
|
}
|