Skip to content

fix(advisor): make the reviewer usable on a Claude subscription, and give it reasoning effort - #793

Merged
ericleepi314 merged 9 commits into
mainfrom
feat/advisor-subscription-and-effort
Aug 3, 2026
Merged

fix(advisor): make the reviewer usable on a Claude subscription, and give it reasoning effort#793
ericleepi314 merged 9 commits into
mainfrom
feat/advisor-subscription-and-effort

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

/advisor was unusable for premium Anthropic models on a subscription login, and where it did run, it ran weakly. Seven defects, found by driving the real path rather than reading it.

The headline bug — system as a string vs a block list

AnthropicProvider._prepare_subscription_request prepends the required "You are Claude Code…" preamble. Given a string it CONCATENATES into one blob; given a list it INSERTS the preamble as its own block at index 0. The subscription endpoint accepts only the latter for premium models.

Wire-probed against claude-opus-5 over subscription OAuth, 3/3 per cell:

system shape result
None → bare preamble string 200
<string> → preamble + advisor text fused 429
[<block>] → preamble block + advisor block 200

The rejection arrives mislabelled by the API as {"type":"rate_limit_error","message":"Error"} — so it reads as capacity and invites a pointless backoff hunt rather than a shape fix. Haiku accepts the string form, which is why a cheap smoke test misses it entirely. The main loop has always sent blocks (that is what carries cache_control), which is why only this path was broken.

The rest

  1. No thinking, no effort on either wire. A model chosen precisely because it reasons harder ran with thinking off at the API default. Extracted build_anthropic_thinking_kwargs from _call_model_sync so both callers share ONE copy of the model gates (adaptive-vs-budget, effort allowlist, xhigh clamp). New advisor_effort setting + /advisor <provider>:<model> --effort <level>.
  2. max_tokens flat 4096. Thinking draws from the SAME budget, so a high-effort reviewer could spend it all reasoning and return stop_reason=max_tokens with no text. Now per-model, floored at 4096 and ceiled at 32768 on the OpenAI-compat wire — the table value there is an auto-compact reservation, not a legal request cap (deepseek 384000, luna 128000). Honest scope: DeepSeek accepts 384000 (probed, 200 OK), so this guards an untested number per provider, not a known outage.
  3. No retry. One transient 429/5xx ended the consultation. Bounded to 3 attempts, abort-aware, honours Retry-After.
  4. Provider key read from config.json only — the one call site ignoring the environment. Now uses the shared resolve_api_key(). Empty stays legitimate: the subscription path requires an empty key to fall through to OAuth.
  5. Effort not translated into the provider's vocabulary — silently re-broke fix(effort): send reasoning effort in the provider's own vocabulary #791, the commit this branched from. A DeepSeek advisor got xhigh, a level it does not know, so it dropped the field and applied its default: a silent downgrade. Extracted normalize_effort_for_provider so the hook cannot be forgotten a third time.
  6. Dead call_kwargs.get("model"); a function-local import logging shadowing the module import for the whole scope; a negative time.sleep in the backoff.

Harbor adapter

New --ak advisor=<provider>:<model> and --ak advisor_effort=. subscription=true now accepts anthropic in either role — it previously required an anthropic main model, making "cheap API-key worker + premium subscription reviewer" inexpressible. The advisor provider's key is forwarded, and ANTHROPIC_API_KEY is withheld via both routes (process env and the seeded config env block, which get_secret also reads) so OAuth stays the only path to the subscription.

Verification

  • Full suite 9720 passed, 10 skipped, exit 0
  • Live e2e: gpt-5.6-luna worker (API, xhigh) consulting claude-opus-5 (subscription, xhigh) — advisor called twice, real advice both times, worker acted on it
  • Container tb2.1: 4 task-runs reward 1.0, advisor answered
  • Two independent reviews, both APPROVE. One verified the _call_model_sync extraction differentially over 17,280 combinations (0 divergences) and killed 11/11 mutants; the other over 2,688 (0 divergences) and 8/8.

A recurring theme worth calling out for reviewers: every failure mode here is quiet. A failed consultation lets the worker carry on, so a run can score 1.0 with a reviewer that never once answered — that is exactly how the missing key forwarding was found. RUN_ADVISOR_TB21.md leads with how to verify the advisor actually replied.

🤖 Generated with Claude Code

ericleepi314 and others added 9 commits August 2, 2026 23:27
…give it effort

The client-side advisor had four defects that compounded: it was
outright broken for premium Anthropic models on a subscription login,
and where it did run it ran weakly.

1. `system` went as a plain STRING. On the subscription (OAuth) path
   `_prepare_subscription_request` prepends the required "You are Claude
   Code…" preamble; with a string it CONCATENATES into one blob, with a
   list it INSERTS the preamble as its own block. The endpoint accepts
   only the latter for premium models. Wire-probed against claude-opus-5
   over subscription, 3/3 per cell:

     system=None (bare preamble string)          -> 200
     system=<string> (preamble + advisor text)   -> 429
     system=[<block>] (preamble block + text)    -> 200

   The rejection arrives MISLABELLED as
   {"type": "rate_limit_error", "message": "Error"}, so it reads as
   capacity and invites a backoff hunt rather than a shape fix. Haiku
   accepts the string form, which is why a cheap smoke test misses it.

2. No thinking config and no reasoning effort on either wire — a model
   chosen precisely because it reasons harder ran with thinking off at
   the API default. Extracted `build_anthropic_thinking_kwargs` from
   `_call_model_sync` so both callers share ONE copy of the model gates
   (adaptive-vs-budget, the effort allowlist, the xhigh clamp) and they
   cannot drift; OpenAI-compat wires get `extra_body.reasoning_effort`
   with clamp_xhigh=False, since that allowlist holds Anthropic model
   names and matched nothing here. New `advisor_effort` setting and
   `/advisor <provider>:<model> --effort <level>`; unset inherits the
   session effort, then omits the parameter entirely.

3. `max_tokens` was a flat 4096. Thinking is drawn from the SAME budget,
   so a high-effort reviewer could spend it all reasoning and return
   stop_reason=max_tokens with no text — surfacing as the useless
   "Advisor returned no text content". Now per-model, floored at 4096.

4. No retry: one transient 429/5xx ended the consultation. Bounded to 3
   attempts, abort-aware, honouring Retry-After via the main loop's own
   classifier so both lanes agree on "transient".

Also removes a dead `call_kwargs.get("model")` lookup (call_kwargs never
carries one) and a function-local `import logging` that shadowed the
module import for the whole scope.

Harbor adapter: new `advisor` / `advisor_effort` agent kwargs, and
`subscription=true` now covers an anthropic ADVISOR rather than only an
anthropic main model — the pairing that motivated this (an API-key
worker consulting a subscription reviewer) was previously inexpressible.
The worker's own provider key is still forwarded in that configuration;
only ANTHROPIC_API_KEY stays withheld so OAuth remains the sole route.

Verified live: gpt-5.6-luna worker (API, effort=xhigh) consulting
claude-opus-5 (subscription, effort=xhigh) end to end on the headless
path — advisor called twice, real advice both times, worker acted on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The adapter forwarded only the MAIN model provider's env vars, so a run
whose reviewer sits at a different vendor got an advisor with no
credentials. Caught on the first container smoke: the advisor fired
twice and both calls died on "Missing credentials".

That failure mode is quiet by design — a failed consultation leaves the
worker to carry on, and it still solved the task, so the job reported
reward 1.0 with an advisor that never answered once. Anything reading
the score alone would have concluded the advisor worked.

Union the advisor provider's keys into the forwarded set, deduped and
order-preserving. ANTHROPIC_API_KEY stays excluded under subscription in
BOTH roles, so OAuth remains the only route to the subscription.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… too

execute_client_advisor read providers.<name>.api_key straight out of
config.json, making it the one call site in the codebase that ignored
the environment. An advisor pointed at a provider whose key lives in an
env var — how eval containers and most shells supply credentials — was
constructed with api_key="" and died on "Missing credentials", while the
exact same provider worked fine as the main loop.

Use resolve_api_key(), the shared resolver: configured value first, then
the provider's known env vars via the secret store.

Empty stays a legitimate, non-fatal outcome — the Anthropic subscription
path REQUIRES an empty key so the provider falls through to OAuth (a key
would silently outrank it and bill the API). Pinned by a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RUN_ADVISOR_TB21.md covers the luna-worker/opus-5-reviewer pairing: the
two prerequisites (a build with these fixes, and a real `clawcodex login`
— an imported keychain token cannot carry a long run past the refresh
threshold), smoke and full-run commands, the no-advisor control run
needed to read the delta, and the shared-subset comparison rule.

It also spells out how to VERIFY the advisor answered. A failed
consultation degrades quietly, so a job can report reward 1.0 with a
reviewer that never once replied — the reward alone cannot tell you.

`_advisor_sleep` read the clock twice (loop condition, then remainder),
so the remainder could go negative in between and `time.sleep` raises
ValueError on a negative argument. Clamped at zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…over the gaps

Five findings from an adversarial review of the branch.

MAJOR — the per-model max_tokens from fix #3 reached the OpenAI-compatible
wire, where the main loop deliberately sends none and the table value is an
auto-compact reservation rather than a legal request cap (deepseek 384000,
luna 128000; openai_compatible does forward max_tokens). Clamped to 32768
there; the Anthropic branch still takes the table value whole, because on
that wire it IS the request's max_tokens by design.

  Honest scope: I probed DeepSeek at 384000 and it returns 200, so this was
  never a live outage — it is an untested number per provider across ~30 of
  them, where a rejection is a non-retryable 400 that dies on attempt 1 and
  degrades silently. Real, but MINOR in practice, not MAJOR as first called.

MAJOR — ~280 lines of new command and adapter logic had no tests. Added 12
for the /advisor --effort parser (both flag forms, missing and invalid
values, retune-without-model, auto-clears, unset-clears, status render) and
19 for the adapter (advisor key forwarding, ANTHROPIC_API_KEY withheld under
subscription in both roles, settings seeding, the relaxed subscription gate,
kwarg validation). The adapter file is importorskip'd — it runs for people
with harbor installed and is invisible to CI, which is worth knowing.

MINOR — changing the reviewer model no longer carries a stale advisor_effort
across. That is not merely untidy: the xhigh clamp keys on Anthropic model
NAMES, so an xhigh set for an Opus reviewer went out UNCLAMPED to a
newly-selected OpenAI-compatible one. Cleared, and the confirmation says so.

NIT — resolve_max_output_tokens now receives base_url, so per-endpoint
overrides apply as they do in the main loop.

NIT — a consultation that exhausts its retries logs at INFO. It used to be
invisible: the worker carries on and the task can still score, so a run that
quietly lost its advisor looked identical to a healthy one.

Also: /advisor unset --effort <level> now errors instead of silently
discarding the flag.

Verified correct by the same review: the block-list system change is safe on
Minimax (already receives block lists from the main loop) and on the plain
API-key path; the build_anthropic_thinking_kwargs extraction was proven
behavior-preserving differentially across 2688 combinations with 0
divergences; and the test suite survived 8 mutants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All found by mutation testing, all previously invisible.

1. `test_anthropic_takes_the_model_table_value_whole` was VACUOUS. 32768 is
   itself the largest max_output_tokens in the Anthropic family, so no real
   model could distinguish clamped from unclamped and the wire asymmetry the
   whole fix rests on was unpinned — applying the clamp to BOTH wires passed
   green. Now patches the ceiling down to 8192 so the assertion has to mean
   something. Mutant (clamp both wires) → red.

2. The `base_url` passthrough was untested; deleting the kwarg was invisible.
   Mutant → red.

3. The ANTHROPIC_API_KEY strip on the MAIN-model branch was untested, and it
   guards the expensive failure: for a mapped provider it is a no-op, but an
   unmapped one falls back to the all-providers set, and without the strip
   the key rides into the container where it silently outranks OAuth and
   bills the API instead of the subscription. Mutant → red.

Writing (3) turned up a real misreading on my part, now pinned separately:
`_ALL_PROVIDER_ENV_VARS` is the union of the seven MAPPED vendors, so an
unmapped provider's OWN key is never forwarded at all. That is pre-existing
and unrelated to the advisor, but it looks exactly like advisor breakage
from a container log, so it gets its own test saying so.

Also documents why the ceiling is 32768 (a rule — the Anthropic family
maximum — not a taste) and that it also bounds a CLAUDE_CODE_MAX_OUTPUT_TOKENS
override on this wire while the Anthropic wire honours one whole.

Moves the `__main__` block below the new classes in test_advisor_command.py:
it was stranded mid-file, so a direct `python tests/...` run executed 15 of
28 tests and silently skipped the rest — the exact class of quiet no-op this
branch keeps finding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he second credential door

A second review pass caught three issues the first missed, one of them a
regression against the very commit this branch is built on.

M1 — the advisor's OpenAI-compat branch skipped `normalize_reasoning_effort`,
silently reintroducing what #791 (ac741d6) had just fixed for the main loop.
A DeepSeek advisor received `xhigh` — a level DeepSeek does not know — so it
dropped the field and applied its own default. No error; just a level the
user did not ask for, biased DOWNWARD on the setting people reach for when a
task is hard. My comment claiming it "mirrors query.py" was false.

Extracted `normalize_effort_for_provider` so the two call sites share one
implementation, same reasoning as build_anthropic_thinking_kwargs: this hook
has now been forgotten once, and a copy would let it happen again. The
validate-don't-trust guard moves with it (a duck-typed getattr on a MagicMock
answers with a callable, which would otherwise write a repr into the body).

M3 — `_host_env_keys()` defeated the subscription ANTHROPIC_API_KEY
exclusion. We withheld the key from the container's process env in both
roles, then forwarded the host's whole global-config `env` block into the
container's config.json — and `get_secret` reads process env THEN that block,
so a stored key there is found by `resolve_api_key`, takes the API-key path,
and OAuth never engages: silently billing the API on a run that asked for the
subscription. Not currently triggered (this host's block holds only
TAVILY_API_KEY), but the previous commit message asserted "OAuth remains the
only route" as though it held. Both doors are now shut.

M2 — the /advisor status line credited "(inherited from /effort)", asserting a
link that exists only on the registry path and under an eval adapter: the
TUI's /effort writes a session-only field and headless --effort is per-turn,
so on the surface most users are looking at, that inheritance is dead. Names
the setting instead of the command.

Minors: `advisor=` now validates BOTH halves (a bare colon test let
"anthropic:" and ":claude-opus-5" seed a silently inert advisor, where
`fusion=` validates both); `_VALID_ADVISOR_EFFORTS` derives from
VALID_EFFORT_VALUES rather than being a third hand-maintained ladder; the
retry classifier's comment no longer claims a 529 arm that is in fact
unreachable; /advisor's TUI menu hint documents --effort.

Both new fixes mutation-tested (skip the normalize → red; drop the env-block
strip → red). Full suite 9719 passed, 10 skipped, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ximum

_ADVISOR_MAX_OPENAI_WIRE_TOKENS is documented as a RULE — "the OpenAI wire
never gets a larger budget than the most generous Anthropic model" — so pin
it rather than leaving the claim to a comment, the way VALID_THINKING_EFFORT_
LEVELS pins its ladder.

Load-bearing because the anchor is a SINGLE LEGACY ROW: claude-opus-4-
20250514 is 32768 while opus-5, opus-4-8 and fable-5 are all 32000. Pruning
old model rows would drop the family maximum to 32000 and silently turn the
constant's stated rationale into a false statement, with nothing noticing.
Verified against the table rather than taken on trust, and mutation-tested
(32_768 → 32_000 turns it red).

Full suite 9720 passed, 10 skipped, exit 0. One earlier run showed
test_sigterm_triggers_drain failing; it passes 3/3 in isolation, 13/13 on
main, and green on a clean re-run — a load-dependent timing flake in the
same family as the known test_sigint_during_prefetch one, and nothing here
touches signal handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…etic

The "Harbor adapter (3.13)" job installs harbor but ran only
test_headless_usage_events.py, so both adapter test files were invisible to
CI by OMISSION, not by necessity. They open with importorskip("harbor"), so
under the main test (3.11) job they skip silently — this job is the only
place they can run, and a file left out of its list never runs anywhere.
Added both, with a note to add future tests/test_harbor_* files too. The
"SKIPPED IN CI" docstrings were wrong and are corrected.

That change immediately earned its keep: running the real job command turned
up test_subscription_accepted_for_an_anthropic_advisor asserting a
RuntimeError that only occurs when the host has NO Anthropic login. It
passed on a machine without credentials and broke the moment a real
`clawcodex login` landed. Now stubs fresh_subscription_credentials to a
sentinel and asserts identity, so it tests the role gate — which is what it
was always meant to test — rather than the developer's login state.
Verified both ways: green with the oauth file present AND absent.

Note for anyone reproducing the job locally: `uv run --isolated` is safe,
but a bare `uv run` in this repo DELETES and recreates .venv.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericleepi314
ericleepi314 merged commit ce78282 into main Aug 3, 2026
3 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.

1 participant