This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new d46d97fa8d5 Note the unparseable-output case in OpenAIResponseOperator 
XCom docs (#74519)
d46d97fa8d5 is described below

commit d46d97fa8d5082e73dea68610b514aadc6346419
Author: Yash jain <[email protected]>
AuthorDate: Thu Nov 5 20:21:08 2026 +0530

    Note the unparseable-output case in OpenAIResponseOperator XCom docs 
(#74519)
    
    The class docstring said a structured response that fails the task still
    records its id and token usage. That does not hold when the SDK cannot
    parse the output, because it raises before returning a response object.
    
    The test for a rejected response now also covers an incomplete response,
    which pins that both keys are pushed before the status check.
---
 .../airflow/providers/openai/operators/openai.py   |  4 ++-
 .../tests/unit/openai/operators/test_openai.py     | 29 ++++++++++++++++++----
 2 files changed, 27 insertions(+), 6 deletions(-)

diff --git a/providers/openai/src/airflow/providers/openai/operators/openai.py 
b/providers/openai/src/airflow/providers/openai/operators/openai.py
index f761442bc37..202d311867e 100644
--- a/providers/openai/src/airflow/providers/openai/operators/openai.py
+++ b/providers/openai/src/airflow/providers/openai/operators/openai.py
@@ -205,7 +205,9 @@ class OpenAIResponseOperator(BaseOperator):
     only reflects the current attempt rather than a silently under-reported 
total across
     retries. With ``text_format`` set, both keys are pushed before the 
structured output is
     checked, so a response that then fails the task still records its id and 
token usage.
-    Both XCom pushes are skipped when ``do_xcom_push=False``.
+    The exception is output the SDK cannot parse, such as JSON cut off 
mid-object: it raises
+    before returning a response object, so there is no id or usage to push and 
neither key
+    is recorded. Both XCom pushes are skipped when ``do_xcom_push=False``.
     """
 
     template_fields: Sequence[str] = (
diff --git a/providers/openai/tests/unit/openai/operators/test_openai.py 
b/providers/openai/tests/unit/openai/operators/test_openai.py
index dafb984ffc8..39381a117f5 100644
--- a/providers/openai/tests/unit/openai/operators/test_openai.py
+++ b/providers/openai/tests/unit/openai/operators/test_openai.py
@@ -918,20 +918,39 @@ class TestOpenAIResponseOperatorStructuredOutput:
                 key="usage", value={**usage.model_dump(mode="json"), 
"try_number": 2}
             )
 
-    def test_rejected_response_still_records_id_and_usage(self):
+    @pytest.mark.parametrize(
+        ("response_fields", "expected_error"),
+        [
+            pytest.param(
+                {"refusal": "I cannot help with that request."},
+                "did not return a structured output",
+                id="refusal",
+            ),
+            pytest.param(
+                {
+                    "output_parsed": _StructuredPerson(name="Alice"),
+                    "status": "incomplete",
+                    "incomplete_details": 
IncompleteDetails(reason="max_output_tokens"),
+                },
+                "did not complete",
+                id="incomplete",
+            ),
+        ],
+    )
+    def test_rejected_response_still_records_id_and_usage(self, 
response_fields, expected_error):
         # The API call behind a rejected response was still billed, so its id 
and token usage
-        # are pushed before the structured output is checked.
+        # are pushed before the status and the structured output are checked.
         operator, hook = self._operator()
         usage = _build_usage()
         hook.parse_response.return_value = _build_parsed_response(
-            response_id="resp_refused", refusal="I cannot help with that 
request.", usage=usage
+            response_id="resp_rejected", usage=usage, **response_fields
         )
         context = _build_execute_context()
 
-        with pytest.raises(ValueError, match="did not return a structured 
output"):
+        with pytest.raises(ValueError, match=expected_error):
             operator.execute(context)
 
-        context["ti"].xcom_push.assert_any_call(key="response_id", 
value="resp_refused")
+        context["ti"].xcom_push.assert_any_call(key="response_id", 
value="resp_rejected")
         context["ti"].xcom_push.assert_any_call(
             key="usage", value={**usage.model_dump(mode="json"), "try_number": 
1}
         )

Reply via email to