Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/landing-page/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Next.js 16 Project Setup', () => {
});

test('Next.js 16.x is installed and locked', () => {
expect(pkg.dependencies.next).toBe('16.1.6');
expect(pkg.dependencies.next).toBe('16.2.3');
});

test('React 19.x is installed and locked', () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/landing-page/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next": "16.2.3",
"next-intl": "^4.8.2",
"next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1",
Expand All @@ -56,7 +56,7 @@
"axe-core": "^4.11.1",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"eslint-config-next": "16.2.3",
"happy-dom": "^20.8.8",
"jest-axe": "^10.0.0",
"madge": "^8.0.0",
Expand Down
42 changes: 27 additions & 15 deletions packages/claude-code-plugin/hooks/codingbuddy-hud.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,40 @@
sys.path.insert(0, _LIB_DIR)

# === test_hud.py compatibility re-exports — DO NOT REMOVE without coordinated test update ===
# Defensive fallback: statusLine is a hot path invoked by Claude Code on
# every render. If any lib module is temporarily broken (e.g. mid-wave
# refactor), fall back to minimal inline implementations so the status
# bar still renders instead of crashing the Claude Code subprocess.
# Narrow the fallback to ImportError only: real logic bugs in lib modules
# (SyntaxError, NameError, AttributeError) must surface immediately instead
# of being silently swallowed by a catch-all. If a lib module fails to import
# entirely, the outer main() try/except at the bottom of this file still
# emits the minimal safe output via the BUDDY_FACE constant.
try:
from hud_buddy import BUDDY_FACE # canonical SSoT via tiny_actor_presets
except Exception: # pragma: no cover - defensive
BUDDY_FACE = "\u25d5\u203f\u25d5" # ◕‿◕
except ImportError: # pragma: no cover - defensive
BUDDY_FACE = "◕‿◕" # minimal constant for safe-output path

try:
from hud_rate_limits import format_rate_limits
except Exception: # pragma: no cover - defensive
def format_rate_limits(stdin_data: dict) -> str: # type: ignore[misc]
return ""
from hud_rate_limits import format_rate_limits # noqa: F401 re-exported for test_hud.py
except ImportError: # pragma: no cover - defensive
pass # main() catch-all handles absence

try:
from hud_version import get_fresh_version as _get_fresh_version # backcompat alias
except Exception: # pragma: no cover - defensive
def _get_fresh_version( # type: ignore[misc]
hud_state: dict, *, plugins_file: str = ""
) -> str:
return hud_state.get("version", "")
except ImportError: # pragma: no cover - defensive
pass # main() catch-all handles absence

# Wave 2-B velocity + Wave 2-C cache savings hot-path suffixes for the cost segment.
# Hoisted to module top per perf-1485 H1 so format_status_line avoids a
# sys.modules lookup on every render (~0.47μs saved per call).
try:
from hud_velocity import format_velocity_segment as _format_velocity_segment
except ImportError: # pragma: no cover - defensive
def _format_velocity_segment(stdin_data, hud_state=None): # type: ignore[misc]
return ""

try:
from hud_cache_savings import format_cache_savings as _format_cache_savings
except ImportError: # pragma: no cover - defensive
def _format_cache_savings(stdin_data): # type: ignore[misc]
return ""

# Agent eye glyphs from .ai-rules agent definitions.
AGENT_GLYPHS = {
Expand Down
125 changes: 115 additions & 10 deletions packages/claude-code-plugin/hooks/lib/hud_cache_savings.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,119 @@
"""Cache-savings badge for CodingBuddy statusLine (#1326).
"""Cache savings calculator for CodingBuddy statusLine (#1326, Wave 2-C).

Wave 0 skeleton — reserved for **Wave 2-C**.
Claude API's prompt caching charges ``cache_read_input_tokens`` at
10% of the base input price — a 90% discount. This module quantifies
that discount so the HUD can surface "how much you saved by caching"
as a badge like ``"💰$4.56 saved"`` appended to the cost segment.

Planned contents (Wave 2-C owner fills):
* ``compute_cache_savings(cost_breakdown: dict) -> float`` — USD
avoided by cache hits
* ``format_cache_savings_badge(savings_usd: float) -> str``
Primary entry points:

Source of truth for the computation is the stdin ``cost`` payload
(cached-input vs non-cached-input token counts combined with
``MODEL_PRICING`` — both already available in ``codingbuddy-hud``).
This module will be the single import target for Wave 3 assembly.
- :func:`compute_cache_savings` — pure arithmetic helper (tokens +
model_id → dollars saved).
- :func:`format_cache_savings` — end-to-end renderer that reads
Claude Code stdin, extracts the relevant fields, and returns the
formatted badge string (or ``""`` when there is nothing to show).
"""
from __future__ import annotations

from typing import Any, Dict

# Money glyph — U+1F4B0 money bag emoji
_MONEY_GLYPH: str = "\U0001f4b0" # 💰

# cache_read tokens cost 10% of the input price, so the per-token
# savings equals 90% of the input price.
_CACHE_DISCOUNT: float = 0.90

# Minimum dollar savings required to show the badge. Hides noise
# below one cent so the status bar does not flicker on tiny reads.
_MIN_DISPLAY_USD: float = 0.01

# Baseline input prices in USD per million tokens. Mirrors the
# ``MODEL_PRICING`` table in ``codingbuddy-hud.py``.
_INPUT_PRICE_PER_M: Dict[str, float] = {
"haiku": 0.80,
"sonnet": 3.00,
"opus": 15.00,
}

# Sonnet as the safe default when the model family cannot be
# identified. Avoids over-claiming savings on unknown tiers.
_DEFAULT_INPUT_PRICE_PER_M: float = 3.00


def _input_price_per_million(model_id: str) -> float:
"""Return the baseline input price (USD per million tokens).

Case-insensitive substring match against the known family keys.
Falls back to the sonnet tier when no key matches.
"""
if not model_id:
return _DEFAULT_INPUT_PRICE_PER_M
lowered = model_id.lower()
for key, price in _INPUT_PRICE_PER_M.items():
if key in lowered:
return price
return _DEFAULT_INPUT_PRICE_PER_M


def compute_cache_savings(
cache_read_tokens: Any,
model_id: str,
) -> float:
"""Return the dollar amount saved by cache reads.

Formula::

savings = cache_read_tokens * (input_price / 1_000_000) * 0.90

Defensive coercion: negative or non-numeric inputs return
``0.0`` so callers never render a "saved -$0.12" surprise when
upstream payloads are malformed.
"""
try:
tokens = int(cache_read_tokens)
except (TypeError, ValueError):
return 0.0
if tokens <= 0:
return 0.0
price = _input_price_per_million(model_id)
return (tokens / 1_000_000.0) * price * _CACHE_DISCOUNT


def format_cache_savings(stdin_data: Dict[str, Any]) -> str:
"""Render the cache savings badge from a stdin payload.

Output format:

``💰$4.56 saved``

Returns an empty string when any of the following hold:

* ``stdin_data`` is empty or has no ``context_window``
* ``current_usage`` is missing
* ``cache_read_input_tokens`` is zero, absent, or negative
* Computed savings < ``$0.01`` (noise floor)

Model identification is sourced from ``stdin_data.model.id``
(or ``display_name`` fallback). Unknown models default to the
sonnet-tier input price so the display still shows a
conservative estimate.
"""
if not stdin_data:
return ""

ctx = stdin_data.get("context_window") or {}
usage = ctx.get("current_usage") or {}
cache_read = usage.get("cache_read_input_tokens", 0) or 0

if not cache_read or (isinstance(cache_read, (int, float)) and cache_read <= 0):
return ""

model_info = stdin_data.get("model") or {}
model_id = model_info.get("id") or model_info.get("display_name") or ""

savings = compute_cache_savings(cache_read, model_id)
if savings < _MIN_DISPLAY_USD:
return ""

return f"{_MONEY_GLYPH}${savings:.2f} saved"
Loading
Loading