Skip to content

Fix pricing cache/shared-dict bugs and a mistyped terminal error handler - #232

Open
RasAlGhul96 wants to merge 1 commit into
Maciek-roboblog:mainfrom
RasAlGhul96:fix/pricing-cache-and-shared-dicts
Open

Fix pricing cache/shared-dict bugs and a mistyped terminal error handler#232
RasAlGhul96 wants to merge 1 commit into
Maciek-roboblog:mainfrom
RasAlGhul96:fix/pricing-cache-and-shared-dicts

Conversation

@RasAlGhul96

@RasAlGhul96 RasAlGhul96 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

Found while doing a source-level audit of core/pricing.py, error_handling.py, and terminal/manager.py. No behavior change for the currently-shipped pricing tables, but these are latent correctness bugs:

  • core/pricing.py — cache key ignored strict. calculate_cost's cache key was f"{model}:{input}:{output}:{cache_creation}:{cache_read}", without strict. A strict=False lookup for an unknown model caches a $0 result; a later strict=True lookup for the same token counts then silently returns that cached value instead of raising KeyError. Fixed by including strict in the cache key.
  • core/pricing.py — shared pricing dicts mutated in place. Several model aliases (claude-opus-4-5 .. claude-opus-4-8, etc.) are assigned the same FALLBACK_PRICING["opus"] dict object in self.pricing. _get_pricing_for_model filled in missing cache_creation/cache_read keys by mutating that dict in place, so the fix could leak across every alias sharing the object (and into FALLBACK_PRICING itself) the first time a pricing dict without those fields was used — e.g. via custom_pricing. Replaced with a small copy-on-write helper (_ensure_cache_pricing) that only allocates a new dict when a field is actually missing.
  • terminal/manager.pyhandle_error_and_exit typed Union[Exception, str] but always does raise error. Passing a str would raise TypeError: exceptions must derive from BaseException, masking the original error. The only caller (cli/main.py) always passes an Exception, so the str branch was dead and misleading; narrowed the type to Exception.
  • error_handling.py — silent except Exception: pass in the logging fallback. If the logger call itself fails, the original error vanished with zero trace. Now writes a last-resort line to stderr before falling back to pass.
  • pyproject.toml — uncommented the Operating System :: Microsoft :: Windows classifier, which was inconsistent with the existing win32-specific tzdata/tzlocal dependencies and the dedicated WSL path support/tests already in the codebase.

Test plan

  • pytest src/tests/test_pricing.py src/tests/test_error_handling.py — all green
  • Full suite (pytest src/tests) — same 15 pre-existing failures as on unmodified main (Windows/Python 3.14 locale & signal environment issues, verified via git stash comparison), no new failures introduced
  • mypy on the touched files — no new errors (pre-existing HAS_TERMIOS redefinition warnings in themes.py/manager.py are unrelated to this change)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed pricing cache behavior so strict and non-strict calculations return the correct costs.
    • Prevented pricing data from being unintentionally modified when cache details are added.
    • Improved error reporting with a fallback message when primary logging fails.
  • Compatibility
    • Windows support is now correctly reflected in package metadata.
  • Maintenance
    • Clarified terminal error-handling requirements for more consistent failures.

Found during an audit of core/pricing.py, error_handling.py, and
terminal/manager.py:

- calculate_cost's cache key omitted `strict`, so a strict=False lookup
  for an unknown model could cache a result that a later strict=True
  lookup for the same tokens would silently reuse instead of raising.
- _get_pricing_for_model filled in missing cache_creation/cache_read
  fields by mutating the pricing dict in place. Several model aliases
  (e.g. claude-opus-4-5..4-8) share the same FALLBACK_PRICING dict
  object, so that mutation could leak across aliases (and into
  FALLBACK_PRICING itself) once a pricing dict without those fields
  was used. Replaced with a copy-on-write helper.
- handle_error_and_exit declared Union[Exception, str] but always did
  `raise error`, which would raise TypeError if a str was ever passed.
  The only caller always passes an Exception, so the str branch was
  dead and misleading; narrowed the type.
- report_error's fallback `except Exception: pass` swallowed logger
  failures with zero visibility; now writes a last-resort line to
  stderr first.
- Uncommented the Windows PyPI classifier, which was inconsistent with
  the existing win32-specific tzdata/tzlocal deps and WSL path support.

Verified with the existing test suite (no new failures) and mypy on
the touched files.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Pricing cache handling

Layer / File(s) Summary
Cache key and pricing normalization
src/claude_monitor/core/pricing.py
Cost-cache keys now include strictness, and pricing lookups use copy-on-write cache-field normalization.

Error handling contracts

Layer / File(s) Summary
Error reporting fallback
src/claude_monitor/error_handling.py
Logging failures now fall back to stderr output without propagating secondary failures.
Terminal error contract
src/claude_monitor/terminal/manager.py
handle_error_and_exit now accepts an Exception parameter instead of `Exception

Package metadata

Layer / File(s) Summary
Windows classifier
pyproject.toml
The Windows operating system Trove classifier is enabled.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: maciek-roboblog

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly covers the main pricing cache and terminal error-handler fixes, even though it omits the stderr fallback and Windows classifier change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/claude_monitor/core/pricing.py`:
- Around line 205-222: Update _ensure_cache_pricing to return the copied pricing
dictionary after adding missing cache fields without assigning it back through
self.pricing[key]. Preserve the existing fast path for pricing entries that
already contain both cache fields, and avoid mutating the caller-provided
custom_pricing dictionary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 422bd3e4-8723-45a2-9edd-237df2879672

📥 Commits

Reviewing files that changed from the base of the PR and between c59a83b and 799dde4.

📒 Files selected for processing (4)
  • pyproject.toml
  • src/claude_monitor/core/pricing.py
  • src/claude_monitor/error_handling.py
  • src/claude_monitor/terminal/manager.py

Comment on lines +205 to +222
def _ensure_cache_pricing(self, key: str) -> Dict[str, float]:
"""Return self.pricing[key] guaranteed to have cache fields.

Several keys (e.g. the claude-opus-4-5..4-8 aliases) point at the
*same* FALLBACK_PRICING dict object. Filling in missing cache fields
in place would mutate that shared object for every alias (and, for
FALLBACK_PRICING itself, globally). Copy-on-write instead: only
allocate a new dict when a field is actually missing.
"""
pricing = self.pricing[key]
if "cache_creation" in pricing and "cache_read" in pricing:
return pricing
pricing = dict(pricing)
pricing.setdefault("cache_creation", pricing["input"] * 1.25)
pricing.setdefault("cache_read", pricing["input"] * 0.1)
self.pricing[key] = pricing
return pricing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid mutating the outer self.pricing dictionary.

Assigning self.pricing[key] = pricing mutates the outer dictionary. If self.pricing was initialized with a user-provided custom_pricing dictionary in __init__ (via self.pricing = custom_pricing or ...), this will unexpectedly mutate the user's object, creating a side effect.

Since cost calculations are already cached at the calculate_cost level (via self._cost_cache), memoizing the patched pricing dictionary here is unnecessary and risks modifying user data. Consider returning the copied dictionary without writing it back.

💡 Proposed fix to avoid side effects
     def _ensure_cache_pricing(self, key: str) -> Dict[str, float]:
         """Return self.pricing[key] guaranteed to have cache fields.
 
         Several keys (e.g. the claude-opus-4-5..4-8 aliases) point at the
         *same* FALLBACK_PRICING dict object. Filling in missing cache fields
         in place would mutate that shared object for every alias (and, for
         FALLBACK_PRICING itself, globally). Copy-on-write instead: only
         allocate a new dict when a field is actually missing.
         """
         pricing = self.pricing[key]
         if "cache_creation" in pricing and "cache_read" in pricing:
             return pricing
-        pricing = dict(pricing)
-        pricing.setdefault("cache_creation", pricing["input"] * 1.25)
-        pricing.setdefault("cache_read", pricing["input"] * 0.1)
-        self.pricing[key] = pricing
-        return pricing
+        patched_pricing = dict(pricing)
+        patched_pricing.setdefault("cache_creation", patched_pricing["input"] * 1.25)
+        patched_pricing.setdefault("cache_read", patched_pricing["input"] * 0.1)
+        return patched_pricing
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _ensure_cache_pricing(self, key: str) -> Dict[str, float]:
"""Return self.pricing[key] guaranteed to have cache fields.
Several keys (e.g. the claude-opus-4-5..4-8 aliases) point at the
*same* FALLBACK_PRICING dict object. Filling in missing cache fields
in place would mutate that shared object for every alias (and, for
FALLBACK_PRICING itself, globally). Copy-on-write instead: only
allocate a new dict when a field is actually missing.
"""
pricing = self.pricing[key]
if "cache_creation" in pricing and "cache_read" in pricing:
return pricing
pricing = dict(pricing)
pricing.setdefault("cache_creation", pricing["input"] * 1.25)
pricing.setdefault("cache_read", pricing["input"] * 0.1)
self.pricing[key] = pricing
return pricing
def _ensure_cache_pricing(self, key: str) -> Dict[str, float]:
"""Return self.pricing[key] guaranteed to have cache fields.
Several keys (e.g. the claude-opus-4-5..4-8 aliases) point at the
*same* FALLBACK_PRICING dict object. Filling in missing cache fields
in place would mutate that shared object for every alias (and, for
FALLBACK_PRICING itself, globally). Copy-on-write instead: only
allocate a new dict when a field is actually missing.
"""
pricing = self.pricing[key]
if "cache_creation" in pricing and "cache_read" in pricing:
return pricing
patched_pricing = dict(pricing)
patched_pricing.setdefault("cache_creation", patched_pricing["input"] * 1.25)
patched_pricing.setdefault("cache_read", patched_pricing["input"] * 0.1)
return patched_pricing
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/claude_monitor/core/pricing.py` around lines 205 - 222, Update
_ensure_cache_pricing to return the copied pricing dictionary after adding
missing cache fields without assigning it back through self.pricing[key].
Preserve the existing fast path for pricing entries that already contain both
cache fields, and avoid mutating the caller-provided custom_pricing dictionary.

@kramersharp

Copy link
Copy Markdown

ClaimVerifier: blocking findings

The following file claims in commit messages / PR body were not actually modified on this branch:

  • cli/main.py — Claim in pr_body references cli/main.py but branch diff does not modify it
  • src/tests/test_pricing.py — Claim in pr_body references src/tests/test_pricing.py but branch diff does not modify it
  • src/tests/test_error_handling.py — Claim in pr_body references src/tests/test_error_handling.py but branch diff does not modify it

Please update the claim or add the missing changes.

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