feat(levelcode): Fable 5 and Fable 5.1 at Pro — lead with the frontier - #431
Merged
Merged
Conversation
…ead with the frontier Fable 5.1 (anthropic/claude-fable-5.1) joins the catalog and Fable 5 moves from max to pro, so both reach Pro, Pro+, Max and Ultra. GPT-6 Astra stays at pro_plus. The decision, recorded so it is not mistaken for drift: on 2026-09-08 the owner chose reach over rec #3 ("~19 turns a month feels broken"). As of that date no business-approved editor ships a Fable/Astra-class model, and the window to lead with one is now. The dollar budget keeps this a UX decision rather than a margin one — a pricey pick drains the allowance faster and margin stays 1 - CREDIT_COGS_RATIO — so the mitigation is honesty, not gating: the pricing-page copy now names the models and prints the real counts (~20 on Fable 5.1 for Pro), and a new spec pins those printed counts to turns_for so the page can never promise turns the budget does not buy. Fable 5.1 is NOT a price twin of Fable 5. Same $10/$50, but cached reads are $0.25/M against $1.00, so a reference turn is cheaper — 12.93x Kimi against 13.34x — and the monotonic-multiplier invariant puts it BEFORE Fable 5 and Astra, not beside them. Confirmed against the OpenRouter models API on 2026-09-08 (listed 2026-09-01): 1M ctx, image + file. The routing ceiling from #428 holds for both: every Fable 5.1 endpoint is at exactly $10/$50; Fable 5 has five there and one (Google EU, $11/$55) the ceiling now excludes. Verified: full suite 1033 examples, 0 failures; rubocop clean on every changed file. Each change reverted in turn fails only its own examples: catalog reverted -> 9; only Fable 5's tier flipped back to max -> exactly the 5 entitlement examples; pricing copy perturbed (~20 -> ~25) -> exactly the advertised-count guard.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The new/updated specs have a couple of correctness/maintainability gaps (format-parsing fragility and incomplete roster confirmation coverage) that should be fixed to keep the test suite reliable.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the Levelcode model catalog and plan messaging to make Anthropic’s Fable models available starting at the Pro tier, while ensuring the pricing page’s advertised “turn counts” remain mechanically consistent with Levelcode.turns_for.
Changes:
- Add
anthropic/claude-fable-5.1to the confirmed OpenRouter catalog atmin_tier: :pro, and moveanthropic/claude-fable-5from:max→:pro. - Update
Levelcode::PLANS[:features]strings (pricing page output) to explicitly include Fable 5.1 turn counts and model-line naming. - Add/adjust specs to pin advertised turn counts to
turns_forand update entitlement expectations for Pro/Pro+/Max.
File summaries
| File | Description |
|---|---|
| spec/models/levelcode_plans_spec.rb | Adds a spec to validate advertised plan turn counts against turns_for; updates entitlement expectations to include Fable at Pro. |
| spec/models/levelcode_model_catalog_spec.rb | Extends catalog specs for Fable 5.1 context/multiplier/tier entitlement behavior. |
| lib/levelcode.rb | Updates pricing features strings surfaced by GET /pricing to include Fable 5.1 counts and model naming. |
| app/services/levelcode/model_catalog.rb | Adds Fable 5.1 catalog row and moves Fable 5 to Pro tier, preserving monotonic multiplier ordering. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… a hand-kept list From review on #431: Fable 5.1 was not in the list of ids the "carries a confirmed price on every roster engine" example iterates, so a status regression on it would go unnoticed there. True, and it was not the only one. The list had nine ids and the roster has eleven — GPT-6 Astra, added in #428, was missing too. Rather than add two ids and wait for the next model to be forgotten, the example now rejects every row whose status is not :confirmed and expects none. That also widens what it can see: the old second half looked only for :assumption, so a mistyped or missing status on an unlisted row matched neither check. That matters because allowed_models selects on `status == :confirmed`: any other value makes a model silently unselectable and unbillable. Verified by breaking the catalog and running the example before and after: hand-kept list roster-wide Fable 5.1 status mistyped passes FAILS GPT-6 Astra status key removed passes FAILS Fable 5 mistyped (was on the list) fails fails a row staged as :assumption fails fails The failure now names the row: expected [], got ["anthropic/claude-fable-5.1"].
Two findings from review on #431, and a third of the same kind found while fixing them. The example read its numbers with `line[/…/, 1].delete(',').to_i`. If the copy changed shape, a missed pattern was a nil: NoMethodError for the Kimi figure, and for the other two a count that silently read as 0. The line is now matched against one pattern with named captures, and that match is asserted before anything is read from it. The assertions also carried custom messages — "orbits_pro: Opus 5" — which replace RSpec's own. So a genuine drift printed the plan and the model and neither number. They are gone; the two exact counts are compared as one hash, whose diff names the model and shows both sides. And the example looped over the plans inside a single `it`, so the first plan to fail hid the rest. There is now one example per plan. What it prints, before and after, for the same three breaks: copy reshaped (Kimi) NoMethodError: undefined method 'delete' for nil -> expected "2,000 credits/mo · ~260 turns on Kimi · …" to match /…/ copy reshaped (Opus) orbits_pro: Opus 5 -> expected "… ~39 with Opus 5 …" to match /…/ Pro advertises ~25 orbits_pro: Fable 5.1 -> expected: {fable: 20, opus: 39} got: {fable: 25, opus: 39} On the wording: the reviewer read `it 'match what turns_for computes'` as ungrammatical. RSpec prints it joined to its describe — "the turn counts … match what turns_for computes" — where the plural is correct and "matches" would not be. But an `it` that does not read on its own is a fair complaint, so the subject is now singular: "the pricing line advertises the turns Pro's budget buys". Also verified: a drift on Max alone fails only Max's example; the rounded Kimi figure off by more than 1% fails; and a catalog price change — Fable 5.1's cached read to $1.00 — fails all four plans, each showing what the budget buys against what the page says.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
anthropic/claude-fable-5.1joins the catalog atmin_tier: :pro,:confirmed— OpenRouter models API, 2026-09-08 (listed 2026-09-01): $10/M in · $50/M out · $0.25/M cached · 1M ctx · image + file.anthropic/claude-fable-5moves:max→:pro.:pro_plus— not part of this call; it's one token when it is.PLANS[:features], surfaced byGET /pricing) now names the models and prints honest counts. Exact strings:2,000 credits/mo · ~260 Kimi turns · ~39 on Opus 5 · ~20 on Fable 5.1Kimi K2.7 Code + Opus 5 + Fable 5 / 5.14,000 credits/mo · ~520 Kimi turns · ~78 on Opus 5 · ~40 on Fable 5.1Kimi K2.7 Code + Opus 5 + Fable 5 / 5.1 + GPT-6 Astra6,000 credits/mo · ~780 Kimi turns · ~117 on Opus 5 · ~60 on Fable 5.110,000 credits/mo · ~1,300 Kimi turns · ~195 on Opus 5 · ~100 on Fable 5.1turns_for— exact for Opus 5 and Fable 5.1, within 1% for the rounded Kimi figure — so a catalog price change or a hand edit can't leave the page promising turns the budget no longer buys. (Astra was never named on the pricing page after feat(levelcode): GPT-6 Astra at Pro+, and a price ceiling on every paid OpenRouter route #428; Pro+/Max/Ultra now carry it.)The decision, on the record
Owner's call, 2026-09-08, reaching over rec #3 ("~19 turns feels broken"): as of that date no business-approved editor ships a Fable/Astra-class model, and the window to lead with one is now. The dollar budget (
CREDIT_COGS_RATIO) keeps this a UX decision, not a margin one — a pricey pick drains the allowance faster; margin is unchanged. So the mitigation is honesty rather than gating: the counts are printed where people choose, and pinned by a spec.What a user actually gets per month:
Fable 5.1 is not a price twin
Same $10/$50 as Fable 5, but cached reads are $0.25/M vs $1.00 → 12.93× a Kimi turn against 13.34×. The monotonic-multiplier invariant orders by cost, so it sits before Fable 5 and Astra rather than beside them. The multiplier table in the spec says so.
The #428 ceiling still holds
Every Fable 5.1 endpoint (Anthropic, Azure, Bedrock, Google) is at exactly $10/$50. Fable 5 has five there and one — Google EU at $11/$55 — that the ceiling now excludes rather than absorbs.
Re-checked against the live API on 2026-10-03, since this bills Pro users at the catalog rate and the row was confirmed 25 days earlier: Fable 5.1 ($10 / $0.25 cached / $50), Fable 5 and Opus 5 are all unchanged, and the adapter sends
max_price: { prompt: 10.0, completion: 50.0 }for both Fable rows.Verification
developis merged in (39 commits, none touching these files).:max→ exactly the 5 entitlement examples; pricing copy perturbed (~20→~25) → exactly Pro's pricing-line example.Review
Three comments, all on the specs this PR added. Each was checked by breaking the code before and after the fix.
Fable 5.1 was missing from the "every roster engine" list. Right, and so was GPT-6 Astra (feat(levelcode): GPT-6 Astra at Pro+, and a price ceiling on every paid OpenRouter route #428): the list had nine ids, the roster eleven. The example now asks the roster itself — every row whose status is not
:confirmed— so there is no list to forget. A mistyped or missing status on either model used to pass; it now fails and names the row.The pricing-line parse raised
NoMethodErrorif the copy changed shape. Right. The line is matched against one pattern with named captures, and that match is asserted before anything is read. The custom failure messages also turned out to be hiding RSpec's own, so a real drift printed neither number; the two exact counts are now compared as a hash, and there is one example per plan instead of a loop that stopped at the first failure.NoMethodError: undefined method 'delete' for nilexpected "2,000 credits/mo · ~260 turns on Kimi · …" to match /…/orbits_pro: Fable 5.1expected: {fable: 20, opus: 39} got: {fable: 25, opus: 39}"match" vs "matches". Not changed to "matches": RSpec prints the description joined to its
describe— "the turn counts … match what turns_for computes" — where the plural is correct. But anitthat does not read on its own is a fair point, so the subject is now singular: "the pricing line advertises the turns Pro's budget buys".Not in this PR
Editor companion: levelcodeai/levelcode#91 now also carries explicit caps rows for Fable 5 / 5.1 (both were getting the heuristic's 200k window against a real 1M).