Model providers and custom endpoints¶
Which model an agent runs, and against which endpoint, is a ledger scalar per agent —
model, skill_model, provider, and an endpoint: block. agentctl agents set <name>
<key> <value> records it; apply renders it.
A pi model mapping can also set the request tier and reasoning effort:
model: { provider: openai, name: gpt-6-luna, service_tier: fast, reasoning_effort: low }
service_tier currently accepts fast and requires a pinned OpenAI model; unset uses
OpenAI's default tier. Fast-mode usage costs are tier-adjusted. reasoning_effort accepts
Pi's thinking levels (off, minimal, low, medium, high, xhigh, max) and is
applied when the session starts; unset preserves Pi's configured default. After changing
either field, apply <name> renders the per-agent environment and restarts that agent.
For the one-table summary of each mode, see Custom model endpoints.
Two endpoint shapes exist and they are mutually exclusive — setting one requires clearing the other:
| claude | pi | |
|---|---|---|
endpoint_base_url |
rendered as ANTHROPIC_BASE_URL |
registered as a base-URL override on a named provider |
foundry_resource / foundry_base_url |
Azure AI Foundry | ignored — pi has no foundry provider |
Two refusals, both silent misconfigurations otherwise:
- A pi agent with
endpoint.foundry_*set is warned that the field is ignored and the agent stays on its provider's own endpoint. Useendpoint.base_urlfor a pi gateway. - A pi agent with
endpoint.base_urlbut nomodel.provideris warned that the URL is ignored. A pi base URL overrides a named provider; with none pinned there is nothing to override. Fix withagentctl agents set <name> provider <name>.
For claude, --provider selects anthropic (default) or foundry; choosing foundry
means declaring the endpoint with agents set <name> foundry_resource <resource> or
foundry_base_url <url>.
Where the credential lives¶
The endpoint: block is non-secret by construction — the ledger is a plaintext git
repo, so no key field exists there and none ever will. The matching credential is a
per-agent secret in the sops store, and which var it goes under follows the mode:
| mode | store the key as (either one) |
|---|---|
claude, endpoint_base_url |
ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN |
claude, foundry_resource / foundry_base_url |
ANTHROPIC_FOUNDRY_API_KEY or ANTHROPIC_FOUNDRY_AUTH_TOKEN (an Entra bearer token) |
pi, endpoint_base_url |
the pinned provider's own var — see below |
A pi base URL is an override of a provider. The credential is therefore unchanged from a pi agent with no custom endpoint:
| provider | var |
|---|---|
anthropic |
ANTHROPIC_API_KEY |
openai |
OPENAI_API_KEY |
google / gemini |
GEMINI_API_KEY |
groq |
GROQ_API_KEY |
cerebras |
CEREBRAS_API_KEY |
xai |
XAI_API_KEY |
openrouter |
OPENROUTER_API_KEY |
| anything else | <NAME>_API_KEY, uppercased |
sudo agentctl secret set ANTHROPIC_FOUNDRY_API_KEY --agent <name> --value "$KEY"
printf %s "$KEY" | sudo agentctl secret set ANTHROPIC_FOUNDRY_API_KEY --agent <name>
The second form keeps the key out of argv, which is world-readable on a multi-agent host.
apply renders both halves into separate files:
| file | holds | read by |
|---|---|---|
~/.config/agentctl/agent.env |
CLAUDE_MODEL, CLAUDE_SKILL_MODEL, ANTHROPIC_BASE_URL, ANTHROPIC_FOUNDRY_* |
start-agent.sh and cron-run.sh, both under set -a so the vars reach the claude child |
/etc/agentctl/env/agent-<name>.env |
the sops-rendered secrets | the unit's EnvironmentFile= |
~/.config/agentctl/pi-agent.env |
PI_PROVIDER, PI_MODEL, PI_BASE_URL, PI_SERVICE_TIER, PI_REASONING_EFFORT, PI_SKILL_MODEL |
the pi bridge |
Nothing in the endpoint block is written by hand. apply reconciles rather than
upserts. Clearing endpoint_base_url removes ANTHROPIC_BASE_URL on the next apply, so a
decommissioned gateway stops taking traffic.
Azure AI Foundry, end to end¶
Foundry is claude-only. Three commands on an existing agent:
sudo agentctl secret set ANTHROPIC_FOUNDRY_API_KEY --agent ada --value "$FOUNDRY_KEY"
sudo agentctl agents set ada foundry_resource my-foundry-resource
sudo agentctl apply ada
foundry_base_url "$URL" is the alternative spelling — the full endpoint URL Azure shows
for the resource, instead of the resource name. Set one, not both.
At add time the key can ride along, and only the key:
sudo agentctl agents add ada --runtime claude --user-email ada@example.com \
--api-key "$FOUNDRY_KEY" --provider foundry
--provider foundry chooses which var the key is stored under and nothing else. It
declares no endpoint, so agents add prints the agents set command still owed — a
Foundry key with no endpoint starts an agent that fails every turn. Any other
--provider value on a claude agent is refused with a warning and the key is stored as
ANTHROPIC_API_KEY.
Model ids pass through untranslated. agentctl hands the model scalar to
claude --model verbatim and performs no deployment-name mapping. Resolving that id to a
deployment is the Foundry client's job, not agentctl's. If a deployment is named
differently from the model id, the model scalar is where that name goes.
--provider the flag and provider the ledger scalar are different things. The flag
is consumed at add time to pick a key var. The scalar is a pi concept. On a claude agent
set to anything but anthropic, agents validate warns that it is ignored — claude
speaks only Anthropic.
A custom base URL¶
claude, against a self-hosted or corporate Anthropic-compatible gateway:
sudo agentctl secret set ANTHROPIC_API_KEY --agent ada --value "$GATEWAY_KEY"
sudo agentctl agents set ada endpoint_base_url https://gateway.corp.example/anthropic
sudo agentctl apply ada
pi, where the provider must be pinned first — the base URL overrides a named provider:
sudo agentctl agents set bo provider openai
sudo agentctl agents set bo model <model-id>
sudo agentctl secret set OPENAI_API_KEY --agent bo --value "$KEY"
sudo agentctl agents set bo endpoint_base_url https://gateway.corp.example/v1
sudo agentctl apply bo
PI_BASE_URL is narrower than the claude equivalent: it layers a base-URL override over
the built-in provider. The model catalog, the per-model costs, the api implementation and
the provider's own env-var auth all stay as pi ships them; only the URL moves. It declares
no new provider, which is why one has to be named.
Switching modes goes through an explicit clear. agents set refuses a second endpoint key
while another is declared:
sudo agentctl agents set ada endpoint_base_url ""
sudo agentctl agents set ada foundry_resource my-foundry-resource
agents set also refuses a base URL with no http:// or https:// scheme. A bare host
reaches the runtime as an unusable URL and fails at the first turn.
Which model runs where¶
The scalars do not override each other — they name different lanes, each resolved independently:
| lane | claude | pi |
|---|---|---|
| main session | model → CLAUDE_MODEL, default sonnet |
model → PI_MODEL, honoured only with provider also set; otherwise the bridge auto-discovers |
| turn-review skilling hook | skill_model → CLAUDE_SKILL_MODEL, default claude-haiku-4-5 |
skill_model → PI_SKILL_MODEL; empty falls back to PI_MODEL |
| subagents | each subagent's own model: frontmatter — coding and deep-work pin opus |
— |
| crons | by skill, CLAUDE_CRON_MODEL overriding per unit — see Crons |
the headless pi runner, on PI_PROVIDER + PI_MODEL |
The two claude defaults are back-filled into the ledger on apply. agents list
therefore shows what an agent actually runs on, and a changed default reaches agents that
already exist. pi gets neither default.
model and skill_model are ledger-only until an apply. agents set writes and commits
the scalar, then names the apply you owe it. apply re-renders the env file and bounces
the agent, because the ledger entry and the secret digest are both in the agent's
fingerprint.
What a wrong endpoint looks like¶
Neither runtime treats a bad endpoint as a startup failure. pi's provider registration never throws — an unknown provider id composes to an empty model list — and claude's launcher only exports a var. So the agent starts clean and every turn fails on auth or connection, which from the cockpit is indistinguishable from the model being down. Read the earlier gates rather than wait for a turn:
| gate | what it says |
|---|---|
agents set |
refuses the second endpoint key (the endpoint modes are alternatives; clear it first), and a base URL with no scheme |
agents validate <name> |
pi: provider-key when the store holds no key for the pinned provider, provider when a model is pinned without one. claude: provider when the scalar is anything but anthropic |
apply |
<name> declares a custom endpoint but the store holds neither ANTHROPIC_FOUNDRY_API_KEY nor ANTHROPIC_FOUNDRY_AUTH_TOKEN — every turn will fail auth, plus the two pi refusals above |
| runtime | agentctl agents logs <name> — the unit journal, via agentd |
The apply-time credential check reads the secret store only for an agent that declares an endpoint.
One failure mode raises no error at all: a fallback to the default Anthropic endpoint
bills the wrong account. agentctl agents logs shows a turn that failed; it does not show
a turn that succeeded against the wrong endpoint. Confirm the vars the child actually
received rather than the ones the ledger declares.