Story-2969 Walkthrough

Change: model-price churn is now one committed command instead of a scratch script. uv run models refresh pulls OpenRouter prices and Artificial Analysis intelligence scores, surgically updates model-pricing.yaml, and prints the exact diff. uv run models drift renders the same comparison without writing; CI runs it as an advisory warning on trusted post-merge pushes because an external vendor price change must not block unrelated work, and pull-request code must never receive the Artificial Analysis API key.

The command informs; it never crowns. Registry aliases, agent seats, and the D6 gate are untouched. rank, what-if, and automatic seat re-pointing remain Stage 2 or explicitly out of scope.

The maintenance loop

OpenRouter /api/v1/models ── per-token price × 1,000,000 ─┐
                                                          ├─ compare existing tracked rows
Artificial Analysis ── max INT per normalized model slug ─┘
    │
    ├─ models drift    → readable diff + non-zero on divergence; NO write
    └─ models refresh  → same diff, then surgical scalar-line rewrite

The writer deliberately does not use yaml.safe_dump. It rewrites only targeted numeric scalar lines, so the file header, ordering, comments, and hand-written structured_note provenance survive byte-for-byte.

The tokenizer tax is data now

tokens_per_text_ratio defaults to 1.0. Claude 4.7+ rows carry 1.30, so refresh and drift show the number a seat reviewer actually compares:

ModelList input $/M tokensRatioEffective $/M of the same text
Claude Sonnet 52.001.302.60
Claude Opus 5 / Opus 4.8 / Opus 4.75.001.306.50
Claude Fable 510.001.3013.00

Runtime token-cost functions are intentionally unchanged: providers already return the actual tokenizer-produced token count. Applying 1.30 there would double-count the tax. The ratio exists only for same-document-text comparison in model operations.

The route wall

Pricing key shapeOpenRouter prices?AA score?
vendor/model — gatewayYes, exact pinned id onlyYes, max per slug
vendor:model — provider directNoYes, route-independent model score
bedrock:…NoNo — row untouched

OpenRouter ids beginning with ~, and rows with non-positive prompt/completion prices, are ignored. Prices use decimal rounding to six places, preventing values such as 0.09999999999999999 from creating permanent drift.

Live proof and idempotence

uv run models refresh
→ corrected live gateway price/cache drift and AA scores
→ preserved every structured_note and the file header

uv run models refresh
→ model-pricing.yaml is current (no drift).
→ exit 0

uv run models drift
→ model-pricing.yaml is current (no drift).
→ exit 0

uv run pytest tests/test_model_ops.py -q
→ 14 passed

uv run ruff check scripts/model_ops.py tests/test_model_ops.py
→ All checks passed!

uv run ruff check agents/ tests/ scripts/
→ All checks passed!

uv run pytest tests -q
→ 319 passed

Not included