Anthropic lanes: send the model as written; the prefix picks the route - #443
Anthropic lanes: send the model as written; the prefix picks the route#443rejojer wants to merge 23 commits into
Conversation
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
83f3422 to
f7266fa
Compare
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
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Re-checked at 🤖 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
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Re-checked at 🤖 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
Our branch's README edits lived in the extracted sections; main's version is taken whole. Claude-Session: https://claude.ai/code/session_014Zs7gzhCtUgnybaDfASW9U
Branch protection points at job names, and matrix job names change; gate is the one name that never does — all legs green, gate green. Claude-Session: https://claude.ai/code/session_014Zs7gzhCtUgnybaDfASW9U
Code reviewNo 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
| # 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 |
| # 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
Code reviewNo 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
Code reviewNo 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 👎. |
The Anthropic-native lanes (
messages(),anthropic_runner_config(),claude_agent_config()) ignoredchat_model; under the two-switch design (api_key=where the documents are,chat=who answers) they were the lanes wherechat=did not answer.Now, on all three:
chat_modelyou 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/, andazure_ai/select that channel,litellm/andanthropic/drop, and anything else (bare ids, aliases likesonnet, gateway names) ships verbatim on the direct route.messages()constructs the transport the prefix declares —Anthropic,AnthropicBedrock,AnthropicVertex, orAnthropicFoundry(Azure / Microsoft Foundry) — withchat_backendpassed to that constructor (aws_region,project_id,resource, …; unset keys fall to each SDK's own env defaults). Theanthropicextra 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'senv, so the prefix picks the channel there too.chat_modelwas 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 raiseneeds a model — pass a Claude model=..., or configure chat_modelinstead of sending a name the user never wrote. Explicitly writing the stock value is a choice like any other and carries."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