Skip to content

fix(openai): handle None arguments in legacy function_call streaming merge#1753

Open
roli-lpci wants to merge 2 commits into
langfuse:mainfrom
roli-lpci:fix/legacy-function-call-none-arguments
Open

fix(openai): handle None arguments in legacy function_call streaming merge#1753
roli-lpci wants to merge 2 commits into
langfuse:mainfrom
roli-lpci:fix/legacy-function-call-none-arguments

Conversation

@roli-lpci

@roli-lpci roli-lpci commented Jul 11, 2026

Copy link
Copy Markdown

What does this PR do?

_extract_streamed_openai_response (langfuse/openai.py) merges the deprecated function_call
streaming delta with a bare +=:

curr["arguments"] += getattr(tool_call_chunk, "arguments", "")

getattr(obj, "arguments", "") only returns the "" default when the attribute is absent. Some
OpenAI-compatible proxies send a first delta with function_call.arguments explicitly set to
None (not missing) — a real payload shape, not a hypothetical one. In that case
curr["arguments"] is None, and None += str raises TypeError, killing the merge for the
whole streamed completion. The TypeError is currently swallowed by a bare except Exception: pass further up the call stack, so today this fails silently: the generation ends with no output
and no error a caller would notice.

This is the same crash class PR #1339 already fixed for the newer tool_calls array — but that
fix only touched the tool_calls branch a few lines below; the sibling legacy function_call
branch was never patched. The tool_calls branch already guards the accumulator side and skips
None increments:

if tool_arguments is not None:
    function_call["arguments"] = (function_call.get("arguments") or "") + tool_arguments

The fix brings the same protection to the legacy branch, adopting that branch's
(... or "") + ... accumulator guard and coalescing the incoming delta inline so a None (or
missing) arguments on either side can't break the concatenation:

curr["arguments"] = (curr.get("arguments") or "") + (
    getattr(tool_call_chunk, "arguments", None) or ""
)

This guards both sides: the accumulator being None, and the incoming delta's arguments being
None or missing. When both sides are normal strings it is equivalent to the original += — no
behavior change on the happy path. Two-line functional change to a single merge branch; does not
touch the tool_calls branch or any other streaming path.

No existing issue or open PR describes this legacy-function_call-branch crash (see the search
under Verification below); happy to open a tracking issue first if that's preferred for bug-fix PRs.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • Tooling, CI, or repo maintenance

Verification

List the main commands you ran:

# RED: fix reverted (source only, test kept)
python -m pytest tests/unit/test_openai.py::test_streaming_chat_completion_handles_legacy_function_call_none_arguments -q
# -> 1 failed: TypeError: unsupported operand type(s) for +=: 'NoneType' and 'str' in the legacy
#    function_call merge branch of _extract_streamed_openai_response

# GREEN: fix restored
python -m pytest tests/unit/test_openai.py::test_streaming_chat_completion_handles_legacy_function_call_none_arguments -q
# -> 1 passed

# Full mapped file, regression check
python -m pytest tests/unit/test_openai.py
# -> 35 passed (34 pre-existing + 1 new), no regressions

ruff check langfuse/openai.py tests/unit/test_openai.py
# -> All checks passed!

gh pr list --repo langfuse/langfuse-python --state open --search "function_call None arguments"
# -> empty; #1339 (merged) is the sibling tool_calls fix this mirrors; no PR touches the legacy
#    function_call branch.

Test added: test_streaming_chat_completion_handles_legacy_function_call_none_arguments, mirroring
the existing test_streaming_chat_completion_preserves_tool_calls_after_content pattern but for
the legacy branch, with a first delta carrying function_call.arguments=None.

Checklist

  • I self-reviewed the diff using code_review.md.
  • I added or updated tests for behavior changes.
  • I updated docs, examples, or .env.template if needed. (No public API or documented
    behavior changed; two-line internal guard.)
  • I did not hand-edit generated files; if generated files changed, I used the upstream
    regeneration path.
  • I did not commit secrets or credentials.

Greptile Summary

This PR fixes a silent TypeError crash in the deprecated function_call streaming-merge branch of _extract_streamed_openai_response when an OpenAI-compatible proxy sends a delta with arguments explicitly set to None. The fix mirrors the identical guard already applied to the tool_calls branch in PR #1339 that was never extended to this legacy sibling branch.

  • langfuse/openai.py: Replaces the bare curr["arguments"] += getattr(...) accumulation with a null-safe pattern (curr.get("arguments") or "") + (getattr(...) or ""), protecting both the accumulator and the incoming delta from being None.
  • tests/unit/test_openai.py: Adds a regression test that replays the two-delta payload shape (first delta with arguments=None, second with the real JSON string) and asserts correct final output.

Confidence Score: 5/5

Safe to merge — the change is a two-line targeted guard that cannot regress the happy path (string + string produces the same result as before), and the new test directly validates the failing case.

The fix is a minimal null-coalescing guard that mirrors an already-proven pattern from the tool_calls branch. The initialization path at line 807 could still store None for arguments on a single-delta stream, but that pre-existing edge produces None output rather than a crash and is out of scope for this PR. No other correctness issues found.

No files require special attention.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Proxy as OpenAI-compatible Proxy
    participant Extract as _extract_streamed_openai_response
    participant Accum as function_call accumulator

    Proxy->>Extract: "chunk 1: function_call.arguments = None"
    Extract->>Accum: "if not curr → initialize {name, arguments: None}"

    Proxy->>Extract: "chunk 2: function_call.arguments = '{"city":"Berlin"}'"
    Note over Extract,Accum: BEFORE fix: None += str → TypeError (swallowed silently)
    Note over Extract,Accum: AFTER fix: (None or "") + (str or "") → '{"city":"Berlin"}'
    Extract->>Accum: "curr["arguments"] = "" + '{"city":"Berlin"}'"
    Accum-->>Extract: "{name: "get_weather", arguments: '{"city":"Berlin"}'}"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Proxy as OpenAI-compatible Proxy
    participant Extract as _extract_streamed_openai_response
    participant Accum as function_call accumulator

    Proxy->>Extract: "chunk 1: function_call.arguments = None"
    Extract->>Accum: "if not curr → initialize {name, arguments: None}"

    Proxy->>Extract: "chunk 2: function_call.arguments = '{"city":"Berlin"}'"
    Note over Extract,Accum: BEFORE fix: None += str → TypeError (swallowed silently)
    Note over Extract,Accum: AFTER fix: (None or "") + (str or "") → '{"city":"Berlin"}'
    Extract->>Accum: "curr["arguments"] = "" + '{"city":"Berlin"}'"
    Accum-->>Extract: "{name: "get_weather", arguments: '{"city":"Berlin"}'}"
Loading

Reviews (1): Last reviewed commit: "test(openai): rebase legacy function_cal..." | Re-trigger Greptile

…se#1751 arity

_extract_streamed_openai_response now returns a 5-tuple (adds
service_tier, upstream langfuse#1751) instead of 4. Update this test's unpack
to match; the bug assertions (model/completion/metadata content) are
unchanged.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants