Repository navigation
refactor(modelinfo): one function decides a cache price for a model - #860
Conversation
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 2 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | fresh | 2 of 2 | 1 of 2 | 05907c872ccb |
blocking |
Verdict: blocking
Findings
- dash/ui/app.js:6538 — the lower-tier ping ratios say 25x and 50x; the arithmetic gives 24x and 49x
- What fails: a user reads the keep-alive settings page (and
docs/how-to/cache-keepalive.md:33) and is told a ping buys back "about 25 times" at a 0.05x read rate and "about 50 times" at 0.025x. The page's own formula for the 0.1x anchor is net saving over ping cost: (1.25 − 0.1) / 0.1 = 11.5. The same formula gives (1.25 − 0.05) / 0.05 = 24 and (1.25 − 0.025) / 0.025 = 49. The shipped numbers are the gross write/read ratios (1.25 / 0.05 = 25, 1.25 / 0.025 = 50), so the sentence mixes two different measures: net for 0.1x, gross for the lower tiers. It overstates the saving by about 4% and 2%. - Proof:
python3 -c "print((1.25-0.05)/0.05, (1.25-0.025)/0.025, (1.25-0.1)/0.1)"→24.0 49.0 11.5. The PR description itself says "gives about 24x and 49x for the lower tiers", which disagrees with the text it ships. - Fix: change "about 25 times" to "about 24 times" and "about 50 times" to "about 49 times" in
dash/ui/app.js:6538anddocs/how-to/cache-keepalive.md:33.
- What fails: a user reads the keep-alive settings page (and
Non-blocking
internal/modelinfo/table.go:118— a negative file-levelcache_write_fracis not validated.ParseTablevalidatescache_read_frac > 0but only replacescache_write_frac == 0with the default. Before this PR a negative value produced a negative write rate; nowWriteFrac > 0is false, so the value is silently ignored and the per-model default applies. Both old and new behavior are garbage on invalid input; new is less garbage. Suggest validatingcache_write_frac > 0likecache_read_frac. Note the Python port (kv_ttl_cost_model.py:498) also passes a negative through, and the parity test does not cover a negative frac.- The
ratefillModels/ratefillCheck/ratefillFblock is copied verbatim into 7 test packages (about 90 lines each). A shared helper in one internal package would keep the model list in one place when a new tier ships. Test-only; acceptable as is. deploy/harbor/kv_ttl_price_parity_test.go:95— theknownWriteDiffbranch checks only Go's side (write == input for non-premium families) and skips Python's field-3 value entirely. A Python regression in the write fill for those families would not be caught. The divergence is pinned and named for a human, so this is fine.- PR open question 1 (the
agentCacheReadPerMTokliterals stay, to avoid a 1-ulp change) is the right call; the fixture test pins it.
What I checked
- Read the full diff
c2a06dd5..HEAD(26 files) and the code around each change:internal/modelinfo/modelinfo.go,table.go,cachefrac.go,litellm/litellm.go,kvcache/pricing.go,components/offload/extract_econ.go,cheapmodel/usage.go,dash/keepalive.go,dash/ui/app.js,docs/how-to/cache-keepalive.md,deploy/harbor/kv_ttl_price_parity_test.go,deploy/harbor/kv_ttl_cost_model.py. - Proved the refactor is behavior-preserving at each migrated call site, old code vs new:
Table.Price: read tier still derives from the QUERIED model when the file sets nocache_read_frac; write tier still derives from the entry's own match family (chargesCacheWritePremium(match)), file multiple wins. The removedreadDerivedflag is replaced byReadFrac == 0, same semantics.litellm.fetch:p.Resolved(k)equals the old twoif == 0fills against the same keyk.kvcache.FromPrice: caller read multiple wins, unset follows the model; write multiples carry defaults; the 1h branch and theWrite5m-ratio fallback are unchanged.agentRates,HaikuPricing,dash/medianPingUSD: stated tier wins, then per-model multiple; empty model keeps the Anthropic constants. Verified each old expression against the new one term by term.
- Traced every reader of
Price.CacheRead/CacheWriteoutsidemodelinfo(dash, proxy, keepalive, kvcache, components): all obtain prices through aPricer(TableorLiteLLM), both of which now resolve, so no reader sees a blank tier.Ctx.SelfRatescomes fromHandler.selfRates, alsoPricer-backed. - Checked the new
ReadFrac/WriteFrac/Write1hFracfields arejson:"-", so no wire-format change; no YAML or round-trip serialization ofPriceexists. - Spot-checked the fixtures against hand-computed values (
internal/modelinfo,kvcache,components/offload,dash,cheapmodel,litellmtestdata/ratefill_before.json): opus-5-5 read 2.5e-7 (0.05x), opus-5 5e-7, gemini write at 1.0x,writefrac: 1.5honored on Anthropic and ignored on gemini, statedcache_write1.4 beating the file multiple, haiku 1/5/1.25/0.1, median ping 0.032/0.016/0.008. All match. - Verified the fixture tests catch dropped or added rows (length check), and that the test helpers they rely on exist and compile by inspection (
pythonBinFor,kaExpiry,newKAFixture,kaFixture.db,Filter.TenantAll). - Checked the harbor parity test against the Python port's actual behavior (
from_operator_file,for_model, read re-derivation at lookup, the write-frac divergence) for every id in its list, including[1m]suffixes and the empty model. - Live tests were not run (not approved for this review); all verification above is by reading.
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 1 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | fresh | 1 of 2 | 1 of 2 | 05907c872ccb |
blocking |
Verdict: blocking
Findings
- internal/modelinfo/table.go:128 — a negative
cache_write_fracchanges behavior, and the PR says "No behavior change"- What fails: an operator file with
cache_write_frac: -0.5on an Anthropic-matching entry. On base c2a06dd, ParseTable filledp.CacheWrite = Input * (-0.5)— a negative rate. NowWriteFracis-0.5, andCacheWrite5mFor(modelinfo.go:96) only uses it whenWriteFrac > 0. So it falls through toCacheWriteFracFor(model)= 1.25. The priced cache-write changes sign and size.ParseTablerejectscache_read_frac <= 0(table.go:94) but still accepts a negativecache_write_frac, so this input reaches the new path. No fixture covers it — the ratefill docs use 0.2, 0.3, 1.1 and 1.5 only. - Proof: base table.go, old lines 129-137:
frac := f.CacheWriteFrac; ... p.CacheWrite = p.Input * frac— no sign check onfrac. New modelinfo.go:92-99:if p.WriteFrac > 0 { return p.Input * p.WriteFrac }; return p.Input * CacheWriteFracFor(model). - Why it matters: the repo rule says a behavior change for an edge case needs a human's yes, named at the top of the PR. The PR claims "No behavior change". This input is one. The old output was also wrong (a negative rate), so the new behavior is defensible — but a human should decide, or the input should be rejected.
- Fix: validate next to the
cache_read_fraccheck in ParseTable: rejectcache_write_frac < 0(0 keeps its "use the default" meaning). Then every accepted file prices exactly as before, and the claim holds.
- What fails: an operator file with
Non-blocking
- Same gate change, feed side: a negative stated cache rate in the LiteLLM feed (
cache_read_input_token_cost: -x) was kept as-is on base (fill only when== 0); now> 0replaces it with the model multiple. Implausible feed data; noting for completeness. - The PR body says "about 24x and 49x" for the lower read tiers; app.js and cache-keepalive.md say "about 25 times" and "about 50 times". The pure ratio is 25 and 50; a ping's own input and output tokens pull it slightly below. Both say "about". Pick one pair of figures so the docs agree with the PR.
- deploy/harbor/kv_ttl_price_parity_test.go:87 computes
knownWriteDifffrom the QUERIED id, but the Go Table decides the write frac from the ENTRY's match. They agree for every id in this test. A future id that crosses families (a claude-named id hitting a non-Anthropic match) would be skipped wrongly. Test-only; fine to leave. - I could not run Go on this machine (live tests not approved). I verified parity by hand instead: I recomputed fixture rows in all 7 packages against the new code paths, including
from/noinput/set,from/readonly/readset,list/set/claude-opus-5-5,agent/noinput,agent/blank/gemini-3-flash-preview,ping/1.25/claude-fable-5-1,ping/0.4/azure/gpt-5.5,litellm/blank/aws/claude-opus-5-5and thetable/*matrix. Every value matched.
What I checked
- Read CLAUDE.md, docs/design.md, and the full diff (26 files, one commit 05907c8).
- Traced the old fill at each of the 7 converted sites (ParseTable, Table.Price, LiteLLM fetch, kvcache.FromPrice, agentRates, HaikuPricing, medianPingUSD) against the new
CacheReadFor/CacheWrite5mFor/CacheWrite1hFor/Resolvedrule, for valid inputs: stated rate, operator frac, unset frac, blank tier, blank input, empty model, prefix traps,[1m]suffixes, vendor prefixes, non-Anthropic families, GPT-5.5 vs 5.6. No divergence on valid inputs. - Checked that
ReadFrac/WriteFrac/Write1hFracare set only inside modelinfo, kvcache.FromPrice (on a copy), dash medianPingUSD (on a local unit price) and tests, and that both Pricer sources (Table, LiteLLM) returnResolvedprices — socomponents.TokenRates,SelfRatesand the dash price paths never see a frac field. The new fields carryjson:"-", so the wire form is unchanged. - Checked
Zero(),Cost,CostWithCacheWrite1hand theWrite1h < Write5mclamp for unchanged semantics. - Verified the parity-test scaffolding exists and is coherent:
pythonBinFor,kaExpiry,newKAFixture, the PythonPriceBook.from_operator_file, and the pinned Python/Go write divergence. - Checked the doc text arithmetic: 1.25/0.05 = 25, 1.25/0.025 = 50.
…856) Price.CacheReadFor, CacheWrite5mFor and CacheWrite1hFor apply one rule: a stated rate, then the operator's multiple, then the model's multiple. ParseTable, Table.Price, the LiteLLM feed, kvcache.FromPrice, agentRates, HaikuPricing and the dash median ping all call them. No product price changes. Two changes on invalid input or offline tools: - A negative cache_write_frac now fails to load (Go and Python). - kv_ttl_cost_model.py fills a blank cache_write per family, as Go does. The agent-class constants stay literals. A test pins them to the function. A fixture made on origin/main c2a06dd pins every caller's prices. Assisted-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
05907c8 to
d23257c
Compare
|
Finding 1 (negative |
|
Finding 2 (ratio text): fixed in d23257c. Both places use the net formula (1.25 - r) / r: 11.5x, 24x, 49x. The PR body uses the same numbers. |
|
LiteLLM negative stated rate: fixed in d23257c. A negative stated cache rate is kept as on main, so no price changes for valid or invalid data. |
|
Parity test: the |
|
Fixture block: moved to |
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 1 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | resumed | 1 of 2 | 2 of 2 | d23257c90662 |
approve |
Verdict: approve
Findings
None.
Round-1 findings, re-checked
- Mine, negative
cache_write_frac(table.go) — fixed.ParseTablenow rejectscache_write_frac < 0(table.go:97-99), with0keeping its "default" meaning.negfrac_test.gocovers both cases. The Python reader rejects it too (kv_ttl_cost_model.py:528), with a test intest_cache_read_tiers.py. The PR body names the change and records the owner's decision. This is the fix I proposed. - Mine, non-blocking, LiteLLM negative stated rate — fixed.
litellm.go:220-226restores a negative stated rate afterResolved, so negative feed data prices exactly as on base. The PR body says so. - Mine, non-blocking, parity-test skip — fixed. The
knownWriteDiffbranch is gone. Python now fills a blankcache_writeper family, and the test compares all 5 fields for every id. - Mine, non-blocking, PR-body vs docs figures — fixed. Both places now say 24x and 49x.
- Reviewer 2's finding, 25x/50x vs 24x/49x — fixed, and their math is right. I checked it myself: the 11.5 anchor is the net formula (1.25 − 0.1) / 0.1, so the consistent pair is (1.25 − 0.05) / 0.05 = 24 and (1.25 − 0.025) / 0.025 = 49. app.js and cache-keepalive.md now carry 24x and 49x.
- Reviewer 2's non-blocking, 7x copied fixture block — fixed. The model list and check moved to
internal/modelinfo/ratefilltest; each package keeps only its own test. I checked that no non-test file imports it.
Non-blocking
deploy/harbor/kv_ttl_cost_model.py:363—charges_cache_write_premiumstrips a whole-id bracket group where Go does not: the id[claude]reads non-Anthropic in Python (the regex removes the whole id) and Anthropic in Go (stripVariantkeeps a group at position 0). Pathological id, offline research tool, not covered by the parity list. Fine to leave.
What I checked
- Last review: round 1, 05907c8, 2026-11-02T16:17:02Z.
- New commits read: 1 — d23257c "refactor(modelinfo): one function decides a cache price for a model (#856)" (the PR was squashed;
git diff 05907c87..HEADcovers the delta, 20 files). - Read in full: 2 reviews, 5 comments, 0 inline comments (from
.review-in-reviews.txt,.review-in-comments.txt,.review-in-inline.txt). - New items since my last review: the squash commit d23257c; 5 author comments at 16:54 (one per finding); the updated PR body. I checked each reply against the diff, and the PR body against the code.
- The delta, read around each change:
table.gonegative-frac validation: ordering is right (reject< 0before the== 0default swap);negfrac_test.gopins both cases.litellm.gonegative-rate restore: equal to base, where a fill fired only on== 0, so a negative stated rate survived.kv_ttl_cost_model.py: hand-traced_gpt_56_or_laterandcharges_cache_write_premiumagainst Go'sGPT56OrLaterandIsClaudecase by case (no digits, major ≠ 5,.then no digits, minor ≥ 6, uppercase ids,[1m]suffixes,rfindvsLastIndex). They agree on every real id. The write fill uses the entry's own match, as Go does. Theor-based default maps 0 and −0.0 to the default, which matches Go's== 0check.- Parity test without the skip: recomputed the previously divergent ids by hand (
gemini-3-flash-previewandpremiumat 1.0x underwritefrac: 1.5,azure/gpt-5.5at 1.0x,claude-opus-5-5at 1.5x) — Python and Go now agree. ratefilltestextraction: mechanical; no leftoverratefillModels/ratefillCheck/ratefillFreferences; fixtures unchanged from round 1, so the parity pins still hold.TestAgentClassRatesMatchPrice(both packages): the 1e-12 tolerance is sound. Go constant arithmetic is exact, so the literal is 0.3 while the run-time product is 0.30000000000000004 — a difference of about 4e-17.claude-sonnet-4-5reads 0.1x and writes 1.25x, matching both literals.- app.js and cache-keepalive.md: net-formula figures 11.5x / 24x / 49x, consistent with the PR body.
- Live tests were not run (not approved). All verification is by reading and hand computation.
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 2 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | resumed | 2 of 2 | 2 of 2 | d23257c90662 |
approve |
Verdict: approve
Findings
None. Every round-1 finding is fixed, and the new commit introduces no defect I can show.
Earlier findings
- Mine, round 1 — the 25x/50x ping ratios. Fixed.
dash/ui/app.js:6538anddocs/how-to/cache-keepalive.md:33now say 24x and 49x. Both use the page's own net formula (1.25 − r) / r, which gives 11.5, 24 and 49. The PR body states the same three numbers. - Reviewer 1, round 1 — a negative
cache_write_fracchanged behavior while the PR said "no behavior change". Fixed, and I checked the fix myself.ParseTable(table.go:97) now rejectscache_write_frac < 0; 0 keeps its "use the default" meaning.negfrac_test.gocovers both cases. The Python reader rejects it too (kv_ttl_cost_model.py:528), with a test intest_cache_read_tiers.py. The PR body now names this change at the top. - Reviewer 1, non-blocking — a negative stated cache rate in the LiteLLM feed. Fixed.
litellm.go:220restores a negative stated rate afterResolved(k), so the feed prices invalid data exactly as main did (fill only when the tier is 0). I traced old vs new: the restore is unconditional and uses the raw feed value, same as base. - Mine and reviewer 1, non-blocking — the fixture block copied into 7 packages. Fixed. The model list and the check now live in
internal/modelinfo/ratefilltest; each package keeps only its test and fixture. No non-test file imports it. - Mine, non-blocking — the parity test skipped Python's write tier for non-premium families. Fixed by making the two sides agree instead. See below.
What I checked in the new commit
- The Python port's new family rule, line by line against Go:
charges_cache_write_premiummatchesIsClaude("claude" or "anthropic" substring, lowercased,[...]variant stripped) ORGPT56OrLater._gpt_56_or_latermatches Go'sGPT56OrLater: last "gpt-" token, major ≠ 5 → major > 5; major 5 needs ".minor" with minor ≥ 6; no digits → false; "gpt-5." (dot, no digit) → false on both.- The blank write fill is
cache_write_frac_for(match, file_frac): the file multiple for a premium match, 1.0 otherwise. That is exactly Go'sWriteFrac = f.CacheWriteFrac; if !chargesCacheWritePremium(match) { 1.0 }, and Go's lookup always uses that frac now that negatives are rejected. The parity test compares all 5 fields for every id; I walked each id in its list (gemini, premium, azure/gpt-5.5, gpt-5.6-luna, the Claude ids,[1m]suffixes, the empty model) and both sides agree.
- The negative-frac rejection does not change any accepted file: base accepted
cache_write_frac < 0only by producing a negative rate, which the PR body now names as an owner decision. - The new pinning tests (
ratefill_agent_test.goin offload and proxy): the 1e-12 tolerance correctly covers the gap between the constant product (exact 0.3, untyped constant arithmetic) and the run-time product (0.30000000000000004, 4e-17 off). The proxy test pins only the read literal because proxy has no write literal; offload pins both. ratefilltest.Checkkeeps the relativetestdata/ratefill_before.jsonpath, which resolves per package undergo test— same behavior as the per-package copies it replaces.- The parity test still compiles after removing
knownWriteDiff:mathandmodelinfoare both still used. - Re-read the updated PR body: it names both edge-case changes (negative multiple rejected, Python tool write fill) at the top, with the owner's decision, as the repo rule requires.
Non-blocking
internal/modelinfo/ratefilltestis a normal package that importstesting. Precedent exists (dispatch/dispatchtest); nothing outside tests imports it.- The LiteLLM negative-rate restore has no direct test; the fixtures use non-negative costs only. The code is three lines and restores base behavior; a fixture row with a negative cost would pin it.
- Pre-existing, unchanged: the parity test's doc set has no entry with
in: 0, where Go'sCacheWrite1h()(input × 2) and Python's_derive_write_1h(write_5m-scaled) would take different branches. Test-coverage note only.
What I checked
- Last review: round 1, 05907c8, 2026-11-02T16:14:41Z
- New commits read: 1 (
d23257c9"refactor(modelinfo): one function decides a cache price for a model (#856)") - Read in full: 2 reviews, 5 comments, 0 inline comments (from the three files)
- New items since my last review, and what I did about each:
- Commit
d23257c9: read the full diff (39 KB) and the code around each change; verified every fix listed above. - 5 comments by the author (fix notes for the 5 round-1 items): checked each claim against the code; all true.
- Updated PR body: read in full; the behavior-change naming and owner decisions are present.
- Commit
- Live tests were not run (not approved); all verification is by reading.
No behavior change. A table test per caller compares every price with a fixture made on origin/main c2a06dd. Data dependence: no change (prices and model ids only).
deploy/harbor/kv_ttl_cost_model.pynow fills a blankcache_writeper family, as Go does (1.0 for non-Anthropic families with no write premium, 1.25 for Anthropic and GPT-5.6+). No product code, dashboard or proxy runs this file, and it is not shipped. Only experiment numbers for non-Claude models change. The owner approved it.extract_econ.goandproxy/proxy.go. A test pins each to the new function within 1e-12. Computing them at run time moved0.3by 1 ulp. The owner approved it.Closes #856
Summary
Price.CacheReadFor,CacheWrite5mFor,CacheWrite1hFor.ParseTable,Table.Price, the LiteLLM feed,kvcache.FromPrice,agentRates,HaikuPricing,dash/keepalive.gomedian ping.What changed
internal/modelinfo: the three methods,Resolved,ReadFrac/WriteFrac/Write1hFraconPrice(not in JSON). ThereadDerivedflag is gone.dash/ui/app.js,docs/how-to/cache-keepalive.md).refactor-scan/lessons.md: "Centralize the decision, not the constant".How it was tested
ratefill_test.goin 6 packages. The model list and check helper are shared ininternal/modelinfo/ratefilltest. Fixtures come from c2a06dd.cache_write_fracin Go and Python.-race, at the final head:./internal/...,./kvcache/...,./components/offload/...,./cheapmodel/...,./deploy/..., full./dash/..., full./proxy/...pass.gofmt -lempty,go vet ./...clean, Python test OK.