Skip to content

Feat/claude model selection - #11

Merged
emanuilo merged 17 commits into
emanuilo:mainfrom
mistercap179:feat/claude-model-selection
Oct 5, 2026
Merged

emanuilo merged 17 commits into
emanuilo:mainfrom
mistercap179:feat/claude-model-selection

Conversation

@mistercap179

Copy link
Copy Markdown
Contributor

Summary

Adds Claude model and effort-level selection to Memclaw.

Until now, the Claude backend always ran a hardcoded claude-sonnet-4-6 with no way to change the model or how deeply it thinks. The setup wizard now asks both questions, and the answers are stored as ordinary config values.

What changed

Live model discovery

memclaw/anthropic_models.py

  • Fetches the available models from Anthropic's /v1/models using the credential the user just entered, so a newly released model appears without a Memclaw release. Both credential types are supported (OAuth bearer token and x-api-key); they are not interchangeable, so the auth header is chosen per mode.
  • Reads each model's effort capability tree and keeps only the levels the installed claude-agent-sdk accepts (low / medium / high / xhigh / max), so a level a future API adds drops out on its own instead of reaching the SDK.
  • Models are sorted newest first, and the list is cached in ~/.memclaw/models_cache.json for 24h so repeated memclaw configure runs don't re-hit the API. A missing, stale, or malformed cache silently falls back to the network.

Wizard

memclaw/backends/claude.py

  • After the credential step, a numbered model picker is shown with the current model preselected, followed by an effort picker — but only for models that report effort support.
  • For a model with no effort support, the effort question is skipped and any level left over from a previous choice is dropped, so it cannot be sent to a model that would reject it.
  • Every failure path (no network, bad key, empty list) warns once and keeps the current model. Picking a model is a convenience and must never be the reason memclaw configure cannot finish.

Config and runtime

  • Adds ANTHROPIC_MODEL and ANTHROPIC_EFFORT settings, resolved from the config file or the environment like every other value.

  • Both settings are optional:

    • An unset model falls back to the built-in default.
    • An unset or unrecognised effort level makes the SDK omit the flag entirely.
  • The backend passes the resolved model and effort to ClaudeAgentOptions.

  • memclaw status shows the active model and effort, but only when Claude is the active backend — printing them under Cursor would name a model that isn't going to run.

Dependency

  • Bumps claude-agent-sdk to >=0.2.139,<0.3, the first version exposing ClaudeAgentOptions.effort.

Tests

New coverage for:

  • Model fetching and parsing
  • Effort filtering
  • Cache read/write/expiry
  • Model and effort resolution
  • Wizard picker, including skip-effort and fetch-failure paths
  • Backend tests isolated from the developer's own environment so a local ANTHROPIC_MODEL cannot change the result

Notes

Defaults are unchanged: an existing install that accepts every prompt keeps running the same model it ran before.

README documents both variables and the new wizard steps.

The previous range capped at 0.1.x, which has no `effort` option. 0.2.x
exposes it as Optional[Literal["low","medium","high","xhigh","max"]],
which the upcoming model/effort selection needs.

Verified against the installed 0.2.152 that every ClaudeAgentOptions
field this project uses still exists: env, model, system_prompt,
setting_sources, mcp_servers, allowed_tools, disallowed_tools,
permission_mode, max_turns.

Test suite is unchanged by the bump: 178 passed, 4 failed. The same four
failures (tests/backends/test_mcp_bridge.py) occur on main before this
commit. They are Windows-only and unrelated: mcp_bridge.py passes
fd= to uvicorn, which resolves it via socket.fromfd(..., AF_UNIX, ...),
and AF_UNIX does not exist on Windows.
Adds memclaw/anthropic_models.py, which asks /v1/models what the
configured credential can reach and reads each model's effort support off
its capability tree. The setup wizard will build its model picker from
this, so a model Anthropic releases needs no code change here.

Shaped after openai_health.py: probe an API, return a small dataclass
report, leave presentation to the caller.

Notes on the pieces that are easy to get wrong:

- Auth headers differ by credential. An OAuth token sent as x-api-key is
  rejected 401, so subscription and API-key modes build separate headers.
- Effort levels are filtered through SDK_EFFORT_LEVELS, mirroring what
  ClaudeAgentOptions.effort accepts. A level Anthropic ships before our
  SDK understands it is dropped rather than passed through.
- Results are cached for 24h in ~/.memclaw/models_cache.json. A missing,
  malformed or future-dated cache falls back to the network instead of
  raising; the wizard must never break over a cache file.
- The endpoint pages at 20 by default, so limit=100 is requested.

Verified against the live API with a subscription token: 11 models,
correctly ordered newest first. Effort support turns out to be graded
rather than all-or-nothing, which the capability tree handles for free:
Sonnet 4.6 supports low/medium/high/max but not xhigh, Opus 4.5 stops at
high, and Haiku 4.5 and Sonnet 4.5 report no effort support at all.

23 new tests, all HTTP mocked; the suite still needs no keys and no
network. Full run: 201 passed, plus the 4 pre-existing Windows-only
mcp_bridge failures that also fail on main.
Two new MemclawConfig fields alongside cursor_model, populated from the
environment the same way CURSOR_MODEL already is. Empty means "use the
built-in default", so nothing changes for an existing install until the
wizard starts writing these keys.

They sit in their own Claude block rather than inside the Cursor one, to
keep the two backends visually separate.

204 passed, plus the 4 pre-existing Windows-only mcp_bridge failures.
The Claude backend's wizard_setup now follows the credential question with a
model picker built from the live /v1/models list, and an effort picker holding
only the levels that model actually accepts. A model that supports no effort
skips the second question entirely and drops any level left over from an
earlier choice, so an unsupported value can never reach the SDK.

Fetching the list is a convenience, not a requirement: any failure prints one
warning and keeps the current model, so the wizard always finishes.
Both run_one_shot and run_turn now take the model and effort off the config
instead of the hardcoded constant. _MODEL stays as the fallback for an unset
value, and an effort the SDK does not recognise resolves to None, which makes
the SDK omit the flag rather than pass it to the CLI.

Both are resolved once in __init__, mirroring how the Cursor backend resolves
its model.

Verified against the real SDK, not just the mocks: a one-shot turn succeeds
with effort=high on Sonnet 4.6 and with effort unset on Haiku 4.5.
The panel now reports which model a turn will actually use and which effort
level goes with it, resolved through the same helpers the backend uses, so
status cannot drift from runtime.

Both lines are skipped when Cursor is the active backend — naming a Claude
model there would describe something that is not going to run. "default"
means no level is set and the model decides.
Adds the cases the spec asks for that the backend tests did not yet have:
an empty ANTHROPIC_MODEL falls back to the built-in default, an effort
level the SDK does not know is ignored, both values reach ClaudeAgentOptions,
and a failed model fetch warns once instead of raising out of the wizard.
The autouse fixture already stripped the credential env vars, but not
ANTHROPIC_MODEL/ANTHROPIC_EFFORT. Importing memclaw.config loads
~/.memclaw/.env into os.environ, so once a developer had run the wizard,
the three default-fallback tests asserted against that developer's own
model instead of the built-in default.
Comment thread memclaw/anthropic_models.py Outdated
Comment thread memclaw/anthropic_models.py Outdated
Comment thread memclaw/cli.py Outdated
emanuilo
emanuilo previously approved these changes Sep 23, 2026
Comment thread memclaw/config.py Outdated
Comment thread memclaw/backends/claude.py Outdated
Comment thread memclaw/backends/claude.py Outdated
Comment thread memclaw/backends/claude.py Outdated
Comment thread memclaw/backends/claude.py
Comment thread memclaw/backends/claude.py
Comment thread tests/test_anthropic_models.py Outdated
Comment thread memclaw/backends/claude.py Outdated
Comment thread memclaw/anthropic_models.py Outdated
Comment thread README.md Outdated
anthropic_models.py and backends/claude.py imported from each other, so
both sides needed in-function imports. fetch_models() now takes the auth
mode and credential as plain strings, like probe_openai(api_key), so the
module no longer needs anything from claude.py and lives next to it as
backends/claude_models.py. All imports in claude.py are back at the top.

This also removes _probe_config, which built a whole MemclawConfig (and
its folders and files) just to carry one credential.

The model-list cache is gone: it could serve a list fetched with a
previous credential, and the picker only runs from `memclaw configure`.
The OAuth beta header is dropped too, since /v1/models doesn't need it.
The Claude CLI reads ANTHROPIC_MODEL itself, so someone who exports it
for Claude Code would silently change Memclaw's model too. The new names
match CURSOR_MODEL, and a test pins that ANTHROPIC_MODEL is ignored.
The status command checked for the Claude backend by name and imported
its private helpers, which base.py says nothing outside a backend should
need. Backends now implement status_rows(config) -> [(label, value)]:
Claude returns model and effort, Cursor returns its model, and status
prints whatever the active backend gives it.
If the current model wasn't an exact id in the fetched list (an alias
like `opus`, or a value set only in the shell), the picker fell back to
the newest model, so pressing Enter changed the model. The current model
now gets its own row and stays the default; keeping it leaves both
CLAUDE_MODEL and CLAUDE_EFFORT untouched. Like CURSOR_MODEL, a value
exported only in the shell counts as current.

The Panel + numbered rows + Prompt.ask pattern repeated by the platform,
backend, auth, model and effort pickers now lives in one helper,
memclaw.prompts.choose. It sits outside setup.py so backends can import
it at module level.
The fallback cost still used Sonnet prices, which is wrong once the
model can be Opus or Haiku. Prices are now looked up per model (by id
prefix, so dated snapshots match); an unknown model is estimated at the
default model's price. The SDK normally reports total_cost_usd itself,
so this only covers turns where it doesn't.

Also renames _MODEL to _DEFAULT_MODEL to match cursor.py and refreshes
the out-of-date "Sonnet 4 pricing" and "claude-agent-sdk 0.1.x" notes.
The env-isolation fixture and config helper were copied between test
files, and the copy didn't clear the model/effort vars. They now live in
tests/conftest.py as isolate_claude_env and the claude_config factory,
used by the Claude backend, config and status tests. The model-discovery
tests no longer need either, since fetch_models takes plain strings.

Adds end-to-end tests for ClaudeAgentBackend.wizard_setup: auth choice,
credential, model and effort, including a fetch failure that must still
save the credential. The "(spec x.y)" references are gone, since that
spec isn't in the repo.
The env-var table rows already cover the details.
memclaw imports mcp directly (mcp_tools.py, mcp_bridge.py) but never
declared it, so CI's unpinned pip install pulled mcp 2.x, which removed
the low-level Server decorators.
@emanuilo

emanuilo commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Its great now! Thanks a lot!

@emanuilo
emanuilo merged commit 7993794 into emanuilo:main Oct 5, 2026
4 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