mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
fix(formats): preserve chat-backed Responses metadata
This commit is contained in:
@@ -678,11 +678,30 @@ impl OpenAIResponsesProviderState {
|
||||
if item.get("type").and_then(Value::as_str) != Some("function_call") {
|
||||
return;
|
||||
}
|
||||
const MAPPED_FIELDS: &[&str] = &["type", "id", "call_id", "status", "name", "arguments"];
|
||||
if item
|
||||
.keys()
|
||||
.any(|field| !MAPPED_FIELDS.contains(&field.as_str()))
|
||||
{
|
||||
// Chat-backed Responses implementations may attach transport-only metadata to
|
||||
// function-call items. It does not change the executable tool call, and the
|
||||
// sync Responses aggregator retains the original item verbatim. Treat these
|
||||
// sidecars as recognized while continuing to fail closed for semantic fields
|
||||
// (for example `caller`) that the canonical tool-call events cannot represent.
|
||||
const EXECUTION_FIELDS: &[&str] = &["type", "id", "call_id", "status", "name", "arguments"];
|
||||
let has_chat_metadata_passthrough =
|
||||
item.contains_key("internal_chat_message_metadata_passthrough");
|
||||
let chat_metadata_target_supported = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.map(aether_ai_formats::normalize_api_format_alias)
|
||||
.is_none_or(|client_api_format| {
|
||||
matches!(
|
||||
client_api_format.as_str(),
|
||||
"openai:chat" | "openai:responses" | "openai:responses:compact"
|
||||
)
|
||||
});
|
||||
if item.keys().any(|field| {
|
||||
!EXECUTION_FIELDS.contains(&field.as_str())
|
||||
&& !(chat_metadata_target_supported
|
||||
&& (field == "internal_chat_message_metadata_passthrough"
|
||||
|| (field == "metadata" && has_chat_metadata_passthrough)))
|
||||
}) {
|
||||
out.push(self.unknown_frame(report_context, Value::Object(item.clone())));
|
||||
return;
|
||||
}
|
||||
@@ -3718,6 +3737,119 @@ mod tests {
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_provider_state_accepts_chat_backed_function_call_metadata() {
|
||||
let mut state = OpenAIResponsesProviderState::default();
|
||||
let report_context = json!({});
|
||||
let frames = state
|
||||
.push_line(
|
||||
&report_context,
|
||||
data_line(json!({
|
||||
"type": "response.output_item.added",
|
||||
"response_id": "resp_chat_metadata_123",
|
||||
"output_index": 0,
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"id": "fc_chat_metadata_123",
|
||||
"call_id": "call_chat_metadata_123",
|
||||
"status": "completed",
|
||||
"name": "lookup",
|
||||
"arguments": "{\"query\":\"aether\"}",
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn_123"
|
||||
},
|
||||
"metadata": {
|
||||
"source": "chat"
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.expect("chat-backed function call event should parse");
|
||||
|
||||
assert!(!frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.event, CanonicalStreamEvent::UnknownEvent(_))));
|
||||
assert!(frames.iter().any(|frame| matches!(
|
||||
frame.event,
|
||||
CanonicalStreamEvent::ToolCallStart {
|
||||
index: 0,
|
||||
ref call_id,
|
||||
ref name,
|
||||
} if call_id == "call_chat_metadata_123" && name == "lookup"
|
||||
)));
|
||||
assert!(frames.iter().any(|frame| matches!(
|
||||
frame.event,
|
||||
CanonicalStreamEvent::ToolCallArgumentsDelta {
|
||||
index: 0,
|
||||
ref arguments,
|
||||
} if arguments == "{\"query\":\"aether\"}"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_provider_state_rejects_unpaired_function_call_metadata() {
|
||||
let mut state = OpenAIResponsesProviderState::default();
|
||||
let frames = state
|
||||
.push_line(
|
||||
&json!({}),
|
||||
data_line(json!({
|
||||
"type": "response.output_item.added",
|
||||
"output_index": 0,
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"id": "fc_metadata_only_123",
|
||||
"call_id": "call_metadata_only_123",
|
||||
"status": "completed",
|
||||
"name": "lookup",
|
||||
"arguments": "{}",
|
||||
"metadata": {"semantic": true}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.expect("function call event should parse");
|
||||
|
||||
assert!(frames.iter().any(|frame| matches!(
|
||||
frame.event,
|
||||
CanonicalStreamEvent::UnknownEvent(ref payload)
|
||||
if payload.get("metadata").is_some()
|
||||
)));
|
||||
assert!(!frames.iter().any(|frame| matches!(
|
||||
frame.event,
|
||||
CanonicalStreamEvent::ToolCallStart { .. }
|
||||
| CanonicalStreamEvent::ToolCallArgumentsDelta { .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_provider_state_rejects_chat_metadata_for_non_openai_clients() {
|
||||
let mut state = OpenAIResponsesProviderState::default();
|
||||
let frames = state
|
||||
.push_line(
|
||||
&json!({"client_api_format": "claude:messages"}),
|
||||
data_line(json!({
|
||||
"type": "response.output_item.added",
|
||||
"output_index": 0,
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"id": "fc_chat_metadata_123",
|
||||
"call_id": "call_chat_metadata_123",
|
||||
"status": "completed",
|
||||
"name": "lookup",
|
||||
"arguments": "{}",
|
||||
"metadata": {"source": "chat"},
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn_123"
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.expect("function call event should parse");
|
||||
|
||||
assert!(frames
|
||||
.iter()
|
||||
.any(|frame| matches!(frame.event, CanonicalStreamEvent::UnknownEvent(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_provider_state_treats_failed_event_as_terminal() {
|
||||
let mut state = OpenAIResponsesProviderState::default();
|
||||
|
||||
@@ -680,6 +680,12 @@ fn validate_openai_responses_cross_format_response_extensions(
|
||||
}
|
||||
CanonicalContentBlock::Unknown { .. } => continue,
|
||||
};
|
||||
let has_chat_metadata_passthrough = extensions.iter().any(|(namespace, value)| {
|
||||
matches!(namespace.as_str(), "openai_responses" | "openai_cli")
|
||||
&& value.as_object().is_some_and(|object| {
|
||||
object.contains_key("internal_chat_message_metadata_passthrough")
|
||||
})
|
||||
});
|
||||
validate_openai_responses_response_extension_namespace(
|
||||
source,
|
||||
target,
|
||||
@@ -693,7 +699,13 @@ fn validate_openai_responses_cross_format_response_extensions(
|
||||
}
|
||||
CanonicalContentBlock::Thinking { .. } => key == "item_type",
|
||||
CanonicalContentBlock::ToolUse { .. } => {
|
||||
// Codex may attach a private Responses-to-Chat transport sidecar.
|
||||
// Only a Chat target may intentionally consume the tool call while
|
||||
// discarding that paired transport metadata.
|
||||
matches!(key, "item_id" | "item_type" | "status")
|
||||
|| (target == FormatId::OpenAiChat
|
||||
&& (key == "internal_chat_message_metadata_passthrough"
|
||||
|| (key == "metadata" && has_chat_metadata_passthrough)))
|
||||
}
|
||||
CanonicalContentBlock::ToolResult { .. } => key == "item_type",
|
||||
CanonicalContentBlock::Image { .. }
|
||||
@@ -5141,6 +5153,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_backed_function_call_metadata_is_only_droppable_for_chat_targets() {
|
||||
let body = json!({
|
||||
"id": "resp_chat_metadata",
|
||||
"object": "response",
|
||||
"model": "gpt-5",
|
||||
"status": "completed",
|
||||
"output": [{
|
||||
"type": "function_call",
|
||||
"id": "fc_chat_metadata",
|
||||
"call_id": "call_chat_metadata",
|
||||
"status": "completed",
|
||||
"name": "lookup",
|
||||
"arguments": "{}",
|
||||
"metadata": {"source": "chat"},
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn_123"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
let chat = convert_response_pure("openai:responses", "openai:chat", &body)
|
||||
.expect("Chat-backed metadata should be droppable for a Chat client")
|
||||
.value;
|
||||
assert_eq!(
|
||||
chat["choices"][0]["message"]["tool_calls"][0]["id"],
|
||||
"call_chat_metadata"
|
||||
);
|
||||
|
||||
for target in ["claude:messages", "gemini:generate_content"] {
|
||||
let error = convert_response_pure("openai:responses", target, &body)
|
||||
.expect_err("non-Chat targets must reject Chat-private metadata");
|
||||
assert!(
|
||||
matches!(
|
||||
error,
|
||||
super::FormatError::LossyConversionBlocked { ref field, .. }
|
||||
if field.ends_with(".metadata")
|
||||
|| field.contains("internal_chat_message_metadata_passthrough")
|
||||
),
|
||||
"{target}: {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut unpaired = body;
|
||||
unpaired["output"][0]
|
||||
.as_object_mut()
|
||||
.expect("function call should be an object")
|
||||
.remove("internal_chat_message_metadata_passthrough");
|
||||
let error = convert_response_pure("openai:responses", "openai:chat", &unpaired)
|
||||
.expect_err("generic metadata without the Chat-private marker must be rejected");
|
||||
assert!(matches!(
|
||||
error,
|
||||
super::FormatError::LossyConversionBlocked { ref field, .. }
|
||||
if field.ends_with(".metadata")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_openai_responses_response_cross_format_blocks_raw_output_items() {
|
||||
let body = json!({
|
||||
|
||||
@@ -784,6 +784,58 @@ mod tests {
|
||||
assert!(!sse.contains("HelloHello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_chat_backed_function_call_metadata_to_chat_tool_calls() {
|
||||
let report_context = report_context("openai:responses", "openai:chat");
|
||||
let mut matrix = StreamingStandardFormatMatrix::default();
|
||||
let mut output = Vec::new();
|
||||
|
||||
for line in [
|
||||
data_line(json!({
|
||||
"type": "response.output_item.added",
|
||||
"response_id": "resp_chat_metadata_123",
|
||||
"output_index": 0,
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"id": "fc_chat_metadata_123",
|
||||
"call_id": "call_chat_metadata_123",
|
||||
"status": "completed",
|
||||
"arguments": "{\"query\":\"aether\"}",
|
||||
"name": "lookup",
|
||||
"metadata": {"source": "chat"},
|
||||
"internal_chat_message_metadata_passthrough": {
|
||||
"turn_id": "turn_123"
|
||||
}
|
||||
}
|
||||
})),
|
||||
data_line(json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_chat_metadata_123",
|
||||
"object": "response",
|
||||
"model": "gpt-5",
|
||||
"status": "completed",
|
||||
"output": [],
|
||||
},
|
||||
})),
|
||||
] {
|
||||
output.extend(
|
||||
matrix
|
||||
.transform_line(&report_context, line)
|
||||
.expect("chat-backed function call should convert"),
|
||||
);
|
||||
}
|
||||
|
||||
let sse = String::from_utf8(output).expect("sse should be utf8");
|
||||
assert!(!sse.contains("unsupported_stream_event"), "{sse}");
|
||||
assert!(!sse.contains("Unsupported provider stream event"), "{sse}");
|
||||
assert!(sse.contains("\"id\":\"call_chat_metadata_123\""), "{sse}");
|
||||
assert!(sse.contains("\"name\":\"lookup\""), "{sse}");
|
||||
assert!(sse.contains("\\\"query\\\":\\\"aether\\\""), "{sse}");
|
||||
assert!(!sse.contains("internal_chat_message_metadata_passthrough"));
|
||||
assert!(!sse.contains("\"metadata\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_openai_responses_keepalive_events_for_chat_clients() {
|
||||
let report_context = report_context("openai:responses", "openai:chat");
|
||||
|
||||
@@ -49,12 +49,25 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let provider_stream_event_api_format = report_context
|
||||
.get("provider_stream_event_api_format")
|
||||
.or_else(|| report_context.get("provider_stream_api_format"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.unwrap_or_else(|| provider_api_format.clone());
|
||||
let client_api_format = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
let stream_needs_conversion = needs_conversion
|
||||
|| !is_same_format_family(
|
||||
provider_stream_event_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
);
|
||||
|
||||
if !needs_conversion
|
||||
&& client_consumes_same_private_stream_envelope(
|
||||
@@ -87,25 +100,30 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
return Some(FinalizeStreamRewriteMode::OpenAiImage);
|
||||
}
|
||||
|
||||
if needs_conversion {
|
||||
if stream_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()) {
|
||||
if is_openai_responses_family(provider_api_format.as_str())
|
||||
if is_same_format_family(
|
||||
provider_stream_event_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
) {
|
||||
if is_openai_responses_family(provider_stream_event_api_format.as_str())
|
||||
&& is_openai_responses_family(client_api_format.as_str())
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::OpenAiResponsesCompat);
|
||||
}
|
||||
if provider_api_format == "claude:messages" && client_api_format == "claude:messages" {
|
||||
if provider_stream_event_api_format == "claude:messages"
|
||||
&& client_api_format == "claude:messages"
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
}
|
||||
return model_directive_display_model_from_report_context(report_context)
|
||||
.map(|_| FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||
}
|
||||
return supports_standard_stream_rewrite(
|
||||
provider_api_format.as_str(),
|
||||
provider_stream_event_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
)
|
||||
.then_some(FinalizeStreamRewriteMode::Standard);
|
||||
@@ -118,25 +136,30 @@ pub fn resolve_finalize_stream_rewrite_mode(
|
||||
}
|
||||
|
||||
if model_directive_display_model_from_report_context(report_context).is_some()
|
||||
&& provider_api_format == client_api_format
|
||||
&& is_standard_provider_api_format(provider_api_format.as_str())
|
||||
&& provider_stream_event_api_format == client_api_format
|
||||
&& is_standard_provider_api_format(provider_stream_event_api_format.as_str())
|
||||
&& !provider_adaptation_should_unwrap_stream_envelope(
|
||||
envelope_name.as_str(),
|
||||
provider_api_format.as_str(),
|
||||
)
|
||||
{
|
||||
if provider_api_format == "claude:messages" {
|
||||
if provider_stream_event_api_format == "claude:messages" {
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
}
|
||||
return Some(FinalizeStreamRewriteMode::ModelDirectiveDisplay);
|
||||
}
|
||||
|
||||
if provider_api_format == "claude:messages" && client_api_format == "claude:messages" {
|
||||
if provider_stream_event_api_format == "claude:messages"
|
||||
&& client_api_format == "claude:messages"
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::ClaudeReadToolSanitize);
|
||||
}
|
||||
|
||||
if provider_api_format == client_api_format
|
||||
&& is_openai_responses_family(provider_api_format.as_str())
|
||||
if is_same_format_family(
|
||||
provider_stream_event_api_format.as_str(),
|
||||
client_api_format.as_str(),
|
||||
) && is_openai_responses_family(provider_stream_event_api_format.as_str())
|
||||
&& is_openai_responses_family(client_api_format.as_str())
|
||||
{
|
||||
return Some(FinalizeStreamRewriteMode::OpenAiResponsesCompat);
|
||||
}
|
||||
@@ -1011,6 +1034,61 @@ data: {\"type\":\"response.reasoning_summary_text.delta\",\"response_id\":\"resp
|
||||
assert!(!output.contains("data: [DONE]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_responses_stream_format_preserves_function_call_metadata() {
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:chat",
|
||||
"provider_stream_event_api_format": "openai:responses",
|
||||
"client_api_format": "openai:responses",
|
||||
"needs_conversion": true,
|
||||
});
|
||||
assert_eq!(
|
||||
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||
Some(FinalizeStreamRewriteMode::OpenAiResponsesCompat)
|
||||
);
|
||||
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("responses compat rewriter should exist");
|
||||
let output = rewriter
|
||||
.push_chunk(
|
||||
b"event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"function_call\",\"id\":\"fc_chat_metadata_123\",\"call_id\":\"call_chat_metadata_123\",\"status\":\"completed\",\"arguments\":\"{}\",\"name\":\"lookup\",\"metadata\":{\"source\":\"chat\"},\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}}}\n\n",
|
||||
)
|
||||
.expect("responses event should pass through");
|
||||
let output = String::from_utf8(output).expect("output should be utf8");
|
||||
|
||||
assert!(output.contains("event: response.output_item.added"));
|
||||
assert!(output.contains("\"metadata\":{\"source\":\"chat\"}"));
|
||||
assert!(output
|
||||
.contains("\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_responses_stream_format_converts_to_chat_even_without_request_conversion() {
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:chat",
|
||||
"provider_stream_event_api_format": "openai:responses",
|
||||
"client_api_format": "openai:chat",
|
||||
"needs_conversion": false,
|
||||
});
|
||||
assert_eq!(
|
||||
resolve_finalize_stream_rewrite_mode(&report_context),
|
||||
Some(FinalizeStreamRewriteMode::Standard)
|
||||
);
|
||||
|
||||
let mut rewriter = maybe_build_ai_surface_stream_rewriter(Some(&report_context))
|
||||
.expect("standard rewriter should exist");
|
||||
let output = rewriter
|
||||
.push_chunk(
|
||||
b"event: response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"function_call\",\"id\":\"fc_chat_metadata_123\",\"call_id\":\"call_chat_metadata_123\",\"status\":\"completed\",\"arguments\":\"{}\",\"name\":\"lookup\",\"metadata\":{\"source\":\"chat\"},\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}}}\n\n",
|
||||
)
|
||||
.expect("responses event should convert to chat");
|
||||
let output = String::from_utf8(output).expect("output should be utf8");
|
||||
|
||||
assert!(output.contains("\"object\":\"chat.completion.chunk\""));
|
||||
assert!(output.contains("\"id\":\"call_chat_metadata_123\""));
|
||||
assert!(!output.contains("unsupported_stream_event"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_family_responses_passthrough_preserves_encrypted_content() {
|
||||
// When provider and client are both OpenAI Responses family,
|
||||
|
||||
@@ -684,13 +684,17 @@ fn maybe_build_openai_responses_same_family_stream_sync_body(
|
||||
.get("needs_conversion")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let provider_stream_event_api_format =
|
||||
provider_stream_event_api_format_for_report_context(report_context, &provider_api_format);
|
||||
let normalized_provider_api_format =
|
||||
normalize_openai_responses_family_api_format(&provider_api_format);
|
||||
normalize_openai_responses_family_api_format(&provider_stream_event_api_format);
|
||||
let normalized_client_api_format =
|
||||
normalize_openai_responses_family_api_format(&client_api_format);
|
||||
if !is_openai_responses_family_api_format(&provider_api_format)
|
||||
if !is_openai_responses_family_api_format(&provider_stream_event_api_format)
|
||||
|| !is_openai_responses_family_api_format(&client_api_format)
|
||||
|| (normalized_provider_api_format == normalized_client_api_format && needs_conversion)
|
||||
|| (is_openai_responses_family_api_format(&provider_api_format)
|
||||
&& normalized_provider_api_format == normalized_client_api_format
|
||||
&& needs_conversion)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -3628,7 +3632,7 @@ mod tests {
|
||||
maybe_build_standard_cross_format_sync_product_from_normalized_payload,
|
||||
maybe_build_standard_same_format_sync_body_from_normalized_payload,
|
||||
maybe_build_standard_sync_finalize_product_from_normalized_payload,
|
||||
StandardSyncFinalizeNormalizedProduct,
|
||||
try_aggregate_openai_responses_stream_sync_response, StandardSyncFinalizeNormalizedProduct,
|
||||
};
|
||||
use aether_ai_formats::formats::conversion::response::{
|
||||
convert_claude_chat_response_to_openai_chat, convert_gemini_chat_response_to_openai_chat,
|
||||
@@ -4870,6 +4874,28 @@ mod tests {
|
||||
assert_eq!(result["output"][0]["arguments"], r#"{"location": "Tokyo"}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_and_preserves_chat_backed_function_call_metadata() {
|
||||
let body = concat!(
|
||||
"event: response.output_item.added\n",
|
||||
"data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"function_call\",\"id\":\"fc_chat_metadata_123\",\"call_id\":\"call_chat_metadata_123\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"aether\\\"}\",\"name\":\"lookup\",\"metadata\":{\"source\":\"chat\"},\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}}}\n\n",
|
||||
"event: response.completed\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_chat_metadata_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
|
||||
);
|
||||
|
||||
let result = try_aggregate_openai_responses_stream_sync_response(body.as_bytes())
|
||||
.expect("chat-backed function call metadata should pass stream validation")
|
||||
.expect("chat-backed function call stream should aggregate");
|
||||
|
||||
assert_eq!(result["output"][0]["type"], "function_call");
|
||||
assert_eq!(result["output"][0]["call_id"], "call_chat_metadata_123");
|
||||
assert_eq!(result["output"][0]["metadata"]["source"], "chat");
|
||||
assert_eq!(
|
||||
result["output"][0]["internal_chat_message_metadata_passthrough"]["turn_id"],
|
||||
"turn_123"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_tool_call_input_events_materialize_custom_tool_call() {
|
||||
let body = concat!(
|
||||
@@ -5863,6 +5889,80 @@ mod tests {
|
||||
assert_eq!(body_json["choices"][0]["message"]["content"], "pong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sync_finalize_converts_chat_backed_function_call_metadata() {
|
||||
let body = concat!(
|
||||
"event: response.output_item.added\n",
|
||||
"data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"function_call\",\"id\":\"fc_chat_metadata_123\",\"call_id\":\"call_chat_metadata_123\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"aether\\\"}\",\"name\":\"lookup\",\"metadata\":{\"source\":\"chat\"},\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}}}\n\n",
|
||||
"event: response.completed\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_chat_metadata_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
|
||||
);
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:responses",
|
||||
"client_api_format": "openai:chat",
|
||||
"needs_conversion": true,
|
||||
});
|
||||
|
||||
let product = maybe_build_standard_sync_finalize_product_from_normalized_payload(
|
||||
"openai_chat_sync_finalize",
|
||||
200,
|
||||
Some(&report_context),
|
||||
None,
|
||||
Some(&base64::engine::general_purpose::STANDARD.encode(body)),
|
||||
)
|
||||
.expect("chat-backed function call metadata should convert")
|
||||
.expect("dispatch should produce a body");
|
||||
|
||||
let StandardSyncFinalizeNormalizedProduct::CrossFormat(product) = product else {
|
||||
panic!("Chat-to-Responses conversion should produce a cross-format body");
|
||||
};
|
||||
let body_json = product.client_body_json;
|
||||
let tool_call = &body_json["choices"][0]["message"]["tool_calls"][0];
|
||||
assert_eq!(tool_call["id"], "call_chat_metadata_123");
|
||||
assert_eq!(tool_call["function"]["name"], "lookup");
|
||||
assert_eq!(tool_call["function"]["arguments"], "{\"query\":\"aether\"}");
|
||||
assert!(tool_call.get("metadata").is_none());
|
||||
assert!(tool_call
|
||||
.get("internal_chat_message_metadata_passthrough")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_sync_finalize_preserves_responses_wire_metadata_for_responses_client() {
|
||||
let body = concat!(
|
||||
"event: response.output_item.added\n",
|
||||
"data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"function_call\",\"id\":\"fc_chat_metadata_123\",\"call_id\":\"call_chat_metadata_123\",\"status\":\"completed\",\"arguments\":\"{\\\"query\\\":\\\"aether\\\"}\",\"name\":\"lookup\",\"metadata\":{\"source\":\"chat\"},\"internal_chat_message_metadata_passthrough\":{\"turn_id\":\"turn_123\"}}}\n\n",
|
||||
"event: response.completed\n",
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_chat_metadata_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n",
|
||||
);
|
||||
let report_context = json!({
|
||||
"provider_api_format": "openai:chat",
|
||||
"provider_stream_event_api_format": "openai:responses",
|
||||
"client_api_format": "openai:responses",
|
||||
"needs_conversion": true,
|
||||
});
|
||||
|
||||
let product = maybe_build_standard_sync_finalize_product_from_normalized_payload(
|
||||
"openai_responses_sync_finalize",
|
||||
200,
|
||||
Some(&report_context),
|
||||
None,
|
||||
Some(&base64::engine::general_purpose::STANDARD.encode(body)),
|
||||
)
|
||||
.expect("Responses wire metadata should aggregate")
|
||||
.expect("dispatch should produce a body");
|
||||
|
||||
let StandardSyncFinalizeNormalizedProduct::SuccessBody(body_json) = product else {
|
||||
panic!("matching Responses wire format should produce a success body");
|
||||
};
|
||||
assert_eq!(body_json["output"][0]["type"], "function_call");
|
||||
assert_eq!(body_json["output"][0]["metadata"]["source"], "chat");
|
||||
assert_eq!(
|
||||
body_json["output"][0]["internal_chat_message_metadata_passthrough"]["turn_id"],
|
||||
"turn_123"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_same_format_gemini_finalize_rejects_usage_only_body() {
|
||||
let report_context = json!({
|
||||
|
||||
Reference in New Issue
Block a user