Skip to content

Claude Fable 5 and 5.1 on annapolis, via a second serving path (#156) - #157

Open
jonfroehlich wants to merge 6 commits into
mainfrom
feat/fable-leg-prep-156
Open

Claude Fable 5 and 5.1 on annapolis, via a second serving path (#156)#157
jonfroehlich wants to merge 6 commits into
mainfrom
feat/fable-leg-prep-156

Conversation

@jonfroehlich

Copy link
Copy Markdown
Member

Closes item A of #156. Adds a first-party Anthropic serving path to ClaudeDetector, runs both Fable ids on annapolis, and publishes/registers/documents the results.

Why a second serving path

Vertex gates the whole Fable family behind a project-level publisher data-sharing setting we do not control. Probed 2026-09-04 and again 2026-09-05: both Fable ids return 403 there, claude-opus-5 returns 200, and a deliberately misspelled claude-fable-5.1 returns 404 — which is what proves the 403s resolved real ids behind a per-model gate rather than a blanket publisher block.

Anthropic's first-party API serves both ids today, so these legs ran there: a different account and a different rate card for the same weights.

Results

Annapolis, --claude-effort low, 125/125 panos each.

model P R F1 tp/fp/fn boxes/pano thinking tok cost
claude-fable-5 0.579 0.646 0.611 190/138/104 2.72 23,699 $18.47
claude-fable-5-1 0.637 0.585 0.610 172/98/122 2.28 254 $19.86
claude-opus-5 (low) 0.572 0.605 0.588 178/133/116 2.56 523 $8.94

Both displace claude-opus-5, which had displaced gemini-3.1-pro-preview (0.567) — the first time a general-purpose model has beaten Opus on this split. RampNet still leads by 0.228.

The two ids are the same detector at different operating points. 0.610 against 0.611 is nothing; 5.1 trades 0.061 recall for 0.058 precision and emits 0.44 fewer boxes/pano. Same shape as the effort finding in #123 and the Qwen 8B→32B inversion — the knob moves along the P/R curve, the ceiling does not move. Under this project's recall-first framing that argues for claude-fable-5, the older id.

#156's always-on-thinking cost premise is refuted. It predicted a band "wider than a flat 2x" because Fable cannot disable thinking. It is a flat 2x: ~33 thinking tokens/call on 5, ~0.35 on 5.1, against Opus-low's ~0.7. A 5-pano calibration predicted full-leg cost to within 1.5% on both ids.

The load-bearing design decision

The serving path is deliberately NOT in the detection cache key. It names which account answered, not what was asked. Putting it in signature() would fragment the cache along an axis that does not change the answer, and would orphan $28.82 of already-paid annapolis detections the moment any leg moved. test_claude_cache_key_is_frozen still passes on all four published Vertex legs, and a new test asserts the two paths hash identically.

Provenance goes to the usage log and the published file's new pins field instead — model_versions cannot stand in, because both paths report the bare model id.

This makes claude_serving_path the first roster pin that is not a signature field, which is documented where pins is defined. If the two paths ever do diverge in output, that is a finding to measure, not a cache key to add quietly.

Guards that caught real problems

  • The exporter wrote the bare model id — the exact failure publication_name's docstring warns about. leg_for() could not match a leg pinned on a knob the exporter had no flag for. Added and wired through.
  • test_each_published_file_names_the_leg_it_says_it_is assumed every pin is a signature field. The envelope now carries pins and the test looks there for pins the signature does not hold; legacy files never reach that branch.
  • test_every_registered_leg_has_published_detections refused the roster rows when I added them before the run. They land here with measured density.
  • PUBLISHED_LEGS keys grew a serving-path element: six rows come from Vertex and two do not, and a fixture that could not say which would hide the caveat these numbers most need to carry.

Also fixes a stale pointer — the docs told readers to run tests/test_claude_annapolis_leg.py, renamed to test_claude_published_legs.py some time ago.

Caveats that travel with the numbers

  • Serving path, recorded next to every number in docs/model_comparison.md. Whether the two paths return identical detections for one model id is untested; nothing here depends on it, since no model was run on both.
  • Reconciliation is weaker on this path. scripts/analysis/vertex_usage.py recovers server-side spend for Vertex and has no first-party equivalent, so for these legs the committed ledger is the only record.
  • 1 of 8 pooled splits, standing=False, excluded from pooled tables.
  • Prompt is fixed and Gemini-derived; JPEG q90 encoding and unpinned decoding carry over from the existing Claude legs.

Not done, deliberately

The expansion is not taken. Both legs clear the pre-registered 0.567 gate, so it is now a live decision rather than a hypothetical — ~$155/id for manual_gold alone, ~$390/id for all remaining splits. Priced in the doc, awaiting a call. Item B (GPT-6 Astra) is untouched and independent.

Verification

pytest -q — 1,495 passed, 1 skipped. Both new rows recompute from committed detections with no API key, no GPU and no .model_cache. export_model_cache.py --verify reports both legs score identically to the cache.

Total spend: $41.01, all recorded in analysis_out/usage_log.jsonl.

🤖 Generated with Claude Code (claude-opus-5[1m])

jonfroehlich and others added 6 commits September 4, 2026 15:05
…aks (#156)

Item A of #156 cannot run until the project enables data sharing for the
`anthropic` publisher, but three things that do not depend on that decision were
missing, and two of them would have cost a paid run to discover.

The probe that produced #156's 403/404/200 table existed only in a session
transcript. It is now a script, with the deliberately-misspelled `claude-fable-5.1`
kept as a CONTROL rather than dropped as a typo: a 404 on a spelling that does
not exist is what proves the 403s resolved real model ids behind a per-model
gate, instead of a blanket block on the publisher. `claude-opus-5` rides along as
the positive control, without which a wall of 403s is indistinguishable from
broken credentials. Re-run 2026-09-04: unchanged, gate still closed.

Two API differences in the Fable family would each have surfaced as a confusing
failure mid-leg:

- Forced tool use is gone -- `tool_choice` of `tool`/`any` returns a 400. The
  detector defaults to `auto` so a default run is fine, but `--claude-tool-choice
  forced` would have 400'd on every one of ~750 views with an error that names
  neither the flag nor the family. Refused at construction instead.
- Thinking cannot be disabled there, so the near-zero-thinking floor that makes
  our effort=low Opus leg cheap does not exist, and thinking bills against
  `max_tokens`. That was hardcoded at 4096; it is now `--claude-max-tokens`, and
  the truncation error says which of the two knobs actually helps on Fable.

`max_tokens` enters the detection signature only when it DEVIATES from the as-run
4096, the same rule image_format and temperature follow -- recording an unchanged
default would rewrite the key of every paid detection already in the cache.
test_claude_cache_key_is_frozen still passes on all four published legs.

Not done here, and not blocked on the same thing: pricing.py rows still need the
Vertex Anthropic rate card read by a human (the page does not render through
WebFetch, and this repo does not take a price from memory or an aggregator).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
Vertex gates the Fable family behind a publisher data-sharing setting that is
not ours to flip, so #156 item A now has a second candidate serving path:
Anthropic's own API. Same weights, different credential, different rate card.
Before any of that reaches the detector, the question the probe already answers
for Vertex has to be answerable for first-party too -- *is this id real, and is
it enabled for us?* -- because the alternative is discovering the answer
partway through a paid leg.

Generalized rather than duplicated. The probe's actual work -- call each id with
a 16-token budget, print the status verbatim, run a deliberate misspelling as a
404 control -- is identical on both paths; only the client construction differs.
So `--serving-path {vertex,anthropic}` selects it and the file loses the
`_vertex_` in its name, which was already the wrong name for what it does.
Nothing referenced it, so the rename costs nothing. Vertex stays the default:
it is what the four published Claude legs ran on.

Three things the first-party path needs that Vertex did not:

- Its status codes mean different things, and the docstring now says which.
  401 is a credential problem and nothing else; 400 is a request that was
  understood and refused, which here is either an empty credit balance or the
  Fable family's data-retention requirement -- both name themselves, hence
  printing the provider's message verbatim.
- The 404 control earns its keep twice over. On Vertex it separates "wrong
  spelling" from "not entitled"; on first-party it separates "not a model" from
  "not enabled for us", which otherwise share a status code.
- The key lives in the gitignored repo-root .env, so the probe now loads it the
  way vertex_usage.py does. That also fixes a latent gap on the Vertex side:
  GOOGLE_CLOUD_PROJECT in .env was previously invisible to this script, which
  read os.environ only.

A 200 now prints stop_reason and the token counts alongside it. That is not
decoration: Fable cannot disable thinking, so a 16-token budget may be spent
before any text is emitted. The call still proves reachability, and calling that
a failure would be wrong -- but a reader needs to see which of the two happened.
`--max-tokens` is a flag rather than an edit so bracketing Fable's thinking
floor, when someone gets to it, needs no change to this file.

Costs nothing to have run: every non-200 is free, and a 200 is a fraction of a
cent. No detection cache and no usage-log rows are touched -- this is a probe,
not a leg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
…156)

Vertex gates the Fable family behind a publisher data-sharing setting that is
not ours to flip. Anthropic's first-party API serves both ids today (probed
2026-09-05, e0ca0df), so `--claude-serving-path {vertex,anthropic}` picks the
account. `vertex` stays the default: it is what all four published Claude legs
ran on, and nothing about them changes here.

THE SERVING PATH IS NOT IN THE CACHE KEY, and that is the load-bearing decision.
It names which account answered, not what was asked -- the same model id on the
same views should return the same detections whoever bills for them. Putting it
in `signature()` would fragment the cache along an axis that does not change the
answer, and would orphan $28.82 of paid annapolis detections the moment any leg
moved. test_claude_cache_key_is_frozen still passes on all four published legs,
and a new test asserts the two paths hash identically. If the paths ever DO
diverge in output, that is a finding to measure and write down, not a cache key
to add quietly.

So provenance goes to the usage log instead, as a top-level `serving_path` field
beside `model_versions` -- same reasoning, it cannot be reconstructed later.
`model_versions` cannot stand in: both paths report the bare model id. Without
this field nothing distinguishes a Vertex leg from a first-party one afterwards.

Three behaviours are Vertex-specific and now say so:

- The transient-404 retry. That is entitlement propagating unevenly across Vertex
  serving backends; on first-party a 404 is permanent, and retrying it turns a
  mistyped --claude-model into 30 seconds of backoff before the same error.
- The regional-pricing warning. GOOGLE_CLOUD_LOCATION is shared with the Gemini
  legs, so it can be set to a region for reasons unrelated to Claude. On Vertex
  that really does make cost figures ~9% low; first-party has no endpoint
  dimension, and warning anyway trains the reader to ignore a real alarm.
- The credential error. Each path names what IT needs, and the first-party one
  names the prepaid trap: a valid key with no balance authenticates and then
  fails every call with a 400.

pricing.py gains both Fable ids, read off claude.com/pricing on 2026-09-05 --
the page that governs THIS billing path, which is also why the rows that were
blocked on an unrenderable Vertex rate card are no longer blocked. Both are
$10/$50, exactly 2x claude-opus-5. The cache rows are why reading rather than
deriving matters: Fable 5 reads cache at the standard 0.1x of input ($1.00), but
Fable 5.1 reads at $0.25/MTok, which is no multiple of its input rate. The usual
multipliers would have overstated 5.1's cache reads 4x.

The two Fable legs are deliberately NOT registered in roster.py yet. A row there
is a leg that has RUN and published detections -- three tests enforce that, and
they caught me adding the rows in advance. They land with the annapolis results.
Recorded next to the gap: `published_as` will have to name the serving path as
well as the effort, because claude_serving_path is the first pin that is not a
signature key and every pin's value must appear in the published name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
…legs (#156)

Five annapolis panos on each Fable id, first-party path, effort low. Purpose was
to replace the $19-$90 cost band in this issue with a measured number before
committing to 750 calls. Answer: the low end.

  claude-fable-5-1   2,033 in / 136 out per call   $0.0272/call   -> $20.40
  claude-fable-5     2,035 in / 98 out per call    $0.0253/call   -> $18.98

Input per call lands within 4 tokens of the claude-opus-5 measurement (2,031),
which is the cross-check that the rig is sending identical pixels down a
different serving path.

THIS REFUTES A PREMISE OF THIS ISSUE. #156 reasoned that because the Fable
family cannot disable thinking, the near-zero-thinking floor that makes our
effort=low Opus leg cheap does not exist. It does. claude-fable-5 spent 1,618
thinking tokens across 48 calls (~34/call) and claude-fable-5-1 reported zero
across 54; both sit near Opus's 110 output tokens/call. Always-on is adaptive,
not a floor. The earlier probe hinted at this on a toy prompt; this is the same
finding on real 1024x1024 views, which is what makes it load-bearing.

Accuracy is NOT reported as a result -- n=5, and the confidence intervals cover
most of the unit interval. It is recorded only because the direction is worth
knowing before the full run: identical recall (4/5 both), but 1 false positive
for 5.1 against 6 for 5. If that survives 125 panos it decides which leg is the
one to publish; at n=5 it decides nothing.

Cost of the calibration itself: $2.68 over 102 calls, against a ~$1.55 estimate
for 60. The overrun is bookkeeping, not the models. A first attempt was launched
with `set -x` in the wrapper, which would have traced the API key into a log
file; it was killed for that reason, but it had already completed both models
(60 calls, $1.58) -- killing a task does not un-spend it. The re-run then
re-called 42 of 60 because most of the first run's detections were not in the
cache when it started, a gap this commit does not explain and does not paper
over. The cache is verified intact now: 5 panos per model resolve at the keys
the detector computes today, so the full leg pays for 120 panos, not 125.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
Both clear the 0.567 gate and both beat claude-opus-5 at effort low (0.588),
making this the first time a general-purpose model has displaced Opus on
annapolis. The two are also indistinguishable from each other -- 0.610 vs 0.611
-- while sitting at visibly different operating points, which is the same shape
as this benchmark's effort finding (#123): the knob moves precision against
recall, not the ceiling.

  claude-fable-5-1   P 0.637  R 0.585  F1 0.610   172/98/122/15   $19.86
  claude-fable-5     P 0.579  R 0.646  F1 0.611   190/138/104/12  $18.47

Cost landed within 1.5% of the calibration projection on both legs, and thinking
held at the calibrated rate: 254 tokens over 720 calls for 5.1, 23,699 (~33/call)
for 5. The always-on-thinking cost premise this issue assumed is now refuted at
full scale on both ids.

Ledger rows committed before any write-up: token counts are the one artifact a
re-run cannot reproduce.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
Annapolis, first-party path, effort low, 125/125 panos each:

  claude-fable-5     P 0.579  R 0.646  F1 0.611  190/138/104  2.72 boxes/pano
  claude-fable-5-1   P 0.637  R 0.585  F1 0.610  172/98/122   2.28 boxes/pano

Both displace claude-opus-5 (0.588), which had displaced gemini-3.1-pro (0.567).
First time a general-purpose model has beaten Opus on this split. RampNet still
leads by 0.228.

The two are separated by 0.001 F1 -- nothing -- while sitting at visibly
different operating points: 5.1 trades 0.061 recall for 0.058 precision and
emits 0.44 fewer boxes/pano. Within this family the version is an
operating-point dial, the same shape as effort (#123) and the Qwen 8B->32B
inversion. Under this project's recall-first framing that argues for the OLDER
id, which is worth saying out loud because the reflex is the opposite.

Both are standing=False at 1/8 splits, and both clear the pre-registered 0.567
expansion gate -- so that expansion is now a live decision. It has NOT been
taken and is priced in the doc (~$155/id for manual_gold alone).

Publishing this took four guards to get right, and each caught something real:

- The exporter wrote the BARE model id, exactly the failure its own docstring
  warns about: leg_for() could not match a leg pinned on claude_serving_path
  because the exporter had no such flag. Added, wired into overrides, and
  documented as NOT a signature field -- it plays no part in finding the cache,
  only in resolving the leg's published name from the registry.
- test_each_published_file_names_the_leg_it_says_it_is assumed every pin is a
  signature field. That assumption is what claude_serving_path breaks by design,
  so the published envelope now carries `pins` and the test looks there for pins
  the signature does not hold. Legacy files lack the field and never reach that
  branch, since their pins are all signature fields.
- test_every_registered_leg_has_published_detections had already refused these
  rows once, when they were added before the run. They land now, with the
  measured density they could not have had then.
- PUBLISHED_LEGS keys grew a serving_path element. Six rows come from Vertex and
  two from a different account; a fixture that could not say which would be
  hiding the one caveat these numbers most need to carry.

Also fixed a stale reference: docs pointed at tests/test_claude_annapolis_leg.py,
renamed to test_claude_published_legs.py when it grew past annapolis. The
reproduction instructions named a file that does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjWxvSJybx4xJUaVTvGnR7
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.

1 participant