# Choose credentials and model routes Every codon in a `hank.json` names a model, and that single string decides more than it appears to: which provider serves the request, which agent harness runs it, and which credential has to exist before startup will proceed. When authentication fails, the error can come from several different places, and the fix depends on knowing which one. This page follows a model spelling from `hank.json` through resolution, harness dispatch, credential lookup, and the startup checks, so that each failure message points to a specific cause. Along the way we cover sentinel credentials, the ChatGPT subscription route, provider quota, and providers outside the registry's default routing. ## Which key does each provider need? Start with the route in your codon's `model` field; we'll match it to a credential, then check that the selected harness can serve it. A codon is one model-backed task in a `hank.json` configuration. A sentinel is a separate monitoring task that can make its own provider call and use a separate credential. For an enforced provider, set the standard environment variable listed below. | Provider | Standard variable | Sentinel override | Pi-route credential handling | | ------------------- | -------------------------- | --------------------------------------------- | -------------------------------------------- | | Anthropic | `ANTHROPIC_API_KEY` | `HANKWEAVE_SENTINEL_ANTHROPIC_API_KEY` | Environment key is enforced for the Pi route | | OpenAI | `OPENAI_API_KEY` | `HANKWEAVE_SENTINEL_OPENAI_API_KEY` | Environment key is enforced for the Pi route | | Google | `GEMINI_API_KEY` | `HANKWEAVE_SENTINEL_GEMINI_API_KEY` | Environment key is enforced for the Pi route | | Groq | `GROQ_API_KEY` | `HANKWEAVE_SENTINEL_GROQ_API_KEY` | Pi resolves the credential | | DeepSeek | `DEEPSEEK_API_KEY` | `HANKWEAVE_SENTINEL_DEEPSEEK_API_KEY` | Pi resolves the credential | | AWS Bedrock | `AWS_BEARER_TOKEN_BEDROCK` | `HANKWEAVE_SENTINEL_AWS_BEARER_TOKEN_BEDROCK` | Provider-specific handling | | Z.AI (0.7.4+) | `ZAI_API_KEY` | – | Environment key is enforced for the Pi route | | OpenRouter (0.7.5+) | `OPENROUTER_API_KEY` | – | Environment key is enforced for the Pi route | Read the last column as the Pi-route behavior for that provider. First-party Anthropic and Anthropic-family Bedrock models normally use the Claude Agent SDK harness; an explicit `pi//` spelling forces the Pi harness instead. The `openai-codex` subscription route is a separate Pi-provider exception and uses Pi's OAuth store rather than `OPENAI_API_KEY`; see [ChatGPT subscription](#how-do-you-use-a-chatgpt-subscription) below. For the full variable catalog, including `HANKWEAVE_` prefixes, trace and telemetry variables, and masking rules, see [Environment variables](/0.10.0/files/reference/environment-variables). One Google spelling caveat applies at 0.10.0. Since 0.8.0, `GEMINI_API_KEY` is the only Google key spelling read by the registry, Pi key injection, and wizard detection; the legacy `GOOGLE_API_KEY` alias was removed. Set `GEMINI_API_KEY` and unset the legacy name. A separate recorded run announced `Using GOOGLE_API_KEY` when both names were set, but that behavior is not defined by the 0.10.0 runtime, so treat `GEMINI_API_KEY` as the supported spelling. [Environment variables](/0.10.0/files/reference/environment-variables) also documents `--validate`'s environment echo and name-based masking of secret values. ## From a model string to a running codon Knowing which variable to set is only half of authentication. The other half is what the runtime does with your `model` string, because a credential can be present and correct while the route itself fails. When a codon does not authenticate as expected, inspect these decisions in order: authored spelling, retained identity, optional harness override, dispatch target, credential source, and startup self-test. Three terms carry the rest of this page. A **harness** is the agent runner selected for a given path. The **registry** is Hankweave's provider-and-model list. Pi is Hankweave's embedded agent harness for routes that do not use the Claude Agent SDK harness. Note that the key-handling column in the table above identifies which component supplies or enforces a key; it does not determine the dispatch harness for every model. | Decision point | What to inspect | Where to look next | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Authored spelling | The codon's `model`: shortcut, full ID, provider-qualified ID, or explicit `pi//` | [Model resolution](/0.10.0/files/reference/model-resolution) | | Retained identity | The real `(providerId, modelId)`; resolution does not rewrite it to a pseudo-provider | [Model resolution](/0.10.0/files/reference/model-resolution) | | `harnessOverride` | Set to `"pi"` only by an explicit `pi/…` spelling | [Model resolution](/0.10.0/files/reference/model-resolution) | | Dispatch target | First-party Anthropic and Anthropic-family Bedrock models use the Claude Agent SDK harness; other models use the embedded Pi harness | [Model resolution](/0.10.0/files/reference/model-resolution) | | Credential source | `anthropic`, `google`, `openai`, `openrouter`, and `zai` receive environment keys in memory; other Pi providers use the process environment or Pi's credential store | [Environment variables](/0.10.0/files/reference/environment-variables) | | Startup self-test | Each unique `harness:provider/model` gets a local self-test before any codon runs | [Errors and exit codes](/0.10.0/files/reference/errors-and-exit-codes) | The table gives the lookup view; here is the same path as the runtime walks it. Start with the `model` value in `hank.json`. For shortcut, phase, and fuzzy matching rules, see [Model resolution](/0.10.0/files/reference/model-resolution); here we follow that spelling into the runtime. Resolution retains the real provider and model pair, and harness selection is computed at dispatch time from that pair. An explicit `pi//` spelling sets `harnessOverride` to `"pi"`, even when the default route would use the Claude Agent SDK harness. The Claude Agent SDK harness handles first-party Anthropic and Anthropic-family Bedrock models. Other routes go to the embedded Pi harness through its target conversion. The conversion is alias-aware: `zhipuai` becomes `zai`, and `moonshotai` becomes `openrouter/moonshotai/…`. For the Pi harness, the five enforced providers receive their environment keys as in-memory runtime keys. Other Pi providers resolve credentials from the process environment or Pi's on-disk credential store. At startup, the self-test checks every unique codon model in a temporary directory. It keys models by `harness:provider/model`, so an explicit-Pi spelling and a default spelling of the same model are tested separately. A failure stops startup before a codon runs. The checks cover SDK imports, executable and credential presence, and Pi catalog lookup. They make no billable API call at 0.10.0; replay mode is the only mode that skips them. The welcome wizard's separate pre-demo credit check is the one-token call, and the provider health check described below is a different check that makes its own provider call. `--validate` resolves the configuration and performs harness setup, credential, and catalog checks; it does not execute codons or make a model-generation call. Runtime startup performs the provider health check separately. The welcome wizard is the first-run interactive setup flow, while `--init` provides templates. See [First run](/0.10.0/files/start/first-run) for the wizard and `--init` anatomy. One route-specific timeout is worth knowing before the first long run: a `pi/baseten/deepseek-ai/DeepSeek-V4-Pro` route can idle silently for more than 15 minutes while reasoning without streamed agent events. The `--shim-idle-timeout` flag sets a per-codon harness idle timeout (default: 120 seconds); the documented anchor run used `--shim-idle-timeout 1800` for that route. ### Check-it 1: remove the provider key To see the startup failure rather than just read about it, unset `ANTHROPIC_API_KEY`, then start the minimal single-provider hank. The failure occurs before a codon runs, which is the signature of the self-test plane: ```text … [] [ERROR] Self-test FAILED: Some checks failed [] [ERROR] - authentication: ✗ No authentication found (set ANTHROPIC_API_KEY) [ERROR] Server startup failed! Error message: Self-test failed for 1 model(s): … ``` The authentication check reports the missing variable by name and startup aborts. No codon executed; the self-test stopped the run first. ## Why can an auth error survive editing hank.json? An edit to `hank.json` cannot change the model chosen by the provider health check, because two model-selection planes are involved and only one of them reads the hank. Plane 1 is the codon self-test. It tests the resolved codon model, including the spelling you control in `hank.json`. Plane 2 is the provider health check. A registry provider is initialized only when a credential source exists and provider creation succeeds; an unkeyed provider is `not-configured`. At startup, the check probes each initialized registry provider with a pinned model from `PROVIDER_DEFINITIONS.healthCheckModels`. The health-check model is not configurable from `hank.json`. This startup probe is also separate from a sentinel trigger: the probe uses the pinned registry model, while a sentinel uses its own model spelling. The error text tells you which plane produced it. `Self-test failed` and `Unknown pi provider` identify the codon-model plane; provider-call errors identify the health-check plane. The pinned probe models are: | Provider | Pinned probe model | | --------- | ------------------------------------------------ | | Anthropic | `claude-haiku-4-5` | | OpenAI | `gpt-5.4-mini` | | Google | `gemini-flash-latest` | | DeepSeek | `deepseek-v4-flash` | | Groq | No pinned model; use the cheapest-model fallback | | Bedrock | Region-dependent function form | The health check makes a real `generateText` call with `"Hi"`, `maxOutputTokens: 16`, and a five-second abort timeout. Candidates are tried in order. Startup can wait for all checks, allow a grace period, or let them continue in the background. > **Pitfall:** If an auth or model error survives editing `hank.json`, inspect the other plane. The provider health-check model is pinned in the runtime and is not configurable from the hank. If the error still survives the hank edit, inspect the wizard, template, or health-check plane. This split has bitten in practice. In the March 2026 issue-#1 failure, the health-check fallback selected the dated preview `gemini-2.5-flash-lite-preview-09-2025`, and changing the hank did not change that spelling. The fix pinned health models, filtered dated previews, and added the 0.10.0 `model_catalog` preflight. The planes can also disagree in the other direction. A sentinel's model spelling can cause `No LLM provider available`; that is a sentinel-trigger error, not the startup provider-health probe. A short name such as `haiku` passes `--validate` with `GOOD TO RUN` but fails every trigger at run time, because `GOOD TO RUN` means validation accepted the configuration, not that a trigger can resolve its model. Use a full registry spelling such as `anthropic/claude-haiku-4-5` or `google/gemini-2.5-flash` so the sentinel can fire and write its log. This is a spelling and plane issue, not a credential-path divergence: codon credentials on the Pi path and sentinel/provider-registry credentials remain distinct resolution paths. The failure looks like this: ```text … [] [ERROR] [Sentinel:quality-observer] LLM call failed: Error: No LLM provider available [] [ERROR] [Sentinel:quality-observer] Error in trigger : Error: No LLM provider available … ``` The sentinel name in the log line and the trigger-level error are what distinguish this from the startup probe. ## When the registry knows a model Pi cannot serve A registry can recognize a model while Pi cannot serve the provider target. At startup, the `model_catalog` check applies the same lookup gate used for model spawning. A miss reports `Pi model not found: /`; an unknown provider reports `Unknown pi provider ''` and lists known providers. > **VersionNote:** Version 0.8.0 removed `GOOGLE_API_KEY`; set `GEMINI_API_KEY`. Version 0.10.0 added the `model_catalog` preflight, which fails an unservable Pi spelling at configuration load. Following `glm-5.3` through resolution shows why a bare spelling can produce an unfamiliar provider error: 1. Start with the authored spelling `glm-5.3`. For `glm-*`, the registry first prefers `zhipuai`, but that provider has no `glm-5.3`. Because no preferred provider owns the model, the bare short key uses the last-loaded provider carrying it, so the spelling resolves to `(zai-coding-plan, glm-5.3)`. The shipped registry carries `glm-5.3` under `opencode-go`, `zhipuai-coding-plan`, and `zai-coding-plan`. 2. Let the catalog check inspect `zai-coding-plan`. No Pi alias covers that provider, so Pi cannot serve `zai-coding-plan/glm-5.3`; the self-test fails with `Unknown pi provider 'zai-coding-plan'` before any codon runs. 3. Change the spelling to `pi/zai/glm-5.3`. The explicit spelling sets `harnessOverride`, and the catalog check uses Pi's `zai` provider. Use this explicit form rather than relying on bare-key provider order. Pi's catalog combines a static vendored snapshot with `~/.pi/agent/models.json`. A miss at self-test time is therefore a miss at launch for that catalog state. ### Check-it 2: verify the explicit spelling Use a hank with the explicit `pi/zai/glm-5.3` spelling and set `ZAI_API_KEY`. The capture below is real validation output: the model resolves to its registry identity and the run reports `GOOD TO RUN`. ```json { "$schema": "https://unpkg.com/hankweave@0.10.0/schemas/hank.schema.json", "meta": { "name": "zai preflight probe", "version": "1.0.0" }, "hank": [ { "id": "m", "name": "probe glm", "model": "pi/zai/glm-5.3", "continuationMode": "fresh", "promptText": "Say ok." } ] } ``` ```text … model: glm-5.3 (GLM 5.3 Preview) │ mode: fresh … ╭─ GOOD TO RUN! ─────────────────────────────────────────────────────╮ │ 1 codons • 0 prompts • 0 system prompts • 0 rigs • 0 checkpoints │ ╰────────────────────────────────────────────────────────────────────╯ … ``` ## How do you give sentinels their own keys? Set `HANKWEAVE_SENTINEL_` when a sentinel must use a different credential from the codons it watches. Sentinel lookup checks that variable first, then the standard provider variable, and the initialization log line records which source it used. The six supported sentinel spellings are: * `HANKWEAVE_SENTINEL_ANTHROPIC_API_KEY` * `HANKWEAVE_SENTINEL_OPENAI_API_KEY` * `HANKWEAVE_SENTINEL_GROQ_API_KEY` * `HANKWEAVE_SENTINEL_GEMINI_API_KEY` * `HANKWEAVE_SENTINEL_DEEPSEEK_API_KEY` * `HANKWEAVE_SENTINEL_AWS_BEARER_TOKEN_BEDROCK` Sentinel variables never reach codon agents. This lets the main agent use one provider while cost-tracking sentinels use a cheaper model on another provider. Bedrock sentinels accept a narrower set of credentials than Bedrock codons. Sentinel authentication accepts the bearer token or an explicit `AWS_ACCESS_KEY_ID` plus `AWS_SECRET_ACCESS_KEY` pair. Profile, SSO, container, and IMDS credentials can work for codons but not for sentinels. The not-configured error names the sources a sentinel accepts. ## How do you use a ChatGPT subscription? OpenAI offers two credential doors, and the model spelling has to match the one you hold: a metered API key or a ChatGPT subscription. | Door | Model spelling | Credential | | ----------------------------- | -------------------------------------------------- | --------------------------------------------------------- | | Metered API | A normal OpenAI spelling | `OPENAI_API_KEY` | | ChatGPT Plus/Pro subscription | The verbatim Pi spelling `pi/openai-codex/` | OAuth credential in Pi's store at `~/.pi/agent/auth.json` | The shipped `--init` scaffold uses the subscription door for its fourth codon, `analyze-gpt`. Put the OAuth credential in Pi's store with `pi login`, or delete that codon or change it to a keyed spelling. The keyed fallback named by the scaffold is `gpt-5.5` with `OPENAI_API_KEY`. The scaffold's Auth bullet and the codon object show both halves of that contract: ```md - The `pi/openai-codex/gpt-5.6-terra` codon runs on a **ChatGPT subscription** (no `OPENAI_API_KEY`): the OAuth credential is read from Pi's store at `~/.pi/agent/auth.json`. Put it there with `pi login` (choose "OpenAI (ChatGPT Plus/Pro)"). No subscription? Delete that codon, or switch its model to a keyed spelling like `gpt-5.5` with `OPENAI_API_KEY` set. ``` ```json { "id": "analyze-gpt", "name": "Analyze Project (GPT)", "model": "pi/openai-codex/gpt-5.6-terra", "continuationMode": "fresh", "promptFile": "./prompts/analyze-gpt.md", "checkpointedFiles": ["analysis-gpt.md"], "outputFiles": [ { "copy": ["analysis-gpt.md"] } ] } ``` The `gpt-5.6` family has `-sol`, `-luna`, and `-terra` variants. The init artifact selects the `-terra` subscription spelling; its keyed fallback is the older `gpt-5.5` spelling named above. `openai-codex` is not a provider for which Hankweave enforces `OPENAI_API_KEY`. Its self-test reports that Pi resolves credentials itself. Its catalog contains subscription-eligible models only. If a model is absent from that catalog, the `model_catalog` preflight fails at startup rather than when a codon starts. ### Check-it 3: inspect the shipped subscription codon Create the shipped scaffold, then inspect its Auth section and the `analyze-gpt` object: ```sh bunx hankweave@0.10.0 --init ``` Confirm that the artifact names `pi login`, `~/.pi/agent/auth.json`, and the option to delete or re-model the codon. ## When does provider quota end a run first? Two limits can end a run independently. Provider quota is a budget axis that Hankweave does not model, so it can terminate a run before Hankweave's budget sees provider spend. An observed run using `pi/zai/*` failed mid-chain when Z.AI returned a 429 with code 1308 for its five-hour usage window. The failure was classified as billing/usage, marked non-retriable, and ended with exit 1. The cost-reporting gap makes this sharper at 0.10.0: `pi/zai/*` codons report cost `0` on `codon.completed`. Consequently, `budget.maxDollars` does not bind for those codons, and `maxTimeSeconds` is the effective cap on that route. The provider can still enforce its own quota even though the reported cost is zero. This evidence is specific to `pi/zai/*`; it does not establish the same cost-reporting behavior for other Pi providers. To reduce quota failures: 1. Stagger codons that use the same provider. 2. Split a chain across providers. 3. Keep a time cap on every codon routed to a provider whose usage is not metered in the reported cost. For the meanings of `maxDollars`, `maxTimeSeconds`, `onExceeded`, and the two-party budget contract, use [Budgets](/0.10.0/files/concepts/budgets). ## How do you reach providers outside the registry route? Write an explicit `pi//` spelling when the registry does not route to that provider on its own. Validated Baseten spellings are: * `pi/baseten/deepseek-ai/DeepSeek-V4-Pro` * `pi/baseten/moonshotai/Kimi-K3` Set `BASETEN_API_KEY`. Baseten is not key-enforced by Hankweave; the self-test reports that Pi resolves credentials itself, and Pi reads `BASETEN_API_KEY` from the process environment. A passing preflight checks setup and catalog availability; it does not prove that the Baseten key is valid for a live request. The registry and Pi's shipped catalog can skew. The registry's Baseten block lists 19 models, including five `zai-org` GLM entries, while Pi's shipped `baseten.json` catalog serves 17 models. The registry-only `MiniMaxAI/MiniMax-M2.5` and `deepseek-ai/DeepSeek-V3.1` are absent from Pi's catalog; the `model_catalog` preflight catches this mismatch before a codon runs. For Bedrock credential scoping, IAM, and limitations, use [AWS Bedrock](/0.10.0/files/reference/aws-bedrock). Its Anthropic-versus-non-Anthropic routing split belongs to [Model resolution](/0.10.0/files/reference/model-resolution).