fix(ai): allow null OpenAI Responses error fields

This commit is contained in:
fawney19
2026-04-29 21:12:14 +08:00
parent a16550249b
commit 37392d8774
6 changed files with 142 additions and 7 deletions

View File

@@ -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]
fn local_finalize_handles_openai_chat_stream_response_from_openai_chat() {
let body = concat!(

View File

@@ -483,7 +483,7 @@ pub(crate) fn has_nested_error(value: &serde_json::Value) -> bool {
return false;
};
if object.contains_key("error") {
if object.get("error").is_some_and(|error| !error.is_null()) {
return true;
}
if object
@@ -500,7 +500,9 @@ pub(crate) fn has_nested_error(value: &serde_json::Value) -> bool {
.is_some_and(|chunks| {
chunks.iter().any(|chunk| {
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
.get("type")
.and_then(|value| value.as_str())

View File

@@ -11,8 +11,9 @@ use crate::ai_pipeline_api::{
GatewayControlSyncDecisionResponse,
};
use crate::execution_runtime::submission::{
build_best_effort_local_core_error_body, resolve_core_error_background_report_kind,
resolve_core_success_background_report_kind, resolve_local_core_error_response_body_json,
build_best_effort_local_core_error_body, has_nested_error,
resolve_core_error_background_report_kind, resolve_core_success_background_report_kind,
resolve_local_core_error_response_body_json,
};
use crate::execution_runtime::{
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]
fn build_best_effort_local_core_error_body_converts_sync_errors_across_standard_families() {
let cases = vec![