Skip to content

Anthropic lanes: send the model as written; the prefix picks the route - #443

Open
rejojer wants to merge 23 commits into
mainfrom
feat/claude-agent-config-model
Open

Anthropic lanes: send the model as written; the prefix picks the route#443
rejojer wants to merge 23 commits into
mainfrom
feat/claude-agent-config-model

Conversation

@rejojer

@rejojer rejojer commented Aug 28, 2026

Copy link
Copy Markdown
Member

The Anthropic-native lanes (messages(), anthropic_runner_config(), claude_agent_config()) ignored chat_model; under the two-switch design (api_key= where the documents are, chat= who answers) they were the lanes where chat= did not answer.

Now, on all three:

  • A chat_model you set carries over, and any model name is sent as written — no gate, no model list, no LiteLLM lookup; the destination judges the id. Only the routing prefix is read: bedrock/, vertex_ai/, and azure_ai/ select that channel, litellm/ and anthropic/ drop, and anything else (bare ids, aliases like sonnet, gateway names) ships verbatim on the direct route.
  • messages() constructs the transport the prefix declaresAnthropic, AnthropicBedrock, AnthropicVertex, or AnthropicFoundry (Azure / Microsoft Foundry) — with chat_backend passed to that constructor (aws_region, project_id, resource, …; unset keys fall to each SDK's own env defaults). The anthropic extra floor moves to >=0.122.0, where those clients gain the tool runner. anthropic_runner_config() stays transport-agnostic (the caller builds the client and its kwargs now work on all of them); claude_agent_config() hands the stripped id to the Claude Code CLI and carries the matching switch (CLAUDE_CODE_USE_BEDROCK / _VERTEX / _FOUNDRY) in the config's env, so the prefix picks the channel there too.
  • The stock default never impersonates a choice. Whether chat_model was ever set is recorded at construction and on assignment, not inferred by comparing values. With the untouched stock default, claude_agent_config() omits the model key (the SDK's own default runs, as before) and the two wire-bound surfaces raise needs a model — pass a Claude model=..., or configure chat_model instead of sending a name the user never wrote. Explicitly writing the stock value is a choice like any other and carries.
  • LiteLLM leaves these lanes entirely: no import (2.5–2.7 s and an offline-hang risk on first touch, measured), no provider table, no "claude" substring. The rules are vendor-free — nothing changes if the stock default ever changes vendors.

Compared to the previous revision of this branch, the refusals are gone: Bedrock/Vertex/Azure Claude ids work on every lane, an explicit non-Claude choice goes to the wire and gets the provider's own 404, and error messages no longer claim a name "is not Claude" or leak None.

One deliberate behavior flip from the previous revision: explicitly constructing with the stock value (chat_model="gpt-5.6-sol") now carries it like any chosen model instead of being silently treated as no choice.

Tests: 452 green; every new or flipped test red-verified against the previous revision; the no-frameworks CI leg simulated (agents/anthropic/claude-agent-sdk/boto3/google blocked: 303 passed, 149 skipped); pyright on the two touched modules 36 → 33.

README: main's extraction of the usage guide to docs.pageindex.ai landed mid-PR and is merged in; this branch's earlier README snippet fixes lived in the extracted sections and are superseded by that move. The agent-integration contract now lives at docs.pageindex.ai/sdk/agents, which needs updating for these semantics.

https://claude.ai/code/session_014Zs7gzhCtUgnybaDfASW9U

The Claude Agent SDK lane ignored chat_model: the agent ran on the SDK's
default no matter what the client was told. A chosen Claude chat_model is
now forwarded (LiteLLM's anthropic/ prefix stripped, provider detected via
litellm.get_llm_provider so routed names are refused, not prefix-guessed);
a chosen non-Claude model raises with a pointer to model= and
openai_agent_config(); the never-chosen default keeps the SDK's own model
so existing clients are untouched. New model= takes the SDK's own name
verbatim and wins.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
messages(), anthropic_runner_config(), and claude_agent_config(model=) all
hand the name to Anthropic directly, where the client's LiteLLM spelling
(anthropic/claude-x) is a 404 — verified live. A method on the PageIndex
client takes the client's spelling: the prefix is dropped, by string strip
so SDK aliases like "sonnet" still pass. max_tokens defaults now resolve on
the stripped id as well.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
@rejojer
rejojer force-pushed the feat/claude-agent-config-model branch from 83f3422 to f7266fa Compare August 28, 2026 13:15
Same rule as claude_agent_config(): model= left unset takes the chosen
Claude chat_model with LiteLLM's anthropic/ prefix stripped; any other
provider raises with a pointer to model=. These two have no SDK default to
fall back on, so the never-chosen gpt default raises as well — as it did
when model= was missing, now readably. model= relaxes to Optional on both;
positional callers are unaffected. One resolver serves all three surfaces.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
LiteLLM resolves bare names through its model map, so a Claude release
newer than the installed LiteLLM (or a -latest alias) raised BadRequest and
was refused as "not Claude". LiteLLM stays the authority; a name it cannot
resolve falls back to Anthropic's own rule: every Claude id starts with
"claude". Prefixed anthropic/ names were never affected.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
Folded in from the docs branch (#442): a new "MCP server" item under (b)
with the hosted config verbatim from docs.pageindex.ai/mcp; the Claude
Agent SDK item names ANTHROPIC_API_KEY, the chat_model rule, and model= in
the explicit form; messages() and the tool runner examples drop model=
and say they assume a Claude chat_model; the OpenAI Agents explicit form
notes the litellm/ prefix and cache settings the helper adds; wording
touches on the (b) intro and the Anthropic SDK item title.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
@rejojer

rejojer commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Re-checked at 11c74b7, which folded the two model-name paths into one resolver: the litellm/ spelling now resolves on chat_model and explicit model= alike, and max_tokens resolves on the stripped id.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Explicit model= went through a prefix strip of its own while chat_model
went through LiteLLM; the two disagreed on litellm/ (dropped everywhere
else in the client, refused here) and were two code paths for one job.
Both now read the name the client's way: litellm/ dropped, LiteLLM names
the provider, anthropic/ goes. The fallback for a name LiteLLM cannot
place is this lane's rule — no provider prefix means Anthropic's own — so
Claude Agent SDK aliases and unreleased ids reach the destination instead
of being guessed at by a "claude" prefix.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
The OpenAI Agents explicit form said "local clients only" where the
condition is own-model chat (cloud with chat= included; None on managed
chat); the tool-runner explicit form dropped the cache_control the helper
sets, so a copy lost prompt caching silently; ANTHROPIC_API_KEY is named
on every Anthropic lane, not just the Claude Agent SDK; one phrasing for
the chat_model rule across the three.

Claude-Session: https://claude.ai/code/session_01F84VYrC858n9cr5eRjV4Cy
The docstring ended on "anything else is refused, like every other
provider", but the fallback it had just described does the opposite: a
slash-free name LiteLLM cannot place is passed through for the
destination to judge, which is the whole point of the fallback. Only a
name LiteLLM places with another provider, or an unplaceable prefixed
one, is refused. All three branches now read as the code runs.

Claude-Session: https://claude.ai/code/session_01W6ZhgbVFJxjfzRWya7rPbg
_claude_model_name() leaves litellm alone when there is no name to
place. On a managed-chat cloud client, anthropic_runner_config() without
model= imported litellm synchronously before _preload_litellm had
stamped LITELLM_LOCAL_MODEL_COST_MAP: a multi-second network fetch of
the model map, paid for an error the call raises anyway. Now it raises
in a millisecond.

litellm prints a red "Provider List" banner to stdout before raising on
a name it cannot place, which is every alias and every id newer than its
map, the exact names the fallback exists for; suppress_debug_info is its
switch for that.

claude_agent_config() resolves the model before building the config,
the order anthropic_runner_config() already has, so a non-Claude
chat_model no longer pays the MCP initialize round trip before raising.
The DEFAULT_CHAT_MODEL import moves into the branch that uses it: a
managed-chat cloud client no longer imports utils, and with it
load_dotenv() into os.environ, from a config getter.

README: the two examples that lean on a Claude chat_model say so inline,
since the Step 1 client's chat_model is not one. Tests match the gate's
own message; "model=" also matched the messages() own-chat guard.

Claude-Session: https://claude.ai/code/session_01TkZw9WFCEZKbBy7HNaX8oF
@rejojer

rejojer commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Re-checked at 08ac1dc, which resolves the model before building the config and drops the resolver's side effects. Verified by execution: the no-name path raises in ~18 ms without importing litellm, and the "Provider List" banner is gone from every branch.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

The litellm gate is gone: messages(), anthropic_runner_config() and
claude_agent_config() now read only the routing prefix (bedrock/,
vertex_ai/, anthropic/, litellm/) and ship the id verbatim — the
destination judges it. messages() constructs the transport class the
prefix declares (AnthropicBedrock / AnthropicVertex; anthropic
>= 0.122.0 has their tool runner). The stock chat_model default is
tracked as a construction-time flag instead of a value compare, and it
never impersonates a choice: the claude lane omits the model key, the
two wire-bound surfaces ask for one.

Claude-Session: https://claude.ai/code/session_014Zs7gzhCtUgnybaDfASW9U
LiteLLM already names the route (azure_ai/, Claude entries in its map),
AnthropicFoundry ships at the 0.122.0 floor, and Claude Code serves the
channel via CLAUDE_CODE_USE_FOUNDRY — every roster condition holds, so
the row costs one tuple element and one class mapping.

Claude-Session: https://claude.ai/code/session_014Zs7gzhCtUgnybaDfASW9U
@rejojer rejojer changed the title claude_agent_config(): forward a Claude chat_model, add model= Anthropic lanes: send the model as written; the prefix picks the route Aug 31, 2026
@rejojer

rejojer commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- Construction failures wrap as "backend is not configured" on every
  route: Vertex and Foundry refuse a missing region or credential at
  construction, each with its own exception type; only the direct
  route's TypeError was caught.
- A tool-runner probe where the routes converge: an anthropic build
  predating a route's runner passed _require_anthropic and died in an
  AttributeError; it now names the gap and the upgrade.
- A chat_model set in config.yaml counts as chosen, exactly like the
  constructor spellings (blank values mean absent, also like them); the
  Anthropic surfaces no longer refuse a model the chat lanes honor.
- claude_agent_config() carries a bedrock/vertex_ai/azure_ai prefix as
  the matching CLAUDE_CODE_USE_* switch. Foundry ids double as valid
  direct ids, so without the switch that channel ran the wrong
  transport with no signal; now the prefix picks the channel here too.

Claude-Session: https://claude.ai/code/session_01L3kPdmRDLFP2iqJtuwdRgD
Version numbers in messages drift — this branch moved the floor and
left two copies stale. Messages and docstrings now say what is
missing; pyproject's constraint is the single source. _claude_wire's
docstring also gains the azure_ai row it routes.

Claude-Session: https://claude.ai/code/session_01L3kPdmRDLFP2iqJtuwdRgD
Comment thread tests/test_agent_tools.py
# config.yaml is the third way to name a chat model; a key set there
# must read as chosen, exactly like the constructor spellings.
pytest.importorskip("anthropic")
import pageindex.utils
Comment thread tests/test_local_chat.py
# surface has no tool runner: name the gap, not an AttributeError.
class _Runnerless:
class beta:
class messages: ...
- The route tests' importorskip guards asked for boto3/google-auth that
  the tests never use (explicit-credential construction, no network), so
  every CI leg skipped them and _ROUTE_CLIENTS had no executed coverage.
  Deleted; verified green with those imports blocked.
- gate ran under always(), which also fires on a cancelled run, so
  cancel-in-progress turned every superseded push into a red required
  check. !cancelled() keeps fail-on-red without the false red.
- The stretch between transport construction and the runner handoff (the
  ceiling probe, the tool_runner build) could raise past the close the
  branches below own; one close-on-failure now covers the whole window,
  and the version-gap probe folds into it.

Claude-Session: https://claude.ai/code/session_01EhcWJpnngbgHAbx5MxQuu5
@rejojer

rejojer commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Bedrock resolves credentials per request and fails with a bare
RuntimeError (anthropic/lib/bedrock/_auth.py); Vertex with google.auth's
own types. Neither is AnthropicError nor the direct route's TypeError,
so a missing credential — first-run state on those channels — escaped
messages() as a raw third-party exception. 90c9835 widened the
construction-time catch for exactly this reason; this is the
request-time half, wrapped with the same message. The direct route's
handlers are untouched (route-gated), and non-credential exceptions
still propagate.

Three doc truths alongside: messages()'s summary no longer claims every
call drives /v1/messages under ANTHROPIC_API_KEY (the Args' three
routes say otherwise); claude_agent_config()'s bundle description names
the model/env keys a chosen model adds ("three slots" undersold it
since the route carry landed); the growth-rule comment stops claiming
Mantle waits on a LiteLLM prefix name — litellm 1.97.0 ships
bedrock_mantle and the SDK ships AnthropicBedrockMantle, so Mantle
clears both bars and waits only on someone asking.

Claude-Session: https://claude.ai/code/session_018QVVYEbeo639N97j3TgD1j
Seven round-8 review fixes, three of them this PR's own features
finishing incompletely:

- claude_agent_config: a written route prefix now blanks the other
  CLAUDE_CODE_USE_* switches ("" is off to the CLI; "0" would read as
  on) and anthropic/ blanks all three. ClaudeAgentOptions.env merges
  OVER the inherited environment and the CLI reads any set switch by
  its own fixed precedence, so an exported CLAUDE_CODE_USE_BEDROCK=1
  silently beat an explicit vertex_ai/ prefix — and beat anthropic/
  outright, which emitted no env at all. Bare names still leave env
  out: they name a model, not a channel.

- _default_max_tokens takes the route and looks the ceiling up in the
  route's own spelling via get_model_info (model_cost misses the
  bedrock region-namespaced ids): the stripped wire id resolved
  nowhere, so an enabled thinking budget sailed past the channel's
  output ceiling into a guaranteed 400. anthropic_runner_config keeps
  the route it used to discard, for the same lookup.

- _litellm_claude_marks: azure_ai joins the provider tuple — the one
  route table the PR missed. Claude-on-Foundry was silently repaying
  full prompt price every turn on the LiteLLM lanes.

- run_messages names the missing route extra: pageindex[anthropic]
  does not carry boto3/google-auth, so a route's first request died as
  a bare ModuleNotFoundError whack-a-mole instead of naming
  anthropic[bedrock] / anthropic[vertex]. A tool's own missing module
  stays raw.

- The "set ANTHROPIC_API_KEY" remediation is direct-route-only now
  (_not_configured): AnthropicVertex has no api_key parameter at all,
  so the advice could not work where it was being given.

- owns_transport snapshots _ANTHROPIC_CLIENTS.values() via list():
  dict_values has no __contains__, so membership iterates the live
  view and a concurrent setdefault raises RuntimeError mid-scan.

- gate: back to always(). GitHub counts a SKIPPED required check as
  passing, so !cancelled() let a cancelled run clear the one required
  check on main with zero legs completed. The cost is a cosmetic red
  on superseded shas, which required checks never read.

_ROUTE_ENV now sources _CLAUDE_ROUTES (byte-identical, declared 21
lines apart) and a table-agreement test makes the third copy
(_ROUTE_CLIENTS) and the marks predicate loud on drift. Docstring
truth alongside: messages() backend keys are the selected route's own;
anthropic_runner_config names the client class a routed prefix pairs
with; the constructor points cross-surface Claude users at the
anthropic/ spelling.

Claude-Session: https://claude.ai/code/session_01GRxEKSfeDL4frnfc58ZtMS
An anthropic build predating AnthropicFoundry died on the azure_ai
route with a bare AttributeError at the getattr, while the very next
step — the tool-runner probe — hands the same category of failure (an
old build missing a piece the route needs) a named gap and the
upgrade pointer. Resolve the class explicitly and give the missing
case that same message, one step earlier.

Claude-Session: https://claude.ai/code/session_01GRxEKSfeDL4frnfc58ZtMS
@rejojer

rejojer commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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