Skip to content

feat(levelcode): Fable 5 and Fable 5.1 at Pro — lead with the frontier - #431

Merged
ndemianc merged 4 commits into
developfrom
feat/fable-to-pro
Oct 3, 2026
Merged

ndemianc merged 4 commits into
developfrom
feat/fable-to-pro

Conversation

@ndemianc

@ndemianc ndemianc commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

What

  • anthropic/claude-fable-5.1 joins the catalog at min_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-5 moves :max → :pro.
  • GPT-6 Astra stays at :pro_plus — not part of this call; it's one token when it is.
  • The pricing page (PLANS[:features], surfaced by GET /pricing) now names the models and prints honest counts. Exact strings:
Plan Line 1 Models line
Pro 2,000 credits/mo · ~260 Kimi turns · ~39 on Opus 5 · ~20 on Fable 5.1 Kimi K2.7 Code + Opus 5 + Fable 5 / 5.1
Pro+ 4,000 credits/mo · ~520 Kimi turns · ~78 on Opus 5 · ~40 on Fable 5.1 Kimi K2.7 Code + Opus 5 + Fable 5 / 5.1 + GPT-6 Astra
Max 6,000 credits/mo · ~780 Kimi turns · ~117 on Opus 5 · ~60 on Fable 5.1 same
Ultra 10,000 credits/mo · ~1,300 Kimi turns · ~195 on Opus 5 · ~100 on Fable 5.1 same

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:

Pro Pro+ Max Ultra
Fable 5.1 20 40 60 100
Fable 5 19 39 58 97
Opus 5 39 78 117 195
Kimi K2.7 260 521 782 1303

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

  • Full suite 1077 examples, 0 failures · rubocop clean · brakeman 0 warnings. develop is merged in (39 commits, none touching these files).
  • Each change reverted in turn fails only its own examples: catalog reverted → 12; only Fable 5's tier flipped back to :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 NoMethodError if 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.

    break before after
    copy reshaped NoMethodError: undefined method 'delete' for nil expected "2,000 credits/mo · ~260 turns on Kimi · …" to match /…/
    Pro advertises ~25 orbits_pro: Fable 5.1 expected: {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 an it that 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).

…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.
Copilot AI lite review requested due to automatic review settings September 8, 2026 16:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.1 to the confirmed OpenRouter catalog at min_tier: :pro, and move anthropic/claude-fable-5 from :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_for and 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.

Comment thread spec/models/levelcode_model_catalog_spec.rb
Comment thread spec/models/levelcode_plans_spec.rb Outdated
Comment thread spec/models/levelcode_plans_spec.rb Outdated
… 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.
@ndemianc
ndemianc merged commit 9298956 into develop Oct 3, 2026
3 checks passed
@ndemianc
ndemianc deleted the feat/fable-to-pro branch October 3, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants