Skip to content

Config surface + fallback ladder + docs alignment overhaul - #106

Merged
Brian Krabach (bkrabach) merged 10 commits into
mainfrom
anthropic-surface-overhaul
Aug 29, 2026
Merged

Config surface + fallback ladder + docs alignment overhaul#106
Brian Krabach (bkrabach) merged 10 commits into
mainfrom
anthropic-surface-overhaul

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Summary

Implements the config-surface + fallback-ladder + docs-alignment overhaul per anthropic-surface-spec.md (verified against main@46d4869), landed as 9 independently-revertible commits.

Owner adjudications applied (override spec's open questions):

  • Q-1 = option (a), amended: fallback ladder (fable/mythos→opus→sonnet→haiku) applies to BOTH overload and refusal fallback. refusal_fallback_model is removed entirely (not retained as an override, per owner direction) — refusal fallback resolves via the same fallback_models/live-cache/static-backstop precedence as overload. Rationale: Anthropic's guidance for a refusal is to retry with a less-restrictive (cheaper) model, and fallback should never land on something MORE expensive than the user's selection — which the old hardcoded Opus-escalation violated for sonnet/haiku users. refusal_fallback_enabled remains the gate; haiku refusal is terminal; thinking-block stripping is preserved.
  • Q-2 = yes, gated on live probe (T-C06-live, PASS) — widened supports_output_config to Opus 4.5+/Sonnet 4.6+.
  • Q-3 = CONSERVATIVE, gated on live probe (T-C08-live, PASS: opus-4.6 + thinking.type=enabled → HTTP 200) — hard 400-gate unchanged (4.7+/5+ only); added a deprecation warning for the 4.6 "still works" tier.
  • Q-4 = yestemperature also coerced via the numeric warn-and-default helper.
  • Bonus items (all approved): Sonnet 5 max output 64K→128K; mythos family added to _detect_family; min-cacheable-length behavioral gate declined (capability data + comment only).

Live merge-gate probes (run against api.anthropic.com, tiny spend)

Probe Request Result
T-C06-live output_config.effort="max" on claude-sonnet-4-6 and claude-opus-4-6, max_tokens=16 200/200 — widening confirmed safe
T-C08-live thinking:{type:"enabled",budget_tokens:1024} on claude-opus-4-6, max_tokens=1040 200 — deprecated-but-working confirmed
T-C10-live (A) cache_control:{type:"ephemeral",ttl:"1h"} on ~8000-char system block, no beta header 200, ephemeral_1h_input_tokens=4293>0 — header not required
T-C10-live (B) same request with anthropic-beta: extended-cache-ttl-2025-04-11 200, cache read reused A's write — header harmless but unnecessary → dropped
T-B13-live GET /v1/models 9 models returned, no fable/mythos model currently listed on this account — mythos classification verified by unit test + Anthropic doc citations only; live model-shape confirmation is a known gap (see below)

Deviations from the draft spec (surfaced, not silently improvised)

  1. Q-1(a) refusal override removal — the draft's own code for option (a) retained refusal_fallback_model as a still-functional override. Per explicit owner instruction for this task, the key is removed entirely with no override path. T-B16 (as literally specified: "explicit override still wins") does not apply under this adjudication; replaced with tests asserting the key is retired/inert and that refusal fallback shares overload's target resolution.
  2. _TYPED_REQUEST_PARAMS composition — the draft spec's list included top_k, top_p, betas, none of which are present on the installed SDK's typed create() signature (anthropic==1.0.0, this repo's floor pin) — verified via inspect.signature, not assumed. Replaced with the four typed params the installed SDK actually has that the draft list missed (cache_control, container, inference_geo, user_profile_id). Guarded going forward by a new SDK-contract test that fails loud on future SDK drift.
  3. T-B13-live partial gap — no fable/mythos-family model is currently exposed by list_models() on this account, so live ID-shape confirmation for claude-mythos-5 could not be completed. Family classification and capabilities are covered by unit tests and the spec's own Anthropic-doc citations (platform.claude.com/en/docs/build-with-claude/effort and /context-windows, 2026-08-29) but not a fresh live listing.

Test suite

  • Baseline (main@46d4869): 695 passed
  • After (this branch): 747 passed
  • python_check on touched files: no new errors/warnings vs. baseline (same 15 pre-existing errors / 18 warnings, unrelated to this change — repo CI does not gate on ruff/pyright, only pytest -q)

Migration impact (owner's 5 live settings.yaml instances)

Covered by tests/test_migration_owner_instances.py (parametrized over the 5 real config shapes from the spec's §7): zero false-positive unknown-key warnings; only the expected targeted fallback_sonnet_model/fallback_haiku_model removal messages fire where those instances set them (opus-4.8, opus, sonnet, fable). The fable instance's fallback goes from completely dead to fully functional (fable→opus→sonnet→haiku) — the release's headline fix. The sonnet instance's max_output_tokens goes from 64K to 128K (was silently clamped to half its real ceiling).

🤖 Generated with Amplifier

Sonnet's _get_capabilities branch never set max_output_tokens, silently
inheriting the ModelCapabilities dataclass default of 64000. Per
Anthropic (platform.claude.com/en/docs/build-with-claude/context-windows,
verified 2026-08-29): "A single request to any model with a 1M-token
context window can generate up to 128k output tokens." Sonnet 4.6+ has
1M context (supports_1m=is_46_plus) and is therefore entitled to 128K
output -- the owner's live 'sonnet' instance (claude-sonnet-5) was
clamped to half its real output capacity by this bug.

Fix: max_output_tokens=128000 if is_46_plus else 64000, mirroring the
opus branch's existing pattern.

Test: T-C03 (test_sonnet_46_plus_max_output_tokens_is_128k) -- fails
before (asserts 64000 today), passes after.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md C-03 (commit 1/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Seven numeric config keys (max_tokens, priority, timeout,
overloaded_delay_multiplier, throttle_threshold, throttle_delay,
max_concurrent_requests) were read straight off self.config.get() with
no coercion. The warn-and-default helpers (_config_int/_config_float)
already existed and were already used elsewhere in this same
constructor -- these seven were simply missed:

  - max_tokens/priority/timeout stayed silent strings ('8192', '1',
    '600') all the way toward the wire.
  - overloaded_delay_multiplier/throttle_threshold/throttle_delay/
    max_concurrent_requests RAISED ValueError at mount on a typo'd
    string ('ten', '2%', 'fast', 'many'), killing the whole provider
    instance instead of warning and using a safe default.

Also coerces temperature (D-06, owner-adjudicated Q-4 = yes): it
was the 8th numeric key, left uncoerced, flowing to the wire via
extra_body where Anthropic's JSON parser would reject a string.
Leaving exactly one numeric key uncoerced was flagged as the kind of
inconsistency someone "fixes" later without knowing it was deliberate
-- so it is coerced with the same helper as the signed seven.

New tests (tests/test_config_surface.py): T-D08 (invalid numeric
string warns + defaults, never raises -- fails before on 4 of 7 keys),
T-D09 (numeric string coerces to real int/float -- fails before on all
7, all staying str), plus max_tokens- and passthrough-specific
variants. 18 new tests total.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md D-05/D-06 (commit 2/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… messages

Ports the config-surface pattern from provider-openai's v2 overhaul
(config-surface-v2-openai): a module-level allowlist of every config key
this provider actually reads (_CONSUMED_CONFIG_KEYS, 41 keys -- audited
against every self.config.get() call site in BOTH the constructor and
the deferred request path, since 8 keys are read only in _build_params /
_build_web_search_tool and a constructor-only audit would false-positive
on every extended-thinking or web-search user), plus:

  - _INERT_CONFIG_KEY_MESSAGES: targeted, actionable warnings for keys
    that are recognized but currently do nothing. `debug` and `raw_debug`
    are ghosts -- README.md's "Debug Configuration" section documented
    both for as long as that section has existed, but neither was ever
    implemented (grep for them across __init__.py returns zero matches).
    Anyone who followed the README has them set and got silence; now
    they get a warning naming the real key (`raw`).
  - _DEPRECATED_ALIAS_CONFIG_KEYS + a two-case warning block: `effort`
    (the legacy alias for `reasoning_effort`) now warns even when used
    ALONE, not just when both keys are set -- previously an `effort`-only
    config never learned it should migrate.
  - _warn_unknown_config_keys(): the generic sweep, with a difflib
    did-you-mean suggestion, run after all config is read.
  - EXTRA_KNOWN_CONFIG_KEYS (ClassVar, empty by default): a zero-cost
    subclass extension point. provider-openai needed to retrofit this
    after the fact (its own sweep shipped without it first) -- adding it
    now avoids that repeat.

No config keys are removed in this commit (per the release's revert
safety: fallback_sonnet_model/fallback_haiku_model/refusal_fallback_model
remain fully consumed until the fallback-ladder commits that replace
them land, later in this series).

New tests (tests/test_config_surface.py): T-D01 (deferred keys present
in the allowlist), T-D02 (the sweep produces zero false positives on
the owner's 5 real settings.yaml instances -- shared OWNER_LIVE_CONFIGS
fixture reused by the full migration acceptance test later in this
series), T-D03 (did-you-mean), T-D04/T-D05 (targeted inert-key
messages, single and double), T-D06 (effort alias: alone -> deprecated,
with reasoning_effort -> both-set, never both), T-D07 (subclass
extension point). 12 new tests.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md D-01..D-04 (commit 3/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… routing

Ports the extra_request_params pattern from provider-openai's v2 overhaul,
adapted for Anthropic's typed SDK surface. Settings-only (never a
ConfigField), merged into params LAST so it overrides every
provider-computed value -- deliberately, since whoever sets this owns the
consequences.

The Anthropic wrinkle: unlike provider-openai (whose Responses API client
tolerates extra kwargs), an unrecognized keyword here raises "got an
unexpected keyword argument", which _do_complete's catch-all translates
into a RETRYABLE KernelLLMError -- so a permanent config typo would be
retried max_retries times before surfacing. This module already carries
the scar (see the _WIRE_ONLY_PARAMS comment for `temperature`/`speed`).
So extra_request_params routes PER KEY: known typed params land on the
typed surface, everything else goes to extra_body. The merge runs BEFORE
_route_wire_only_params, so a user-supplied `temperature` still ends up
relocated to extra_body by the same router that handles the provider's
own -- merging after it would reintroduce the exact bug this avoids.

Deviation from the draft spec, found by this commit's own SDK-contract
test: the spec's _TYPED_REQUEST_PARAMS list included `top_k`, `top_p`,
and `betas`, none of which are present on the installed SDK's typed
surface (anthropic==1.0.0, this repo's floor pin) -- verified via
inspect.signature(AsyncMessages.create), not assumed. They met the same
fate as `temperature`: dropped from the typed Messages surface in the
1.0.0 major bump. Listing them as "typed" would have routed a config
value onto the typed surface and reproduced the exact
unexpected-keyword-argument retry bug this allowlist exists to prevent.
Replaced with the four typed params the installed SDK actually has that
the draft list missed: `cache_control`, `container`, `inference_geo`,
`user_profile_id`. Guarded going forward by
test_sdk_contract.py::TestTypedRequestParamsMatchSdkSignature, which
fails loud on any future SDK drift instead of silently trusting the list.

Also adds `extra_request_params` to _CONSUMED_CONFIG_KEYS (D-01/D-04) --
omitting it would have made every user of this new key see a false
"Unknown config key" warning from the sweep landed in commit 3.

New tests (tests/test_config_surface.py): T-D10 (typed key lands on
typed surface), T-D11 (unknown key routes to extra_body), T-D12
(temperature still routes to extra_body even via the escape hatch),
T-D13 (override warns exactly once across primary + continuation calls,
not once per call), T-D14 (non-mapping value raises ValueError at mount,
naming settings.yaml). New test (tests/test_sdk_contract.py): T-D15 (SDK
contract -- every _TYPED_REQUEST_PARAMS name binds to the installed
SDK's create() signature). 6 new tests.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md D-07 (commit 4/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Replaces the hardcoded if/elif fallback-target resolution with an
explicit successor map (_FALLBACK_NEXT_FAMILY) walked downward from the
requested model's family, skipping any unusable rung instead of
terminating the whole ladder there. Fixes three defects, reproduced
against a fresh clone before this change:

  - D-1: fable (and any non-opus/sonnet family) returned None --
    completely dead. The owner's live `fable` instance has fallback
    fully configured (fallback_on_overload, retry_count, cooldown,
    both target models) and NONE of it did anything. This is the
    release's headline fix: fable -> opus -> sonnet -> haiku all now
    resolve.
  - D-2: a blank or self-referential target terminated the ladder
    instead of walking to the rung below it.
  - D-3: targets were hardcoded config-default strings
    (claude-sonnet-4-6) that go stale the moment a newer model ships.
    Target resolution is now three-source: `fallback_models` config
    override (settings-only, per-family map, replacing the two deleted
    scalar keys) -> a live "newest model per family" cache warmed for
    free as a side effect of any list_models() call (or once,
    best-effort, healthy-path-only, via _warm_family_latest) ->
    _STATIC_FALLBACK_MODELS backstop, which always resolves and can
    never silently die the way `fable` did.

Also lands, in the same commit (interlocked with the ladder):

  - B-02/X-2: `mythos` added to _detect_family and _get_capabilities.
    claude-mythos-5 previously fell through to "sonnet" (wrong
    capability row, wrong context window, wrong ladder position, wrong
    fallback target) -- Anthropic ships Mythos 5 as a top-tier,
    1M-context, 128K-output family priced identically to Fable 5.
    Enters the ladder as fable's PEER (both step down to opus), not
    between fable and opus.
  - B-08: a mount-time LOUD warning when fallback_on_overload is
    enabled on an instance whose default_model is already the ladder's
    terminal rung (haiku) -- there is no lower tier, so the setting has
    no effect. The wizard already hides this ConfigField for Haiku, so
    this specifically catches hand-edited settings.yaml.
  - B-09: corrected defaults. fallback_retry_count 1 -> 2,
    fallback_cooldown_seconds 1800.0 -> 300.0 -- matching what 4 of the
    owner's 5 real instances already set explicitly by hand (a
    30-minute cooldown after a transient capacity blip stranded a whole
    session on a downgraded model long after real capacity returned).
  - C-04: `fallback_sonnet_model` / `fallback_haiku_model` retired.
    Superseded by `fallback_models` (a per-family map: `{opus:
    claude-opus-4-8, sonnet: ..., haiku: ...}`). Both keys move to
    _INERT_CONFIG_KEY_MESSAGES with a targeted migration message naming
    the replacement -- the four owner instances that set them get an
    actionable warning at mount, not silence. Their ConfigField wizard
    entries are removed later in this series (A-08), once the wizard
    slims to 5 fields.

New tests (tests/test_fallback_ladder.py, new file): T-B01/T-B02 (fable
resolves and the full 4-rung walk), T-B03 (haiku terminal, regression
guard), T-B04 (an unusable/self-referential rung is skipped, not
fatal -- the actual D-2 fix; plus a companion test showing a truly
*blank* override falls through to the static backstop rather than being
treated as a disabled rung), T-B05/T-B06 (fallback_models override and
live-cache precedence), T-B07/T-B08 (_warm_family_latest error
swallowing and at-most-once), T-B12 (terminal-family mount warning),
T-B13/T-B14 (mythos family detection and capabilities), T-B17 (cycle
guard), T-B18 (corrected defaults). 20 new tests.

New test (tests/test_migration_owner_instances.py, new file): the
release's actual migration merge gate -- mounts each of the owner's 5
real settings.yaml instances verbatim and asserts ONLY the expected
targeted inert-key warnings fire (fallback_sonnet_model /
fallback_haiku_model where set) and zero generic unknown-key
warnings. Extended in a later commit once refusal_fallback_model also
retires.

Updated tests (tests/test_fallback.py): three existing tests asserted
the OLD hardcoded fallback target (`claude-sonnet-4-6`, the previous
config default). The ladder's static backstop is now
`claude-sonnet-5` (the newest GA sonnet as of 2026-08-29) --
test_529_opus_overload_falls_back_to_sonnet,
test_persisted_breaker_state_is_used_by_fresh_provider, and
test_429_overloaded_body_also_triggers_fallback (whose 429-triggers-fallback
*logic* is inverted in the next commit; only its expected model id is
fixed here) updated to match.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md B-01..B-04, B-08, B-09, C-04, B-02/X-2
(commit 5/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…fusal fallback follows the same ladder

**B-06 -- 429 is not overload.** `_is_overload_fallback_error` classified
a 429 as an overload trigger when its body happened to contain the
substring "overload"/"overloaded". Per Anthropic's error docs
(platform.claude.com/docs/en/api/errors, verified 2026-08-29): a 429 is
`rate_limit_error` -- "Your organization has hit a rate limit, reached
its usage tier's monthly spend cap, or reached a spend limit" -- i.e.
PER-ACCOUNT. A lower-tier model draws on the SAME org quota, so
downgrading cannot relieve it; it only hides the real fix (back off,
ramp up gradually, or raise the quota). The substring branch is deleted
entirely -- a 429 now always falls through to the full retry budget on
the SAME model (exponential backoff honoring retry-after), which is the
documented correct response. No new code path was required; deleting
the 429 branch is sufficient.

**B-07 -- refusal fallback: owner-adjudicated Q-1, option (a).**
Decision: the fable->opus->sonnet->haiku downgrade ladder now applies to
BOTH fallback types -- overload AND refusal. Rationale (owner's, recorded
here verbatim): (1) Anthropic's own guidance for a refusal is to retry
with a less-restrictive model, i.e. downward, not up to the most
expensive tier; (2) there is a product expectation that fallback never
lands on a model MORE expensive than the one the user selected -- which
the old hardcoded refusal-escalation target (always claude-opus-4-8)
actually violated for sonnet/haiku users. This is not a pure regression:
inspection shows both `opus` instances' refusal fallback was ALREADY
silently dead under the old chain (opus -> claude-opus-4-8 -> same
family -> None).

`refusal_fallback_model` (the explicit hardcoded-escalation override) is
RETIRED ENTIRELY -- not merely defaulted to None with an override path
retained. `_refusal_fallback_target` now delegates straight to
`_fallback_target_for_model`, using the EXACT SAME three-source
resolution as overload fallback (fallback_models override -> live
list_models cache -> static backstop, skipping unusable rungs).
`refusal_fallback_enabled` remains the gate, default unchanged. Haiku
refusal is now terminal -- the refusal surfaces normally, exactly as a
haiku response always has. Thinking-block stripping on the refusal
retry (`_strip_thinking_blocks`) is unchanged and still runs -- still
required cross-model. The key moves to _INERT_CONFIG_KEY_MESSAGES with
a targeted migration message naming the replacement mechanism.

Deviation from the draft spec, called out explicitly per task
instruction: the spec's own text for "option (a)" retained
`refusal_fallback_model` as a still-functional override that "wins" over
the ladder (so an owner could set it on sonnet/haiku to preserve the old
escalation behavior). The owner's adjudication supersedes that: the key
is removed entirely, with no override path -- refusal fallback follows
the ladder unconditionally once refusal_fallback_enabled is true. This
also means T-B16 as originally specified ("explicit refusal_fallback_model
still wins over the ladder") does not apply; it is replaced with tests
asserting the key is retired and inert, and that refusal fallback uses
the same fallback_models-based resolution as overload.

New/updated tests: tests/test_fallback_ladder.py gains T-B15 (refusal on
opus resolves to the ladder's claude-sonnet-5 target, not the old
claude-opus-4-8, and not None). tests/test_fallback.py's
`test_429_overloaded_body_also_triggers_fallback` is INVERTED to
`test_429_overloaded_body_does_not_trigger_fallback` -- every attempt now
stays on the same model. tests/test_refusal_fallback.py rewritten:
the fallback-target assertion updates from the hardcoded
claude-opus-4-8 to the ladder's claude-sonnet-5 (fable's actual next
rung); the same-family loop-guard test is rebuilt against a pathological
`fallback_models` override (since the old `refusal_fallback_model`
override no longer exists to construct the scenario with); a new
terminal-rung test confirms haiku refusals have no fallback target; a
new test confirms the refusal ladder shares fallback_models' override
precedence with overload fallback.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md B-06, B-07 (owner-adjudicated Q-1,
option (a) as amended) (commit 6/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Verified against platform.claude.com/en/docs/build-with-claude/context-windows
on 2026-08-29: "For every model with a 1M-token context window, 1M is the
default: you don't need a beta header, and long-context requests are
billed at standard pricing." The string "context-1m" appears zero times
on that page.

C-01: deletes `_should_add_context_1m_beta` entirely (it computed which
models still needed the header, on a version threshold that was already
wrong -- Sonnet 4.5 was included when Anthropic's own docs say it has a
200K window, not 1M), deletes the `BETA_HEADER_1M_CONTEXT` constant, and
deletes the call site in `_build_request_beta_headers`. The now-unused
`model_id` parameter is removed from `_build_request_beta_headers`'s
signature and its one call site (verified via grep that no other caller
depends on it).

C-02/A-03: `enable_1m_context`'s constructor default flips `True` ->
`False`, with the ConfigField's declared default flipped to match in the
same commit (both must change together or the wizard and the
constructor disagree; the ConfigField's prompt text reword is deferred
to the wizard-slimming commit later in this series). A comment at the
assignment site encodes the no-premium-tier invariant so cost modeling
never re-adds a long-context price tier: `enable_1m_context`'s only
remaining effect is the ADVERTISED context window handed to the context
manager (how much history is kept per request), which is a cost
decision, not a capability one -- it no longer changes what the API
will accept or send.

Test changes:
  - tests/test_fallback.py: 3 of the 4 existing 1M-beta-header tests are
    deleted (they asserted the header's conditional presence, which no
    longer exists at all); the 4th
    (test_enable_1m_context_does_not_become_global_beta_header) is
    RE-POINTED to test_enable_1m_context_never_adds_a_beta_header --
    asserting the real invariant (no 1M header, ever, for any value of
    enable_1m_context) rather than the narrower "not a GLOBAL header"
    claim. A new end-to-end variant confirms no header reaches a live
    request either.
  - tests/test_model_capabilities.py: TestContextBetaHeaderOpus48 (which
    asserted the header's presence threshold) is replaced with
    TestContextBetaHeaderNeverSent (the header is absent for every
    version, opus included). TestFastModeBetaHeader and
    TestSpeedConfigPlumbing updated to drop the now-removed `model_id=`
    kwarg from `_build_request_beta_headers` call sites.
  - tests/test_opus_47.py: TestBetaHeader1MFix (8 tests asserting the
    now-deleted `_should_add_context_1m_beta`'s per-version threshold
    behavior) is removed; a comment points to its replacement coverage.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md C-01, C-02, A-03 (default only) (commit 7/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…anual thinking on x.6, drop extended-cache-ttl header

All four items in this commit were gated on live merge-gate probes run
against the real Anthropic API before implementation, per the release
plan -- results recorded below and in the PR body.

**C-05 -- corrected xhigh/max effort gating.** Anthropic's own docs
(platform.claude.com/en/docs/build-with-claude/effort, verified
2026-08-29) place the xhigh/max split at 4.6/4.7, not 4.7/4.8 as the
pre-overhaul code assumed:
  - xhigh: Fable 5, Mythos 5, Opus 5/4.8/4.7, Sonnet 5
  - max: all of the above PLUS Opus 4.6 and Sonnet 4.6 (the doc's own
    explanation: "xhigh is a newer level; some models that support max
    don't support xhigh")
  Opus branch: `is_47_plus` gets xhigh+max (was is_48_plus for max,
  is_47_plus for xhigh-only); Opus 4.6 gains `max`. Sonnet branch:
  Sonnet 4.6 gains `max` (previously neither).

**C-06 (scope expansion X-1, owner-adjudicated Q-2 = yes, GATED on
live probe) -- widened supports_output_config.** Was `is_47_plus`
(opus) / `is_5_plus` (sonnet); without this, C-05's `max` addition to
Opus 4.6/Sonnet 4.6 would be INERT (output_config.effort is only
emitted when supports_output_config is True). Doc: "Supported models:
... Opus 4.5, 4.6, 4.7, 4.8, and 5 ... Sonnet 4.6 and 5." Widened to
`is_45_plus` (opus) / `is_46_plus` (sonnet).
  MERGE-GATE PROBE (T-C06-live, run against api.anthropic.com,
  max_tokens=16): `output_config: {"effort": "max"}` on both
  claude-sonnet-4-6 and claude-opus-4-6 -> HTTP 200 on both. PASS --
  shipped as specified.

**C-07 -- manual-thinking deprecation tier (owner-adjudicated Q-3 =
CONSERVATIVE, GATED on live probe).** New `manual_thinking_deprecated`
capability field, True for Opus 4.6 and Sonnet 4.6 specifically (the
one version where `supports_manual_thinking` is still True but the API
already considers `type="enabled"` legacy -- 4.7+/5+ already hard-gate
to adaptive via the existing `supports_manual_thinking=False` path, so
the two flags never both apply on the same model). When
`thinking.type="enabled"` reaches the wire on a deprecated-tier model,
`_build_params` now logs a loud deprecation warning naming the adaptive
migration path -- the hard 400-on-4.7+/5+ gate is UNCHANGED (per the
conservative adjudication: the request still sends `type="enabled"` on
4.6, it is not silently coerced).
  MERGE-GATE PROBE (T-C08-live, max_tokens=1040, budget_tokens=1024):
  `thinking: {"type": "enabled", "budget_tokens": 1024}` on
  claude-opus-4-6 -> HTTP 200 (thinking block returned). PASS --
  confirms "deprecated but working"; shipped as specified.

**C-08 -- min_cacheable_tokens capability data (C-09: decline the
behavioral gate).** New field on ModelCapabilities, populated per family
from Anthropic's prompt-caching page (verified 2026-08-29): fable/mythos
512 (mythos preview 2048), opus non-monotonic 512(5+)/1024(4.8)/2048(4.7)/
4096(4.6, written as an explicit descending chain, not a >= threshold,
so a future "simplification" doesn't flatten a real non-monotonic API
constraint), sonnet 1024 flat, haiku 4096. Per C-09's recommendation,
deliberately NO client-side gate is added to suppress cache_control
placement below this threshold: the API's own failure mode is already
optimal ("processed without caching, and no error is returned" --
Anthropic docs) at zero cost, while a client-side gate would require
counting tokens before every request to avoid a cost that is already
zero. Data lands for diagnostics/README only.

**C-10 -- extended-cache-ttl-2025-04-11 beta header dropped (GATED on
live probe, two-part).** The header appears zero times on Anthropic's
current prompt-caching docs; the documented mechanism is the `ttl`
field on `cache_control` alone.
  MERGE-GATE PROBE (T-C10-live, part A, max_tokens=16, ~8000-char
  system block, ttl:"1h", NO beta header): HTTP 200,
  usage.cache_creation.ephemeral_1h_input_tokens=4293 (>0),
  ephemeral_5m_input_tokens=0 -> the 1h TTL is honored WITHOUT the
  header -> header not required.
  MERGE-GATE PROBE (part B, same request WITH the header): HTTP 200,
  cache_read_input_tokens=4293 (reused the part-A write) -> the header
  is also harmless to keep, but is undocumented and unnecessary. Per the
  probe's own decision rule (part A PASS -> DROP), the header-append
  logic for `cache_stable_region_ttl_1h` is removed entirely; the knob's
  behavior (opt-in 1h TTL on system/tool cache breakpoints) is
  unchanged.

Test changes (re-baselining the largest mechanical surface in the
release, as flagged in the spec's own risk table):
  - tests/test_model_capabilities.py: test_opus_47_no_max_effort ->
    test_opus_47_has_xhigh_and_max (inverted); test_opus_5_capabilities_
    match_opus_48_gate updated for the one EXPECTED non-monotonic
    exception (min_cacheable_tokens: Opus 5=512, Opus 4.8=1024) instead
    of whole-object equality; test_sonnet_46_unchanged_by_sonnet5_gate
    updated for Sonnet 4.6 now getting `max` + output_config + the
    deprecation flag.
  - tests/test_opus_47.py: test_opus_46_no_output_config inverted to
    test_opus_46_output_config_now_supported; test_opus_47_supported_
    efforts and test_opus_46_no_xhigh updated for the corrected xhigh/max
    split; test_opus_47_invalid_effort_omits_output_config re-targeted at
    a value outside ANY model's supported_efforts (since 'max' -- its old
    example -- is now valid on Opus 4.7); test_task_budget_ignored_on_
    unsupported_model updated to assert the task_budget sub-key is absent
    rather than output_config as a whole (which now legitimately exists).
  - tests/test_reasoning_effort.py: test_opus_47_max_still_logs_
    original_gate_b_warning_only inverted to test_opus_47_max_now_
    supported_no_gate_b_warning.
  - tests/test_prompt_cache_breakpoints.py: updated to assert the
    extended-cache-ttl header is ABSENT.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md C-05, C-06 (X-1/Q-2), C-07 (Q-3), C-08,
C-09, C-10 (commit 8/9).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Wizard slims from 12 ConfigFields to 5: api_key, base_url, enable_1m_context,
reasoning_effort, cache_stable_region_ttl_1h (A-01..A-08). "Demoted" fields
(enable_prompt_caching, fallback_on_overload, fallback_retry_count,
fallback_cooldown_seconds, persist_fallback_state) keep working exactly as
before -- settings-only, still in _CONSUMED_CONFIG_KEYS, never trigger an
unknown-key warning. fallback_sonnet_model/fallback_haiku_model were already
removed in the fallback-ladder commit (C-04, this only removes their
ConfigField declarations, A-08).

Reworded prompts (one-liners replacing multi-sentence explanations, with the
detail moved to README.md): api_key ("Anthropic API key"), enable_1m_context
("Use the full 1M-token context window (more context kept per request =
higher cost)"), reasoning_effort ("Reasoning effort -- higher is smarter,
slower, costlier"), cache_stable_region_ttl_1h ("1-hour cache TTL -- 2x write
cost, fewer writes"). cache_stable_region_ttl_1h's show_when is dropped (A-04):
enable_prompt_caching is no longer a ConfigField, so the condition could never
be satisfied in the wizard; the inert combination is already handled loudly by
the constructor guard.

tests/test_validation.py: TestFallbackConfigFields rewritten -- the ConfigField
presence assertions invert (fields are now absent, keys remain functional
settings-only); a new test confirms fallback_sonnet_model/fallback_haiku_model
produce their targeted removal messages. tests/test_prompt_cache_breakpoints.py
updated for the dropped show_when.

README overhaul (E-01..E-05): deleted the `debug`/`raw_debug` ghost keys from
the config example and their entire "Debug Configuration" section (neither was
ever implemented -- grep confirms zero matches in __init__.py; the README
documented them in error), replaced with the real `raw` key. Fixed the
retry_jitter documentation contradiction: the README recommended
`retry_jitter: 0.2`, but `_config_bool` parses `0.2` as `false` -- the exact
opposite of the documented intent. The code is correct (retry_jitter is a
boolean; a float form has never worked in any released version); the docs are
corrected. Deleted the stale manual-beta-header 1M section (it instructed
setting `beta_headers: "context-1m-2025-08-07"` on a model that per Anthropic's
own docs has a 200K window, and a header no longer in Anthropic's
documentation -- actively harmful advice), replaced with the GA/no-header
explanation. Added a house-style key-reference table covering every surviving
key (wizard vs settings-only), plus the two retired keys' migration note.

Part of the config-surface + fallback-ladder + docs-alignment overhaul.
Spec: anthropic-surface-spec.md A-01..A-08, E-01..E-05 (commit 9/9, final).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
test_readme_retry_docs.py read README.md via Path.read_text() with no
encoding, which uses the OS default (cp1252 on Windows) instead of
UTF-8. The README's em-dash/multiplication-sign characters this PR adds
(retry_jitter row, cache-TTL prompts) cannot be decoded as cp1252,
failing the whole file's collection on windows-latest CI with
UnicodeDecodeError. Per IMPLEMENTATION_PHILOSOPHY.md: always specify
encoding="utf-8" for cross-platform file I/O.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@bkrabach

Copy link
Copy Markdown
Collaborator Author

Self-authored PR, landing per maintainer admin pattern (author is repo maintainer). All CI checks green (license/cla + 6-way pytest matrix). Merging via squash.

@bkrabach
Brian Krabach (bkrabach) merged commit 833403b into main Aug 29, 2026
7 checks passed
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