fix: harden auxiliary codex adapter — dict-shaped items + tool call guard (#5734)
Two remaining gaps from the codex empty-output spec: 1. Normalize dict-shaped streamed items: output_item.done events may yield dicts (raw/fallback paths) instead of SDK objects. The extraction loop now uses _item_get() that handles both getattr and dict .get() access. 2. Avoid plain-text synthesis when function_call events were streamed: tracks has_function_calls during streaming and skips text-delta synthesis when tool calls are present — prevents collapsing a tool-call response into a fake text message.
This commit is contained in:
@@ -265,6 +265,7 @@ class _CodexCompletionsAdapter:
|
|||||||
# get_final_response() even when items were streamed.
|
# get_final_response() even when items were streamed.
|
||||||
collected_output_items: List[Any] = []
|
collected_output_items: List[Any] = []
|
||||||
collected_text_deltas: List[str] = []
|
collected_text_deltas: List[str] = []
|
||||||
|
has_function_calls = False
|
||||||
with self._client.responses.stream(**resp_kwargs) as stream:
|
with self._client.responses.stream(**resp_kwargs) as stream:
|
||||||
for _event in stream:
|
for _event in stream:
|
||||||
_etype = getattr(_event, "type", "")
|
_etype = getattr(_event, "type", "")
|
||||||
@@ -276,6 +277,8 @@ class _CodexCompletionsAdapter:
|
|||||||
_delta = getattr(_event, "delta", "")
|
_delta = getattr(_event, "delta", "")
|
||||||
if _delta:
|
if _delta:
|
||||||
collected_text_deltas.append(_delta)
|
collected_text_deltas.append(_delta)
|
||||||
|
elif "function_call" in _etype:
|
||||||
|
has_function_calls = True
|
||||||
final = stream.get_final_response()
|
final = stream.get_final_response()
|
||||||
|
|
||||||
# Backfill empty output from collected stream events
|
# Backfill empty output from collected stream events
|
||||||
@@ -287,7 +290,10 @@ class _CodexCompletionsAdapter:
|
|||||||
"Codex auxiliary: backfilled %d output items from stream events",
|
"Codex auxiliary: backfilled %d output items from stream events",
|
||||||
len(collected_output_items),
|
len(collected_output_items),
|
||||||
)
|
)
|
||||||
elif collected_text_deltas:
|
elif collected_text_deltas and not has_function_calls:
|
||||||
|
# Only synthesize text when no tool calls were streamed —
|
||||||
|
# a function_call response with incidental text should not
|
||||||
|
# be collapsed into a plain-text message.
|
||||||
assembled = "".join(collected_text_deltas)
|
assembled = "".join(collected_text_deltas)
|
||||||
final.output = [SimpleNamespace(
|
final.output = [SimpleNamespace(
|
||||||
type="message", role="assistant", status="completed",
|
type="message", role="assistant", status="completed",
|
||||||
@@ -298,21 +304,29 @@ class _CodexCompletionsAdapter:
|
|||||||
len(collected_text_deltas), len(assembled),
|
len(collected_text_deltas), len(assembled),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extract text and tool calls from the Responses output
|
# Extract text and tool calls from the Responses output.
|
||||||
|
# Items may be SDK objects (attrs) or dicts (raw/fallback paths),
|
||||||
|
# so use a helper that handles both shapes.
|
||||||
|
def _item_get(obj: Any, key: str, default: Any = None) -> Any:
|
||||||
|
val = getattr(obj, key, None)
|
||||||
|
if val is None and isinstance(obj, dict):
|
||||||
|
val = obj.get(key, default)
|
||||||
|
return val if val is not None else default
|
||||||
|
|
||||||
for item in getattr(final, "output", []):
|
for item in getattr(final, "output", []):
|
||||||
item_type = getattr(item, "type", None)
|
item_type = _item_get(item, "type")
|
||||||
if item_type == "message":
|
if item_type == "message":
|
||||||
for part in getattr(item, "content", []):
|
for part in (_item_get(item, "content") or []):
|
||||||
ptype = getattr(part, "type", None)
|
ptype = _item_get(part, "type")
|
||||||
if ptype in ("output_text", "text"):
|
if ptype in ("output_text", "text"):
|
||||||
text_parts.append(getattr(part, "text", ""))
|
text_parts.append(_item_get(part, "text", ""))
|
||||||
elif item_type == "function_call":
|
elif item_type == "function_call":
|
||||||
tool_calls_raw.append(SimpleNamespace(
|
tool_calls_raw.append(SimpleNamespace(
|
||||||
id=getattr(item, "call_id", ""),
|
id=_item_get(item, "call_id", ""),
|
||||||
type="function",
|
type="function",
|
||||||
function=SimpleNamespace(
|
function=SimpleNamespace(
|
||||||
name=getattr(item, "name", ""),
|
name=_item_get(item, "name", ""),
|
||||||
arguments=getattr(item, "arguments", "{}"),
|
arguments=_item_get(item, "arguments", "{}"),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user