mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(ai): allow null OpenAI Responses error fields
This commit is contained in:
@@ -1250,6 +1250,63 @@ fn local_finalize_handles_openai_responses_openai_family_sync_response_even_when
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn local_finalize_converts_openai_responses_null_error_to_claude_cli() {
|
||||||
|
let payload = GatewaySyncReportRequest {
|
||||||
|
trace_id: "trace-openai-responses-to-claude-cli-success".to_string(),
|
||||||
|
report_kind: "claude_cli_sync_finalize".to_string(),
|
||||||
|
report_context: Some(json!({
|
||||||
|
"client_api_format": "claude:messages",
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"mapped_model": "gpt-5",
|
||||||
|
"needs_conversion": true,
|
||||||
|
"has_envelope": false,
|
||||||
|
})),
|
||||||
|
status_code: 200,
|
||||||
|
headers: BTreeMap::from([("content-type".to_string(), "application/json".to_string())]),
|
||||||
|
body_json: Some(json!({
|
||||||
|
"id": "resp_completed_cli_123",
|
||||||
|
"object": "response",
|
||||||
|
"model": "gpt-5",
|
||||||
|
"status": "completed",
|
||||||
|
"error": null,
|
||||||
|
"output": [{
|
||||||
|
"type": "message",
|
||||||
|
"id": "msg_completed_cli_123",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{
|
||||||
|
"type": "output_text",
|
||||||
|
"text": "Done",
|
||||||
|
"annotations": []
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
})),
|
||||||
|
client_body_json: None,
|
||||||
|
body_base64: None,
|
||||||
|
telemetry: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = maybe_build_local_core_sync_finalize_response(
|
||||||
|
"trace-openai-responses-to-claude-cli-success",
|
||||||
|
&test_decision(),
|
||||||
|
&payload,
|
||||||
|
)
|
||||||
|
.expect("local finalize should succeed")
|
||||||
|
.expect("local finalize should convert the response");
|
||||||
|
|
||||||
|
assert_eq!(outcome.response.status(), 200);
|
||||||
|
let response_body = to_bytes(outcome.response.into_body(), usize::MAX)
|
||||||
|
.await
|
||||||
|
.expect("response body should read");
|
||||||
|
let body: serde_json::Value =
|
||||||
|
serde_json::from_slice(&response_body).expect("response should be json");
|
||||||
|
assert_eq!(body["type"], "message");
|
||||||
|
assert_eq!(body["content"][0]["text"], "Done");
|
||||||
|
assert_eq!(body["stop_reason"], "end_turn");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_finalize_handles_openai_chat_stream_response_from_openai_chat() {
|
fn local_finalize_handles_openai_chat_stream_response_from_openai_chat() {
|
||||||
let body = concat!(
|
let body = concat!(
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ pub(crate) fn has_nested_error(value: &serde_json::Value) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if object.contains_key("error") {
|
if object.get("error").is_some_and(|error| !error.is_null()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if object
|
if object
|
||||||
@@ -500,7 +500,9 @@ pub(crate) fn has_nested_error(value: &serde_json::Value) -> bool {
|
|||||||
.is_some_and(|chunks| {
|
.is_some_and(|chunks| {
|
||||||
chunks.iter().any(|chunk| {
|
chunks.iter().any(|chunk| {
|
||||||
chunk.as_object().is_some_and(|chunk_object| {
|
chunk.as_object().is_some_and(|chunk_object| {
|
||||||
chunk_object.contains_key("error")
|
chunk_object
|
||||||
|
.get("error")
|
||||||
|
.is_some_and(|error| !error.is_null())
|
||||||
|| chunk_object
|
|| chunk_object
|
||||||
.get("type")
|
.get("type")
|
||||||
.and_then(|value| value.as_str())
|
.and_then(|value| value.as_str())
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use crate::ai_pipeline_api::{
|
|||||||
GatewayControlSyncDecisionResponse,
|
GatewayControlSyncDecisionResponse,
|
||||||
};
|
};
|
||||||
use crate::execution_runtime::submission::{
|
use crate::execution_runtime::submission::{
|
||||||
build_best_effort_local_core_error_body, resolve_core_error_background_report_kind,
|
build_best_effort_local_core_error_body, has_nested_error,
|
||||||
resolve_core_success_background_report_kind, resolve_local_core_error_response_body_json,
|
resolve_core_error_background_report_kind, resolve_core_success_background_report_kind,
|
||||||
|
resolve_local_core_error_response_body_json,
|
||||||
};
|
};
|
||||||
use crate::execution_runtime::{
|
use crate::execution_runtime::{
|
||||||
resolve_local_sync_error_background_report_kind,
|
resolve_local_sync_error_background_report_kind,
|
||||||
@@ -251,6 +252,23 @@ fn build_best_effort_local_core_error_body_converts_claude_cli_error_to_openai_r
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_nested_error_ignores_null_error_fields() {
|
||||||
|
assert!(!has_nested_error(&json!({
|
||||||
|
"id": "resp_completed_123",
|
||||||
|
"object": "response",
|
||||||
|
"status": "completed",
|
||||||
|
"error": null,
|
||||||
|
"output": []
|
||||||
|
})));
|
||||||
|
assert!(has_nested_error(&json!({
|
||||||
|
"id": "resp_failed_123",
|
||||||
|
"object": "response",
|
||||||
|
"status": "failed",
|
||||||
|
"error": {"message": "quota reached"}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_best_effort_local_core_error_body_converts_sync_errors_across_standard_families() {
|
fn build_best_effort_local_core_error_body_converts_sync_errors_across_standard_families() {
|
||||||
let cases = vec![
|
let cases = vec![
|
||||||
|
|||||||
@@ -4438,6 +4438,7 @@ mod tests {
|
|||||||
"id": "resp_123",
|
"id": "resp_123",
|
||||||
"object": "response",
|
"object": "response",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
|
"error": null,
|
||||||
"model": "gpt-5",
|
"model": "gpt-5",
|
||||||
"output": [
|
"output": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ pub fn to_compact(response: &CanonicalResponse, ctx: &FormatContext) -> Option<V
|
|||||||
|
|
||||||
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
|
pub fn from_raw(body_json: &Value) -> Option<CanonicalResponse> {
|
||||||
let body = body_json.as_object()?;
|
let body = body_json.as_object()?;
|
||||||
if body.contains_key("error") || body.get("status").and_then(Value::as_str) == Some("failed") {
|
if body.get("error").is_some_and(|error| !error.is_null())
|
||||||
|
|| body.get("status").and_then(Value::as_str) == Some("failed")
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let content = openai_responses_output_to_canonical_blocks(body.get("output"))?;
|
let content = openai_responses_output_to_canonical_blocks(body.get("output"))?;
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ fn is_error_like_sync_body(value: &Value) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
object.contains_key("error")
|
object.get("error").is_some_and(|error| !error.is_null())
|
||||||
|| object
|
|| object
|
||||||
.get("type")
|
.get("type")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -572,7 +572,9 @@ fn is_error_like_sync_body(value: &Value) -> bool {
|
|||||||
.is_some_and(|chunks| {
|
.is_some_and(|chunks| {
|
||||||
chunks.iter().any(|chunk| {
|
chunks.iter().any(|chunk| {
|
||||||
chunk.as_object().is_some_and(|chunk_object| {
|
chunk.as_object().is_some_and(|chunk_object| {
|
||||||
chunk_object.contains_key("error")
|
chunk_object
|
||||||
|
.get("error")
|
||||||
|
.is_some_and(|error| !error.is_null())
|
||||||
|| chunk_object
|
|| chunk_object
|
||||||
.get("type")
|
.get("type")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -2877,6 +2879,7 @@ mod tests {
|
|||||||
convert_openai_chat_response_to_openai_responses,
|
convert_openai_chat_response_to_openai_responses,
|
||||||
convert_openai_responses_response_to_openai_chat,
|
convert_openai_responses_response_to_openai_chat,
|
||||||
};
|
};
|
||||||
|
use crate::conversion::{sync_cli_response_conversion_kind, SyncCliResponseConversionKind};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -4234,6 +4237,58 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_sync_finalize_product_converts_openai_responses_null_error_to_claude_cli() {
|
||||||
|
let report_context = json!({
|
||||||
|
"provider_api_format": "openai:responses",
|
||||||
|
"client_api_format": "claude:messages",
|
||||||
|
"model": "claude-sonnet-4-5",
|
||||||
|
"mapped_model": "gpt-5",
|
||||||
|
});
|
||||||
|
let provider_body_json = json!({
|
||||||
|
"id": "resp_completed_cli_123",
|
||||||
|
"object": "response",
|
||||||
|
"model": "gpt-5",
|
||||||
|
"status": "completed",
|
||||||
|
"error": null,
|
||||||
|
"output": [{
|
||||||
|
"type": "message",
|
||||||
|
"id": "msg_completed_cli_123",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{"type": "output_text", "text": "Done", "annotations": []}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
sync_cli_response_conversion_kind("openai:responses", "claude:messages"),
|
||||||
|
Some(SyncCliResponseConversionKind::ToClaudeCli)
|
||||||
|
);
|
||||||
|
assert!(convert_standard_cli_response(
|
||||||
|
&provider_body_json,
|
||||||
|
"openai:responses",
|
||||||
|
"claude:messages",
|
||||||
|
&report_context
|
||||||
|
)
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
let product = maybe_build_standard_sync_finalize_product_from_normalized_payload(
|
||||||
|
"claude_cli_sync_finalize",
|
||||||
|
200,
|
||||||
|
Some(&report_context),
|
||||||
|
Some(&provider_body_json),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("dispatch should succeed")
|
||||||
|
.expect("dispatch should produce a product");
|
||||||
|
|
||||||
|
let StandardSyncFinalizeNormalizedProduct::CrossFormat(product) = product else {
|
||||||
|
panic!("openai responses provider body should be converted for claude client")
|
||||||
|
};
|
||||||
|
assert_eq!(product.client_body_json["type"], "message");
|
||||||
|
assert_eq!(product.client_body_json["content"][0]["text"], "Done");
|
||||||
|
assert_eq!(product.client_body_json["stop_reason"], "end_turn");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn standard_sync_finalize_product_falls_back_to_generic_standard_cross_format() {
|
fn standard_sync_finalize_product_falls_back_to_generic_standard_cross_format() {
|
||||||
let report_context = json!({
|
let report_context = json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user