Skip to content

fix(openai): stop dropping cost when usage is a dict or cost is an int#1781

Open
arthi-arumugam-git wants to merge 1 commit into
langfuse:mainfrom
arthi-arumugam-git:fix/parse-cost-drops-int-and-dict-usage
Open

fix(openai): stop dropping cost when usage is a dict or cost is an int#1781
arthi-arumugam-git wants to merge 1 commit into
langfuse:mainfrom
arthi-arumugam-git:fix/parse-cost-drops-int-and-dict-usage

Conversation

@arthi-arumugam-git

@arthi-arumugam-git arthi-arumugam-git commented Jul 26, 2026

Copy link
Copy Markdown

What

_parse_cost in langfuse/openai.py discards cost in two shapes that occur in practice, so cost_details comes back None and the invocation is recorded with no cost.

if hasattr(usage, "cost") and isinstance(getattr(usage, "cost"), float):

1. usage as a dict. hasattr({"cost": 0.002}, "cost") is False, so cost is dropped entirely. This is not hypothetical: _parse_usage right above it already branches on isinstance(usage, dict), and _update_langfuse_generation calls both with the same object:

if usage is not None:
    update["usage_details"] = _parse_usage(usage)
    update["cost_details"] = _parse_cost(usage)

So any dict-shaped usage that _parse_usage handles correctly loses its cost.

2. An integer cost. isinstance(0, float) is False in Python. OpenRouter reports "cost": 0 for free models and for fully cached responses, and any whole-number cost serialises as an int. Those get dropped too.

Measured against the current code, only one of five realistic shapes survives:

input before after
Usage(cost=0.0021) {"total": 0.0021} {"total": 0.0021}
Usage(cost=0) None {"total": 0.0}
Usage(cost=2) None {"total": 2.0}
{"cost": 0.0021} None {"total": 0.0021}
{"cost": 0} None {"total": 0.0}

Change

Read cost off either shape, and accept int as well as float. bool is excluded explicitly because it is a subclass of int, so a stray "cost": true would otherwise be recorded as a cost of 1.0.

Tests

tests/unit/test_parse_cost.py, 8 cases. Two of them fail on the current code and pass with the change:

FAILED tests/unit/test_parse_cost.py::test_dict_with_float_cost
FAILED tests/unit/test_parse_cost.py::test_integer_cost_is_not_dropped
2 failed, 6 passed

The other six cover None usage, an object without cost, a dict without cost, and the bool and string rejections, so the fix cannot widen the accepted set by accident.

ruff format --check is clean on both files. The pre-existing ruff check findings in openai.py are untouched.

Greptile Summary

This PR fixes OpenRouter cost extraction and expands regression coverage.

  • Reads cost from both dictionary-shaped and object-shaped usage payloads.
  • Accepts integer and floating-point costs while explicitly rejecting booleans.
  • Normalizes accepted costs to floats and adds tests for supported and rejected values.

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified.

The parser now handles the documented usage shapes while preserving rejection of unsupported values, and the downstream cost-details contract accepts the normalized floating-point result.

Reviews (1): Last reviewed commit: "fix(openai): stop dropping cost when usa..." | Re-trigger Greptile

Context used:

_parse_cost only read `cost` off an attribute and only accepted `float`,
so two real shapes were silently discarded:

- `usage` arriving as a dict. `_parse_usage` already branches on
  `isinstance(usage, dict)` and both functions are called with the same
  object in `_extract_streamed_response_api_response`, so a dict reaches
  `_parse_cost` too. `hasattr(dict, "cost")` is always False.
- an integer cost. `isinstance(0, float)` is False, so an OpenRouter
  `"cost": 0` from a free or fully cached model was dropped.

Reads the key either way and accepts int as well as float. bool is
excluded explicitly since it is a subclass of int.

@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

CLAassistant commented Jul 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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