Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d7b14ea
claude_agent_config(): forward a Claude chat_model, add model=
rejojer Aug 28, 2026
593f255
Accept the anthropic/ spelling on every explicit model=
rejojer Aug 28, 2026
f7266fa
Reflow the claude_agent_config model docstring
rejojer Aug 28, 2026
00d493d
messages() and anthropic_runner_config() carry a Claude chat_model too
rejojer Aug 28, 2026
fa84f18
Fall back to the "claude" prefix when LiteLLM does not know the id
rejojer Aug 28, 2026
6fad807
docs: README for the MCP server item and the Anthropic-lane model rules
rejojer Aug 28, 2026
11c74b7
One resolver for every model name on the Anthropic lanes
rejojer Aug 28, 2026
87b428f
docs: align the Anthropic-lane items
rejojer Aug 28, 2026
6cbb2eb
docs: align the cache_control comment column
rejojer Aug 28, 2026
5668e08
Say which names the Anthropic-lane resolver refuses
rejojer Aug 28, 2026
08ac1dc
Resolve Anthropic-lane models without side effects
rejojer Aug 28, 2026
cb9bcff
Send Anthropic-lane models as written; a prefix picks the route
rejojer Aug 31, 2026
3d18929
Wire the azure_ai route to the Foundry client
rejojer Aug 31, 2026
c80ce07
docs: the Foundry client predates the 0.122 floor
rejojer Aug 31, 2026
cc603ba
Merge main: the README usage guide moved to docs.pageindex.ai
rejojer Aug 31, 2026
5f77206
CI: one stable gate check over the test matrix
rejojer Aug 31, 2026
57e8b5a
docs: chat_model's constructor doc names its Anthropic-lane readers
rejojer Aug 31, 2026
90c9835
Anthropic routes: four review fixes
rejojer Aug 31, 2026
1bf82d8
docs: name the capability; the version floor lives in pyproject alone
rejojer Aug 31, 2026
1c29b6b
Round-6 leftovers: route coverage, calm gate, sealed handoff window
rejojer Aug 31, 2026
1466190
Anthropic routes: request-time credential failures keep the contract
rejojer Aug 31, 2026
da7624e
Anthropic routes: prefix beats env, routed ceilings, Foundry marks, gate
rejojer Aug 31, 2026
495f364
A missing route client class gets the probe's contract
rejojer Aug 31, 2026
bad394d
Anthropic lanes: stop second-guessing the third parties
rejojer Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,13 @@ jobs:
- if: matrix.pdfium == '4'
run: pip install "pypdfium2<5"
- run: python -m pytest -q

gate:
needs: tests
# always(), because GitHub counts a SKIPPED required check as
# passing: skipping on cancel would green-light a commit with zero
# legs run. A cancelled run must go red here, not vanish.
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- run: test "${{ needs.tests.result }}" = "success"
210 changes: 175 additions & 35 deletions pageindex/client.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pageindex/integrations/anthropic_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def build_anthropic_tools(client, include_management: bool = False,
except ImportError as exc:
raise PageIndexAPIError(
"as_anthropic_tools requires the Anthropic SDK tool runner "
"(anthropic>=0.108.0) — pip install -U anthropic (or pip install "
"— pip install -U anthropic (or pip install "
"'pageindex[anthropic]')."
) from exc
from ..agent_tools import _tool_specs
Expand Down
133 changes: 74 additions & 59 deletions pageindex/local_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,10 @@ def _reported_model(model_name: str) -> str:
def _litellm_claude_marks(wire: str) -> Optional[dict]:
"""Claude's prompt caching is opt-in per request: on Claude models
routed through LiteLLM (Anthropic direct, Bedrock, Vertex — each
channel live-verified), mark the managed system prefix and the newest
message via LiteLLM's injection param so the loop's later turns and a
conversation's next calls read them instead of repaying full price.
live-verified — and Foundry, same injection), mark the managed system
prefix and the newest message via LiteLLM's injection param so the
loop's later turns and a conversation's next calls read them instead
of repaying full price.
``wire`` is the name LiteLLM itself resolves — each lane strips its
own routing prefixes first, because the lanes normalize differently
(the chat wire treats bare names as OpenAI shorthand; the Agents SDK
Expand All @@ -248,8 +249,9 @@ def _litellm_claude_marks(wire: str) -> Optional[dict]:
model, provider, _, _ = get_llm_provider(model=wire)
except Exception:
return None
if provider == "anthropic" or (provider in ("bedrock", "vertex_ai")
and "claude" in model.lower()):
if provider == "anthropic" or (
provider in ("bedrock", "vertex_ai", "azure_ai")
and "claude" in model.lower()):
# The stable prefix plus the newest message, so each turn re-reads
# the turns before it. LiteLLM seeds nothing unprompted, so this
# pair is the marks' sole source.
Expand Down Expand Up @@ -878,33 +880,50 @@ def _require_anthropic() -> None:
from anthropic.lib.tools import ToolError # noqa: F401
except ImportError as exc:
raise PageIndexAPIError(
"messages requires anthropic >= 0.108.0 (the tool "
"runner with ToolError) — pip install -U anthropic."
"messages requires the anthropic SDK tool runner "
"(with ToolError) — pip install -U anthropic."
) from exc


_ANTHROPIC_CLIENTS: dict = {} # backend key -> client, kept open for reuse
_ANTHROPIC_CLIENTS: dict = {} # (route, backend) key -> client, kept open

# The transport class per routing prefix — the anthropic SDK ships one
# client per channel, so a row here is what makes a route reachable.
_ROUTE_CLIENTS = {"anthropic": "Anthropic", "bedrock": "AnthropicBedrock",
"vertex_ai": "AnthropicVertex",
"azure_ai": "AnthropicFoundry"}

def _anthropic_client(backend=None):

def _anthropic_client(backend=None, route="anthropic"):
"""The backend client — the seam tests replace with a fake transport.
One client per backend: each construction pays ~45 ms of SSL-context
build and a cold connection pool. A backend whose values defeat
hashing constructs per call, as before."""
``route`` (declared by the model's prefix) picks the SDK client
class. One client per (route, backend): each construction pays
~45 ms of SSL-context build and a cold connection pool. A backend
whose values defeat hashing constructs per call, as before."""
import anthropic
kwargs = _sdk_backend(backend)
try:
key = tuple(sorted(
key = (route, tuple(sorted(
(k, tuple(sorted(v.items())) if isinstance(v, dict) else v)
for k, v in kwargs.items()))
for k, v in kwargs.items())))
hash(key)
except TypeError:
key = None
if key in _ANTHROPIC_CLIENTS:
return _ANTHROPIC_CLIENTS[key]
cls = getattr(anthropic, _ROUTE_CLIENTS[route], None)
if cls is None:
# A build predating this route's client class: same contract as
# the tool-runner probe, one step earlier.
raise PageIndexAPIError(
f"messages on this route needs the anthropic SDK's "
f"{_ROUTE_CLIENTS[route]} client, which this anthropic build "
"lacks — pip install -U anthropic.")
try:
client = anthropic.Anthropic(**kwargs)
except TypeError as exc:
client = cls(**kwargs)
except (anthropic.AnthropicError, ValueError, TypeError) as exc:
# Vertex/Foundry refuse a missing region or credential right at
# construction, each with its own type; same contract for all.
raise PageIndexAPIError(
f"The Anthropic backend is not configured: {exc}") from exc
if key is not None and len(_ANTHROPIC_CLIENTS) < 8:
Expand Down Expand Up @@ -988,23 +1007,18 @@ def _default_max_tokens(model: str, thinking=None) -> int:
"""The wire-required per-turn budget when the caller sets none: 8192,
except the claude-3 generation whose output ceiling is 4096. The wire
also requires max_tokens > thinking.budget_tokens, so an enabled
budget lifts the default above itself — clamped to the model's output
ceiling where LiteLLM's capability map knows it."""
budget lifts the default above itself. Pure arithmetic on the
caller's own inputs — whether the sum fits the model's output ceiling
is the API's own ruling (its 400 names both numbers), never a lookup
here."""
budget = (thinking.get("budget_tokens")
if isinstance(thinking, dict) else None)
if isinstance(budget, int) and not isinstance(budget, bool):
want = budget + 8192
try:
import litellm
ceiling = (litellm.model_cost.get(model)
or {}).get("max_output_tokens")
except Exception:
ceiling = None
return min(want, ceiling) if ceiling else want
return budget + 8192
return 4096 if model.startswith(_CLAUDE_4096_MODELS) else 8192


def run_messages(client, messages, model: str,
def run_messages(client, messages, model: str, route: str = "anthropic",
max_tokens: Optional[int] = None,
stream: bool = False, doc_id=None, system=None,
temperature: Optional[float] = None,
Expand Down Expand Up @@ -1047,25 +1061,42 @@ def run_messages(client, messages, model: str,
# network I/O, and a failure there must not strand the client below.
tools = build_anthropic_tools(client, doc_ids=scope)
merged = _merged_backend(client, backend)
backend_client = _anthropic_client(merged)
backend_client = _anthropic_client(merged, route)
# Close only a per-call construction: cached clients stay open for
# reuse; a caller-owned http_client survives regardless.
# list(): an atomic snapshot — a bare .values() scan breaks under a
# concurrent setdefault.
owns_transport = ("http_client" not in (merged or {})
and backend_client not in _ANTHROPIC_CLIENTS.values())
if max_tokens is None:
max_tokens = _default_max_tokens(model, thinking)
runner = backend_client.beta.messages.tool_runner(
max_tokens=max_tokens,
messages=prepared,
model=model,
tools=tools,
system=system_blocks,
stream=stream,
# Bounded like the OpenAI surfaces (their framework default is 10).
max_iterations=max_turns if max_turns is not None else 10,
**passthrough,
**cached,
)
and backend_client
not in list(_ANTHROPIC_CLIENTS.values()))
try:
if not hasattr(backend_client.beta.messages, "tool_runner"):
# An anthropic build predating this route's tool runner passes
# _require_anthropic (its probes are older): name the gap here.
raise PageIndexAPIError(
"messages on this route needs the anthropic SDK's tool "
"runner, which this anthropic build lacks — "
"pip install -U anthropic.")
if max_tokens is None:
max_tokens = _default_max_tokens(model, thinking)
runner = backend_client.beta.messages.tool_runner(
max_tokens=max_tokens,
messages=prepared,
model=model,
tools=tools,
system=system_blocks,
stream=stream,
# Bounded like the OpenAI surfaces (their framework default is 10).
max_iterations=max_turns if max_turns is not None else 10,
**passthrough,
**cached,
)
except BaseException:
# A failure before the runner handoff must not strand the transport
# the branches below would have closed.
if owns_transport:
backend_client.close()
raise

if stream:
def events() -> Iterator[Any]:
Expand All @@ -1075,14 +1106,6 @@ def events() -> Iterator[Any]:
yield event
except anthropic.AnthropicError as exc:
raise _model_backend_error(exc, "messages", client) from exc
except TypeError as exc:
# the SDK's request-time credential-resolution failure
if "authentication" not in str(exc).lower():
raise
raise PageIndexAPIError(
"The Anthropic backend is not configured: set the "
"ANTHROPIC_API_KEY environment variable, or pass an "
f"api_key in chat_backend / backend. ({exc})") from exc
finally:
# runs on exhaustion and abandonment (GeneratorExit) alike
if owns_transport:
Expand All @@ -1093,14 +1116,6 @@ def events() -> Iterator[Any]:
turns = [turn for turn in runner]
except anthropic.AnthropicError as exc:
raise _model_backend_error(exc, "messages", client) from exc
except TypeError as exc:
# the SDK's request-time credential-resolution failure
if "authentication" not in str(exc).lower():
raise
raise PageIndexAPIError(
"The Anthropic backend is not configured: set the "
"ANTHROPIC_API_KEY environment variable, or pass an "
f"api_key in chat_backend / backend. ({exc})") from exc
finally:
# safe here: the params read-back below does no HTTP
if owns_transport:
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ python-dotenv = ">=1.0.0"
pyyaml = ">=6.0"
# Older releases break string prompts with SDK MCP servers (#597, #780).
claude-agent-sdk = { version = ">=0.1.53", optional = true }
# Older releases execute a refusal turn's tool_use blocks.
anthropic = { version = ">=0.108.0", optional = true }
# Pre-0.122 lacks the Bedrock/Vertex tool runner; pre-0.108 executes a
# refusal turn's tool_use blocks.
anthropic = { version = ">=0.122.0", optional = true }

[tool.poetry.extras]
claude = ["claude-agent-sdk"]
Expand Down
Loading
Loading