Skip to content
Merged
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 pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "alpha-engine-lib"
version = "0.40.0"
version = "0.40.1"
description = "Shared utilities for the Alpha Engine modules: preflight, logging, ArcticDB, dates, decision capture, cost telemetry, Anthropic payload chokepoint, artifact freshness, RAG, agent schemas, SSM secrets, Telegram + SNS alerts, EC2 spot resilience, SSM log-capture, SSM dispatcher, Step-Functions execution-state projection, and S3-conditional-PUT writer locks. Full surface documented in README."
readme = "README.md"
# EC2 still runs Python 3.9 on the always-on micro instance (boto3 drops
Expand Down
2 changes: 1 addition & 1 deletion src/alpha_engine_lib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""alpha-engine-lib — shared utilities for Alpha Engine modules."""

__version__ = "0.40.0"
__version__ = "0.40.1"
37 changes: 31 additions & 6 deletions src/alpha_engine_lib/cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@

from __future__ import annotations

import re
from datetime import date, datetime, timezone
from importlib import resources
from pathlib import Path
Expand All @@ -68,6 +69,17 @@

from alpha_engine_lib.decision_capture import ModelMetadata

# Anthropic SDK model IDs come in two forms: the family alias
# (e.g. ``claude-haiku-4-5``) and the dated snapshot form
# (e.g. ``claude-haiku-4-5-20251001``). ``Message.model`` returns the dated
# form even when the caller requested the alias, but our pricing YAML is
# keyed on the alias so a new snapshot date doesn't require a card refresh.
_DATED_SNAPSHOT_SUFFIX_RE = re.compile(r"-\d{8}$")


def _strip_dated_snapshot_suffix(model_name: str) -> str:
return _DATED_SNAPSHOT_SUFFIX_RE.sub("", model_name)

if TYPE_CHECKING:
# Structural Protocol below describes the only attributes we touch on
# an Anthropic SDK ``Message`` — kept here so that ``anthropic`` does
Expand Down Expand Up @@ -172,14 +184,27 @@ def get(self, model_name: str, at: datetime | date) -> PriceCard:
component is used for lookup) or a ``date``. The returned card is
the one whose ``effective_from`` is the latest among cards ≤ ``at``.

Raises :exc:`PriceCardLookupError` if the model has no cards or
every card's ``effective_from`` is later than ``at``.
Lookup tries the model name as-given first; on miss, retries with
any trailing ``-YYYYMMDD`` snapshot suffix stripped. This lets the
YAML stay keyed on family aliases (``claude-haiku-4-5``) while
accepting the dated form (``claude-haiku-4-5-20251001``) that the
Anthropic SDK returns in ``Message.model``.

Raises :exc:`PriceCardLookupError` if neither form matches.
"""
query_date = at.date() if isinstance(at, datetime) else at
candidates = [
c for c in self.cards
if c.model_name == model_name and c.effective_from <= query_date
]

def _candidates_for(name: str) -> list[PriceCard]:
return [
c for c in self.cards
if c.model_name == name and c.effective_from <= query_date
]

candidates = _candidates_for(model_name)
if not candidates:
alias = _strip_dated_snapshot_suffix(model_name)
if alias != model_name:
candidates = _candidates_for(alias)
if not candidates:
raise PriceCardLookupError(
f"No price card for model {model_name!r} active on {query_date}"
Expand Down
41 changes: 41 additions & 0 deletions tests/test_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,47 @@ def test_query_before_first_effective_date_hard_fails(self):
self.table.get("haiku", date(2025, 12, 31))


class TestPriceTableLookupDatedSnapshotSuffix:
"""Anthropic SDK returns ``Message.model`` in the dated snapshot form
(e.g. ``claude-haiku-4-5-20251001``) even when the caller requested
the alias; the YAML is keyed on the alias. Lookup must accept both.
"""

def setup_method(self):
self.table = PriceTable(cards=[
_card("claude-haiku-4-5", 2026, 1, 1, in_p=1.0),
_card("claude-sonnet-4-6", 2026, 1, 1, in_p=3.0),
])

def test_dated_suffix_falls_back_to_alias(self):
c = self.table.get("claude-haiku-4-5-20251001", date(2026, 5, 28))
assert c.input_per_1m == 1.0

def test_alias_lookup_unchanged(self):
c = self.table.get("claude-haiku-4-5", date(2026, 5, 28))
assert c.input_per_1m == 1.0

def test_exact_dated_match_wins_over_alias_fallback(self):
# If someone adds a dated card explicitly, it takes precedence.
table = PriceTable(cards=[
_card("claude-haiku-4-5", 2026, 1, 1, in_p=1.0),
_card("claude-haiku-4-5-20251001", 2026, 1, 1, in_p=9.99),
])
c = table.get("claude-haiku-4-5-20251001", date(2026, 5, 28))
assert c.input_per_1m == 9.99

def test_unknown_alias_with_dated_suffix_still_hard_fails(self):
with pytest.raises(
PriceCardLookupError, match="claude-foo-9-9-20251001"
):
self.table.get("claude-foo-9-9-20251001", date(2026, 5, 28))

def test_non_dated_suffix_is_not_stripped(self):
# Bare 8-digit substring without leading dash → no normalization.
with pytest.raises(PriceCardLookupError):
self.table.get("claude-haiku-4-5.20251001", date(2026, 5, 28))


# ── compute_cost ──────────────────────────────────────────────────────────


Expand Down