Skip to content

fix(openai): handle null choices in default response extraction#1742

Open
CodeBlackwell wants to merge 1 commit into
langfuse:mainfrom
CodeBlackwell:crg-farm/1741
Open

fix(openai): handle null choices in default response extraction#1742
CodeBlackwell wants to merge 1 commit into
langfuse:mainfrom
CodeBlackwell:crg-farm/1741

Conversation

@CodeBlackwell

@CodeBlackwell CodeBlackwell commented Jul 7, 2026

Copy link
Copy Markdown

An OpenAI-compatible provider can return a chat completion with choices: null. response.get("choices", []) returns None in that case — the default only applies to a missing key, not an explicit null — so the next len(choices) raises TypeError: object of type 'NoneType' has no len() and hides the provider's real response (issue #1741).

This falls back to [] when choices is null, in both the chat and completion paths. Also guards the httpx logger handler so it's only added when none exists, matching the comment's intent and avoiding duplicate log lines.

Added a unit test covering choices: null for both resource types (fails with the reported TypeError before the fix). Full tests/unit/test_openai.py + test_logger.py pass.

Fixes #1741

Greptile Summary

This PR fixes a TypeError that occurred when an OpenAI-compatible provider returned choices: null in the response body — dict.get("choices", []) does not substitute the default for an explicit null, so len(None) raised. It also guards the httpx logger handler setup to avoid duplicates on module reload.

  • langfuse/openai.py: Changes response.get("choices", []) to response.get("choices") or [] in both the completion and chat branches of _get_langfuse_data_from_default_response, correctly handling an explicit null value.
  • langfuse/logger.py: Wraps the httpx console handler creation in if not httpx_logger.handlers:, matching the comment's intent and preventing duplicate log lines.
  • tests/unit/test_openai.py / test_logger.py: Adds targeted unit tests for both fixes, including a regression test that would reproduce the reported TypeError before the change.

Confidence Score: 4/5

The core fix is correct and well-targeted; a single style nit in the test file is the only thing holding this back from a clean merge.

Both changes are narrow and correct. The or [] idiom properly covers the null-choices case, the logger guard prevents handler duplication, and the new tests would reproduce the original crash before the fix. The only observation is an in-function import in test_logger.py that violates the project's import ordering rule; it does not affect test correctness but should be moved to the module level.

tests/unit/test_logger.py — the import langfuse.logger as lf_logger statement should move to the top of the module.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[OpenAI-compatible provider response] --> B{response is None?}
    B -- yes --> C[Return NoneType message]
    B -- no --> D[Extract model]
    D --> E{resource.type?}
    E -- completion --> F["choices = response.get('choices') or []"]
    E -- chat --> G["choices = response.get('choices') or []"]
    E -- Responses/AsyncResponses --> H[Extract output]
    E -- embedding --> I[Extract data]
    F --> J{len choices > 0?}
    G --> K{len choices > 0?}
    J -- yes, was crashing on null --> L[Extract text]
    J -- no or null → [] --> M[completion = None]
    K -- yes, was crashing on null --> N[Extract messages]
    K -- no or null → [] --> O[completion = None]
    L --> P["Return (model, completion, usage)"]
    M --> P
    N --> P
    O --> P
    H --> P
    I --> P
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"}}}%%
flowchart TD
    A[OpenAI-compatible provider response] --> B{response is None?}
    B -- yes --> C[Return NoneType message]
    B -- no --> D[Extract model]
    D --> E{resource.type?}
    E -- completion --> F["choices = response.get('choices') or []"]
    E -- chat --> G["choices = response.get('choices') or []"]
    E -- Responses/AsyncResponses --> H[Extract output]
    E -- embedding --> I[Extract data]
    F --> J{len choices > 0?}
    G --> K{len choices > 0?}
    J -- yes, was crashing on null --> L[Extract text]
    J -- no or null → [] --> M[completion = None]
    K -- yes, was crashing on null --> N[Extract messages]
    K -- no or null → [] --> O[completion = None]
    L --> P["Return (model, completion, usage)"]
    M --> P
    N --> P
    O --> P
    H --> P
    I --> P
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
tests/unit/test_logger.py:41-44
The `import langfuse.logger as lf_logger` statement is placed inside the function body, which violates the project's rule requiring imports to live at the top of the module. Because the test's purpose is to call `importlib.reload`, this alias can be imported at module level and the reload can still be called on it — moving the import up doesn't change the test's correctness.

```suggestion
def test_httpx_handler_not_duplicated_when_handler_exists():
    httpx_logger = logging.getLogger("httpx")
```

Reviews (1): Last reviewed commit: "fix(openai): handle null choices in defa..." | Re-trigger Greptile

Context used:

  • Rule used - Move imports to the top of the module instead of p... (source)

Learned From
langfuse/langfuse-python#1387

@CLAassistant

CLAassistant commented Jul 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

An OpenAI-compatible provider can return a chat/completion response with
`choices: null`. `response.get("choices", [])` returns None in that case
(the default only applies to a missing key, not an explicit null), so the
following `len(choices)` raised `TypeError: object of type 'NoneType' has
no len()` and masked the provider's real response. Fall back to [] on a
null value so the wrapper treats it as no choices.

Also guard the httpx logger handler so it is only added when none exists,
matching the comment's stated intent and avoiding duplicate log lines.

Fixes langfuse#1741
@CodeBlackwell CodeBlackwell marked this pull request as ready for review July 7, 2026 22:10

@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.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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.

OpenAI wrapper crashes when chat completion response has choices=null

2 participants