Skip to content

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. Use endpoint.base_url for a pi gateway.
  • A pi agent with endpoint.base_url but no model.provider is warned that the URL is ignored. A pi base URL overrides a named provider; with none pinned there is nothing to override. Fix with agentctl 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.