ax-check.com
by Gauge
O
OpenRouter
openrouter.com
ax-check.com
Checked 7 October 2026
B
83/100
Agents need your login
Agents can install, run, and price OpenRouter with no friction.
19 of 23 checks passed: quickstart, SDKs, API/MCP docs, and pricing all load without login. Only the root llms.txt index is missing.
2 fixes to start with
Concrete changes, backed by what we found.
https://openrouter.com/llms.txt returned 404; no root index, though docs/llms.txt exists.
Recorded coding sessions
Same task, 3 models, no openrouter.com account. Recorded 7 October 2026 on Pi.
3 recorded · 1m 47s
1m 47s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
Let me check the repository and pi's docs to look into the custom provider / model — the user is probably asking for an OpenRouter example using pi (custom provider?). Let me look at the environment.
ls -la /sandbox/repo && env | grep -i -E "PI_|OPENROUTER" | sed 's/=.*KEY.*/=***/'
total 16
total 16 drwxr-xr-x 3 root root 4096 Sep 18 08:00 . drwxr-xr-x 7 root root 4096 Oct 7 17:05 .. drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git -rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore PI_CODING_AGENT=true PI_REASONING_LEVEL=medium PI_SESSION_FILE=/sandbox/.gauge/pi/sessions/2026-10-07T17-05-45-238Z_01a11754-0716-7501-9215-900efd54c660.jsonl PI_PROVIDER=gauge-fireworks PI_MODEL=accounts/fireworks/models/qwen3p8-max PI_SESSION_ID=01a11754-0716-7501-9215-900efd54c660 PI_GATEWAY_API_KEY=unused
Recorded tool call
{
"type": "tool_call",
"seq": 3,
"ts": "2026-10-07T17:05:50.686Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls -la /sandbox/repo && env | grep -i -E \"PI_|OPENROUTER\" | sed 's/=.*KEY.*/=***/'"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-aa88833302a3cc71",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 6,
"ts": "2026-10-07T17:05:50.686Z",
"turn": 1,
"output": {
"content": [
{
"text": "total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Oct 7 17:05 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\nPI_CODING_AGENT=true\nPI_REASONING_LEVEL=medium\nPI_SESSION_FILE=/sandbox/.gauge/pi/sessions/2026-10-07T17-05-45-238Z_01a11754-0716-7501-9215-900efd54c660.jsonl\nPI_PROVIDER=gauge-fireworks\nPI_MODEL=accounts/fireworks/models/qwen3p8-max\nPI_SESSION_ID=01a11754-0716-7501-9215-900efd54c660\nPI_GATEWAY_API_KEY=unused\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-aa88833302a3cc71",
"is_error": false,
"tool_kind": "shell"
}ls /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs
compaction.md
compaction.md containerization.md custom-provider.md development.md docs.json environment-variables.md extensions.md images index.md json.md keybindings.md llama-cpp.md models.md packages.md prompt-templates.md providers.md quickstart.md rpc.md sdk.md security.md session-format.md sessions.md settings.md shell-aliases.md skills.md terminal-setup.md termux.md themes.md tmux.md tui.md usage.md windows.md
Recorded tool call
{
"type": "tool_call",
"seq": 4,
"ts": "2026-10-07T17:05:50.686Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9243c13dd83e0606",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 5,
"ts": "2026-10-07T17:05:50.686Z",
"turn": 1,
"output": {
"content": [
{
"text": "compaction.md\ncontainerization.md\ncustom-provider.md\ndevelopment.md\ndocs.json\nenvironment-variables.md\nextensions.md\nimages\nindex.md\njson.md\nkeybindings.md\nllama-cpp.md\nmodels.md\npackages.md\nprompt-templates.md\nproviders.md\nquickstart.md\nrpc.md\nsdk.md\nsecurity.md\nsession-format.md\nsessions.md\nsettings.md\nshell-aliases.md\nskills.md\nterminal-setup.md\ntermux.md\nthemes.md\ntmux.md\ntui.md\nusage.md\nwindows.md\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9243c13dd83e0606",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user is asking for a simple example using OpenRouter — probably via pi's custom provider support (the OPENROUTER_API_KEY environment variable is not set). Let me read providers.md and custom-provider.md to see how OpenRouter is supported.
grep -i openrouter -l /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/*.md
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/custom-provider.md
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/custom-provider.md /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
Recorded tool call
{
"type": "tool_call",
"seq": 9,
"ts": "2026-10-07T17:05:52.351Z",
"name": "bash",
"turn": 1,
"input": {
"command": "grep -i openrouter -l /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/*.md"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9240713e98aba006",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 10,
"ts": "2026-10-07T17:05:52.351Z",
"turn": 1,
"output": {
"content": [
{
"text": "/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/custom-provider.md\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/extensions.md\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9240713e98aba006",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The user wants to build a simple example using OpenRouter — probably as a custom provider for pi. Let me read the relevant docs: custom-provider.md and providers.md (mentions of openrouter), models.md.
grep -i -n -B2 -A10 openrouter /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md | head -80
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-21-- GitHub Copilot
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-21-- GitHub Copilot
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-22-- xAI (Grok/X subscription)
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:23:- OpenRouter (OAuth-minted API key billed from OpenRouter credits)
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-24-- Radius
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-25-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:26:Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired. OpenRouter instead mints a user-controlled API key that does not expire automatically.
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-27-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-28-### OpenAI Codex
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-29-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-30-- Requires ChatGPT Plus or Pro subscription
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-31-- Officially endorsed by OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-32-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-33-### Claude Pro/Max
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-34-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-35-Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party harness usage draws from [extra usage](https://claude.ai/settings/usage) and is billed per token, not against Claude plan limits.
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-36-
--
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-45-- `XAI_API_KEY` remains available through **Use an API key**
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-46-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:47:### OpenRouter
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-48-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:49:- Run `/login openrouter`, then select **Sign in with OpenRouter** to open the OpenRouter PKCE authorization flow
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:50:- The authorization creates a user-controlled OpenRouter API key billed from your OpenRouter credits
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-51-- On remote/headless machines (e.g. over SSH) the browser cannot reach the loopback callback; paste the final redirect URL (or the authorization code) into the login prompt instead
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:52:- `OPENROUTER_API_KEY` remains available through **Use an API key**
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-53-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-54-### Radius
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-55-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-56-Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-57-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-58-## API Keys
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-59-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-60-### Environment Variables or Auth File
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-61-
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-62-Use `/login` in interactive mode and select a provider to store an API key in `auth.json`, or set credentials via environment variable:
--
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-83-| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-84-| xAI | `XAI_API_KEY` | `xai` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:85:| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-86-| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-87-| ZAI Coding Plan (Global) | `ZAI_API_KEY` | `zai` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-88-| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-89-| OpenCode Zen | `OPENCODE_API_KEY` | `opencode` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-90-| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-91-| Radius | `RADIUS_API_KEY` | `radius` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-92-| Hugging Face | `HF_TOKEN` | `huggingface` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-93-| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-94-| Together AI | `TOGETHER_API_KEY` | `together` |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-95-| Baseten | `BASETEN_API_KEY` | `baseten` |
--
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-342-{
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-343- "providers": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:344: "openrouter": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-345- "modelOverrides": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-346- "anthropic/claude-sonnet-4": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-347- "name": "Claude Sonnet 4 (Bedrock Route)",
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-348- "compat": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:349: "openRouterRouting": {
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-350- "only": ["amazon-bedrock"]
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-351- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-352- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-353- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-354- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-355- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-356- }
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-357-}
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-358-```
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-359-
--
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-467-| `requiresThinkingAsText` | Convert thinking blocks to plain text |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-468-| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:469:| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-470-| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-471-| `chatTemplateArgs` | `chat_template_args` values for `thinkingFormat: "baseten"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-472-| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user, assistant, or tool-result text content. Currently only `anthropic` is supported. |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-473-| `sendSessionAffinityHeaders` | For `openai-completions`, send session-affinity headers from the session id when caching is enabled. Default: `false`. |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:474:| `sessionAffinityFormat` | For `openai-completions` and `openai-responses`, the session-affinity header format: `openai` sends `session_id`/`x-client-request-id` (completions also `x-session-affinity`), `openai-nosession` omits the underscore-containing `session_id` header, `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param. Default: auto-detected. |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-475-| `supportsStrictMode` | Whether the provider accepts strict JSON-schema function tool definitions. Defaults depend on the API; built-in OpenAI models carry explicit capability metadata. |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-476-| `supportsOpenAIGrammarTools` | Whether OpenAI-compatible APIs emit custom Lark/regex grammar tools. When `false`, grammar-constrained tools fall back to normal function tools. Default: `false`; the built-in model catalog enables it for GPT-5+ models on OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, and Cloudflare AI Gateway. |
/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-477-| `deferredToolsMode` | Use provider-specific deferred tool serialization. Currently only `"kimi"` is supported for Kimi's OpenAI-compatible Chat Completions format. |
Recorded tool call
{
"type": "tool_call",
"seq": 13,
"ts": "2026-10-07T17:05:56.189Z",
"name": "bash",
"turn": 1,
"input": {
"command": "grep -i -n -B2 -A10 openrouter /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md | head -80"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9ee961d83b81ba58",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 16,
"ts": "2026-10-07T17:05:56.189Z",
"turn": 1,
"output": {
"content": [
{
"text": "/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-21-- GitHub Copilot\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-22-- xAI (Grok/X subscription)\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:23:- OpenRouter (OAuth-minted API key billed from OpenRouter credits)\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-24-- Radius\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-25-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:26:Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired. OpenRouter instead mints a user-controlled API key that does not expire automatically.\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-27-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-28-### OpenAI Codex\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-29-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-30-- Requires ChatGPT Plus or Pro subscription\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-31-- Officially endorsed by OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-32-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-33-### Claude Pro/Max\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-34-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-35-Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party harness usage draws from [extra usage](https://claude.ai/settings/usage) and is billed per token, not against Claude plan limits.\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-36-\n--\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-45-- `XAI_API_KEY` remains available through **Use an API key**\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-46-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:47:### OpenRouter\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-48-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:49:- Run `/login openrouter`, then select **Sign in with OpenRouter** to open the OpenRouter PKCE authorization flow\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:50:- The authorization creates a user-controlled OpenRouter API key billed from your OpenRouter credits\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-51-- On remote/headless machines (e.g. over SSH) the browser cannot reach the loopback callback; paste the final redirect URL (or the authorization code) into the login prompt instead\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:52:- `OPENROUTER_API_KEY` remains available through **Use an API key**\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-53-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-54-### Radius\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-55-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-56-Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `\"oauth\": \"radius\"` and a gateway `baseUrl`.\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-57-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-58-## API Keys\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-59-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-60-### Environment Variables or Auth File\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-61-\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-62-Use `/login` in interactive mode and select a provider to store an API key in `auth.json`, or set credentials via environment variable:\n--\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-83-| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-84-| xAI | `XAI_API_KEY` | `xai` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md:85:| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-86-| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-87-| ZAI Coding Plan (Global) | `ZAI_API_KEY` | `zai` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-88-| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-89-| OpenCode Zen | `OPENCODE_API_KEY` | `opencode` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-90-| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-91-| Radius | `RADIUS_API_KEY` | `radius` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-92-| Hugging Face | `HF_TOKEN` | `huggingface` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-93-| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-94-| Together AI | `TOGETHER_API_KEY` | `together` |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md-95-| Baseten | `BASETEN_API_KEY` | `baseten` |\n--\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-342-{\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-343- \"providers\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:344: \"openrouter\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-345- \"modelOverrides\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-346- \"anthropic/claude-sonnet-4\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-347- \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-348- \"compat\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:349: \"openRouterRouting\": {\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-350- \"only\": [\"amazon-bedrock\"]\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-351- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-352- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-353- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-354- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-355- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-356- }\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-357-}\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-358-```\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-359-\n--\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-467-| `requiresThinkingAsText` | Convert thinking blocks to plain text |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-468-| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:469:| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-470-| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: \"chat-template\"`; use `{ \"$var\": \"thinking.enabled\" }` or `{ \"$var\": \"thinking.effort\" }` for pi-controlled thinking values |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-471-| `chatTemplateArgs` | `chat_template_args` values for `thinkingFormat: \"baseten\"`; use `{ \"$var\": \"thinking.enabled\" }` or `{ \"$var\": \"thinking.effort\" }` for pi-controlled thinking values |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-472-| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user, assistant, or tool-result text content. Currently only `anthropic` is supported. |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-473-| `sendSessionAffinityHeaders` | For `openai-completions`, send session-affinity headers from the session id when caching is enabled. Default: `false`. |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md:474:| `sessionAffinityFormat` | For `openai-completions` and `openai-responses`, the session-affinity header format: `openai` sends `session_id`/`x-client-request-id` (completions also `x-session-affinity`), `openai-nosession` omits the underscore-containing `session_id` header, `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param. Default: auto-detected. |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-475-| `supportsStrictMode` | Whether the provider accepts strict JSON-schema function tool definitions. Defaults depend on the API; built-in OpenAI models carry explicit capability metadata. |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-476-| `supportsOpenAIGrammarTools` | Whether OpenAI-compatible APIs emit custom Lark/regex grammar tools. When `false`, grammar-constrained tools fall back to normal function tools. Default: `false`; the built-in model catalog enables it for GPT-5+ models on OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, and Cloudflare AI Gateway. |\n/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md-477-| `deferredToolsMode` | Use provider-specific deferred tool serialization. Currently only `\"kimi\"` is supported for Kimi's OpenAI-compatible Chat Completions format. |\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-9ee961d83b81ba58",
"is_error": false,
"tool_kind": "shell"
}✓read /usr/local/lib/node_modules/@earendil-works/pi-coding-agent/docs/custom-provider.md✓└{"content":[{"text":"# Custom Providers\n\nExtensions can register custom model providers via `pi.registerProvider()`. This enables:\n\n- **Proxies** - Route re…ctrl+o
{"content":[{"text":"# Custom Providers\n\nExtensions can register custom model providers via `pi.registerProvider()`. This enables:\n\n- **Proxies** - Route requests through corporate proxies or API gateways\n- **Custom endpoints** - Use self-hosted or private model deployments\n- **OAuth/SSO** - Add authentication flows for enterprise providers\n- **Custom APIs** - Implement streaming for non-standard LLM APIs\n\n## Example Extensions\n\nSee these complete provider examples:\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## Table of Contents\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## Quick Reference\n\nExtensions can register either a complete pi-ai `Provider` or use the legacy provider-config form. Prefer a complete provider when custom authentication, filtering, refresh, or streaming behavior is required. Pi composes `models.json` overrides above registered native providers.\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n pi.registerProvider(createProvider({\n id: \"native-local\",\n name: \"Native Local\",\n baseUrl: \"http://localhost:8080/v1\",\n auth: {\n apiKey: {\n name: \"Local server API key\",\n async login(interaction) {\n return {\n type: \"api_key\",\n key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n };\n },\n async resolve({ credential }) {\n return credential?.key\n ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n : undefined;\n }\n }\n },\n models: [],\n api: openAICompletionsApi()\n }));\n\n // Legacy provider-config form:\n // Override baseUrl for existing provider\n pi.registerProvider(\"anthropic\", {\n baseUrl: \"https://proxy.example.com\"\n });\n\n // Register new provider with models\n pi.registerProvider(\"my-provider\", {\n name: \"My Provider\",\n baseUrl: \"https://api.example.com\",\n apiKey: \"$MY_API_KEY\",\n api: \"openai-completions\",\n models: [\n {\n id: \"my-model\",\n name: \"My Model\",\n reasoning: false,\n input: [\"text\", \"image\"],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: 128000,\n maxTokens: 4096\n }\n ]\n });\n}\n```\n\nThe extension factory can also be `async`. For dynamic model discovery, fetch and register models in the factory instead of `session_start`. pi waits for the factory before startup continues, so the provider is available during interactive startup and to `pi --list-models`.\n\n## Override Existing Provider\n\nThe simplest use case: redirect an existing provider through a proxy.\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n headers: {\n \"X-Custom-Header\": \"value\"\n }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n baseUrl: \"https://ai-gateway.corp.com/google\",\n headers: {\n \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\" // env var or literal\n }\n});\n```\n\nWhen only `baseUrl` and/or `headers` are provided (no `models`), all existing models for that provider are preserved with the new endpoint.\n\n## Register New Provider\n\nTo add a completely new provider, specify `models` along with the required configuration.\n\nIf the model list comes from a remote endpoint, use an async extension factory:\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n const response = await fetch(\"http://localhost:1234/v1/models\");\n const payload = (await response.json()) as {\n data: Array<{\n id: string;\n name?: string;\n context_window?: number;\n max_tokens?: number;\n }>;\n };\n\n pi.registerProvider(\"local-openai\", {\n baseUrl: \"http://localhost:1234/v1\",\n apiKey: \"$LOCAL_OPENAI_API_KEY\",\n api: \"openai-completions\",\n models: payload.data.map((model) => ({\n id: model.id,\n name: model.name ?? model.id,\n reasoning: false,\n input: [\"text\"],\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n contextWindow: model.context_window ?? 128000,\n maxTokens: model.max_tokens ?? 4096,\n })),\n });\n}\n```\n\nThis registers the fetched models before startup finishes.\n\n```typescript\npi.registerProvider(\"my-llm\", {\n baseUrl: \"https://api.my-llm.com/v1\",\n apiKey: \"$MY_LLM_API_KEY\", // env var reference\n api: \"openai-completions\", // which streaming API to use\n models: [\n {\n id: \"my-llm-large\",\n name: \"My LLM Large\",\n reasoning: true, // supports extended thinking\n input: [\"text\", \"image\"],\n cost: {\n input: 3.0, // $/million tokens\n output: 15.0,\n cacheRead: 0.3,\n cacheWrite: 3.75\n },\n contextWindow: 200000,\n maxTokens: 16384\n }\n ]\n});\n```\n\nWhen `models` is provided, it **replaces** all existing models for that provider.\n\n`apiKey` and custom header values use the same config value syntax as `models.json`: `!command` at the start executes a command for the whole value, `$ENV_VAR` and `${ENV_VAR}` interpolate environment variables, `$$` emits a literal `$`, and `$!` emits a literal `!`.\n\n## Unregister Provider\n\nUse `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n baseUrl: \"https://api.my-llm.com/v1\",\n apiKey: \"$MY_LLM_API_KEY\",\n api: \"openai-completions\",\n models: [\n {\n id: \"my-llm-large\",\n name: \"My LLM Large\",\n reasoning: true,\n input: [\"text\", \"image\"],\n cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n contextWindow: 200000,\n maxTokens: 16384\n }\n ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\nUnregistering removes that provider's dynamic models, API key fallback, OAuth provider registration, and custom stream handler registrations. Any built-in models or provider behavior that were overridden are restored.\n\nCalls made after the initial extension load phase are applied immediately, so no `/reload` is required.\n\n### API Types\n\nThe `api` field determines which streaming implementation is used:\n\n| API | Use for |\n|-----|---------|\n| `anthropic-messages` | Anthropic Claude API and compatibles |\n| `openai-completions` | OpenAI Chat Completions API and compatibles |\n| `openai-responses` | OpenAI Responses API |\n| `azure-openai-responses` | Azure OpenAI Responses API |\n| `openai-codex-responses` | OpenAI Codex Responses API |\n| `mistral-conversations` | Native Mistral Chat Completions streaming |\n| `google-generative-ai` | Google Generative AI API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | Amazon Bedrock Converse API |\n\nMost OpenAI-compatible providers work with `openai-completions`. Use model-level `thinkingLevelMap` for model-specific thinking levels, and `compat` for provider quirks. The `xhigh` and `max` levels are opt-in, require non-null map entries, and may be separated by unsupported holes:\n\n```typescript\nmodels: [{\n id: \"custom-model\",\n // ...\n reasoning: true,\n thinkingLevelMap: { // map pi levels to provider values; null hides unsupported levels\n minimal: null,\n low: null,\n medium: null,\n high: \"default\",\n xhigh: null,\n max: \"max\"\n },\n compat: {\n supportsDeveloperRole: false, // use \"system\" instead of \"developer\"\n supportsReasoningEffort: true,\n maxTokensField: \"max_tokens\", // instead of \"max_completion_tokens\"\n requiresToolResultName: true, // tool results need name field\n thinkingFormat: \"qwen\", // top-level enable_thinking: true\n cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n }\n}]\n```\n\nUse `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`.\nUse `cacheControlFormat: \"anthropic\"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user, assistant, or tool-result text content.\n\nFor Anthropic-compatible providers using `api: \"anthropic-messages\"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: \"adaptive\"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: \"\"` on replay.\n\n> Migration note: Mistral moved from `openai-completions` to `mistral-conversations`.\n> Use `mistral-conversations` for native Mistral models.\n> If you intentionally route Mistral-compatible/custom endpoints through `openai-completions`, set `compat` flags explicitly as needed.\n\n### Auth Header\n\nIf your provider expects `Authorization: Bearer <key>` but doesn't use a standard API, set `authHeader: true`:\n\n```typescript\npi.registerProvider(\"custom-api\", {\n baseUrl: \"https://api.example.com\",\n apiKey: \"$MY_API_KEY\",\n authHeader: true, // adds Authorization: Bearer header\n api: \"openai-completions\",\n models: [...]\n});\n```\n\nThe key is resolved for each request. An explicit request `Authorization` header takes precedence over the generated value.\n\n## OAuth Support\n\nAdd OAuth/SSO authentication that integrates with `/login`:\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n baseUrl: \"https://ai.corp.com/v1\",\n api: \"openai-responses\",\n models: [...],\n oauth: {\n name: \"Corporate AI (SSO)\",\n\n async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n const method = await callbacks.onSelect({\n message: \"Select login method:\",\n options: [\n { id: \"browser\", label: \"Browser OAuth\" },\n { id: \"device\", label: \"Device code\" }\n ]\n });\n if (!method) throw new Error(\"Login cancelled\");\n\n let code: string;\n if (method === \"device\") {\n callbacks.onDeviceCode({\n userCode: \"ABCD-1234\",\n verificationUri: \"https://sso.corp.com/device\",\n intervalSeconds: 5,\n expiresInSeconds: 900\n });\n code = await pollDeviceCodeUntilComplete();\n } else {\n callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n }\n\n // Exchange for tokens (your implementation)\n const tokens = await exchangeCodeForTokens(code);\n\n return {\n refresh: tokens.refreshToken,\n access: tokens.accessToken,\n expires: Date.now() + tokens.expiresIn * 1000\n };\n },\n\n async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n const tokens = await refreshAccessToken(credentials.refresh, signal);\n return {\n refresh: tokens.refreshToken ?? credentials.refresh,\n access: tokens.accessToken,\n expires: Date.now() + tokens.expiresIn * 1000\n };\n },\n\n getApiKey(credentials: OAuthCredentials): string {\n return credentials.access;\n }\n }\n});\n```\n\nAfter registration, users can authenticate via `/login corporate-ai`.\n\n### OAuthLoginCallbacks\n\nThe `callbacks` object provides UI-neutral interactions for the provider-owned flow:\n\n```typescript\ninterface OAuthLoginCallbacks {\n // Open URL in browser (for OAuth redirects)\n onAuth(params: { url: string }): void;\n\n // Show device code (for device authorization flow)\n onDeviceCode(params: {\n userCode: string;\n verificationUri: string;\n intervalSeconds?: number;\n expiresInSeconds?: number;\n }): void;\n\n // Show transient progress\n onProgress?(message: string): void;\n\n // Prompt user for input (for manual token entry)\n onPrompt(params: { message: string }): Promise<string>;\n\n // Show an interactive selector, e.g. to choose browser OAuth vs device code\n onSelect(params: {\n message: string;\n options: { id: string; label: string }[];\n }): Promise<string | undefined>;\n}\n```\n\n### OAuthCredentials\n\nCredentials are persisted in `~/.pi/agent/auth.json`:\n\n```typescript\ninterface OAuthCredentials {\n refresh: string; // Refresh token (for refreshToken())\n access: string; // Access token (returned by getApiKey())\n expires: number; // Expiration timestamp in milliseconds\n}\n```\n\n## Custom Streaming API\n\nFor providers with non-standard APIs, implement `streamSimple`. Study the existing provider implementations before writing your own:\n\n**Reference implementations:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Anthropic Messages API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Mistral Conversations API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - OpenAI Chat Completions\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - OpenAI Responses API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - Google Generative AI\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - AWS Bedrock\n\n### Stream Pattern\n\nAll providers follow the same pattern:\n\n```typescript\nimport {\n type AssistantMessage,\n type AssistantMessageEventStream,\n type Context,\n type Model,\n type SimpleStreamOptions,\n calculateCost,\n createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n model: Model<any>,\n context: Context,\n options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n const stream = createAssistantMessageEventStream();\n\n (async () => {\n // Initialize output message\n const output: AssistantMessage = {\n role: \"assistant\",\n content: [],\n api: model.api,\n provider: model.provider,\n model: model.id,\n usage: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n },\n stopReason: \"pending\",\n timestamp: Date.now(),\n };\n\n try {\n // Push start event\n stream.push({ type: \"start\", partial: output });\n\n // Make API request and process response...\n // Push content events as they arrive and set stopReason from the terminal event.\n if (output.stopReason === \"pending\") {\n throw new Error(\"Provider stream ended without a stop reason\");\n }\n if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n throw new Error(output.errorMessage || \"An unknown error occurred\");\n }\n\n // Push done event\n stream.push({\n type: \"done\",\n reason: output.stopReason,\n message: output\n });\n stream.end();\n } catch (error) {\n output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n output.errorMessage = error instanceof Error ? error.message : String(error);\n stream.push({ type: \"error\", reason: output.stopReason, error: output });\n stream.end();\n }\n })();\n\n return stream;\n}\n```\n\n### Event Types\n\nPush events via `stream.push()` in this order:\n\n1. `{ type: \"start\", partial: output }` - Stream started\n\n2. Content events (repeatable, track `contentIndex` for each block):\n - `{ type: \"text_start\", contentIndex, partial }` - Text block started\n - `{ type: \"text_delta\", contentIndex, delta, partial }` - Text chunk\n - `{ type: \"text_end\", contentIndex, content, partial }` - Text block ended\n - `{ type: \"thinking_start\", contentIndex, partial }` - Thinking started\n - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - Thinking chunk\n - `{ type: \"thinking_end\", contentIndex, content, partial }` - Thinking ended\n - `{ type: \"toolcall_start\", contentIndex, partial }` - Tool call started\n - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - Tool call JSON chunk\n - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - Tool call ended\n\n3. `{ type: \"done\", reason, message }` or `{ type: \"error\", reason, error }` - Stream ended\n\nThe `partial` field in each event contains the current `AssistantMessage` state. Update `output.content` as you receive data, then include `output` as the `partial`.\n\n### Content Blocks\n\nAdd content blocks to `output.content` as they arrive:\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n block.text += delta;\n stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### Tool Calls\n\nTool calls require accumulating JSON and parsing:\n\n```typescript\n// Start tool call\noutput.content.push({\n type: \"toolCall\",\n id: toolCallId,\n name: toolName,\n arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n type: \"toolcall_end\",\n contentIndex,\n toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n partial: output\n});\n```\n\n### Usage and Cost\n\nUpdate usage from API response and calculate cost:\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens …›Thinking…
User's request: "Help me build a simple example using OpenRouter" — in other words, build a simple example using OpenRouter via the hosted API/SDK. This probably means using pi's SDK or the OpenRouter API. The environment variable OPENROUTER_API_KEY doesn't exist. Check whether the key is available. If it isn't, we can't actually invoke the API — we'd be blocked by the lack of credentials. I'll also answer about pricing and manageability.
Task: Build a simple example using OpenRouter (the hosted product via API/SDK). Steps: check for OPENROUTER_API_KEY, and if it's absent, report that we're blocked due to missing credentials? Or maybe pi supports openrouter as a built-in provider and we could use the pi SDK... still, the API key would be required.
First check the environment variable, then write a small example (a Node script using fetch against OpenRouter's OpenAI-compatible endpoint, or one using the pi-ai SDK). I could try invoking it to test, but without a key it'll fail — I should just be upfront that we're blocked due to missing credentials.
Also answer questions about pricing: OpenRouter charges per-token based on the underlying model's pricing, you purchase credits, and there's a fee for failed requests, etc. Manageability: yes, easy — simple API, no infrastructure needed.
Let me check whether the API key is present.
The product here is OpenRouter, which has an actual developer workflow (hosted OpenAI-compatible API). So we can work with this. First, let's check the credentials.
env | grep -i -E "OPENROUTER|ANTHROPIC|OPENAI" | sed 's/=.*/=<set>/'; ls ~/.pi/agent 2>/dev/null; node -e "try{require.resolve('@earendil-works/pi-ai');console.log('pi-ai available')}catch(e){console.log('pi-ai not resolvable here')}"; node -e "try{require.resolve('openai');console.log('openai sdk available')}catch(e){console.log('openai sdk not installed')}"auth.json
auth.json models-store.json models.json pi-ai not resolvable here openai sdk not installed
Recorded tool call
{
"type": "tool_call",
"seq": 19,
"ts": "2026-10-07T17:06:04.378Z",
"name": "bash",
"turn": 1,
"input": {
"command": "env | grep -i -E \"OPENROUTER|ANTHROPIC|OPENAI\" | sed 's/=.*/=<set>/'; ls ~/.pi/agent 2>/dev/null; node -e \"try{require.resolve('@earendil-works/pi-ai');console.log('pi-ai available')}catch(e){console.log('pi-ai not resolvable here')}\"; node -e \"try{require.resolve('openai');console.log('openai sdk available')}catch(e){console.log('openai sdk not installed')}\""
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-92c2658c017601c5",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 22,
"ts": "2026-10-07T17:06:04.378Z",
"turn": 1,
"output": {
"content": [
{
"text": "auth.json\nmodels-store.json\nmodels.json\npi-ai not resolvable here\nopenai sdk not installed\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-92c2658c017601c5",
"is_error": false,
"tool_kind": "shell"
}curl -s -m 10 https://openrouter.ai/api/v1/models -o /dev/null -w "%{http_code}\n" 2>&1 || echo "network blocked"200
200
Recorded tool call
{
"type": "tool_call",
"seq": 20,
"ts": "2026-10-07T17:06:04.378Z",
"name": "bash",
"turn": 1,
"input": {
"command": "curl -s -m 10 https://openrouter.ai/api/v1/models -o /dev/null -w \"%{http_code}\\n\" 2>&1 || echo \"network blocked\""
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-a1d2b2bd990076e0",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 23,
"ts": "2026-10-07T17:06:05.367Z",
"turn": 1,
"output": {
"content": [
{
"text": "200\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-a1d2b2bd990076e0",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The network is functioning. Check whether there's an openrouter key in auth.json.
python3 -c "import json;d=json.load(open('$HOME/.pi/agent/auth.json'));print({k:('***' if v else v) for k,v in d.items()})" 2>/dev/null; cat ~/.pi/agent/models.json 2>/dev/null | head -30{}
{}
{
"providers": {
"gauge-fireworks": {
"baseUrl": "https://api.fireworks.ai/inference/v1",
"api": "openai-completions",
"apiKey": "$PI_GATEWAY_API_KEY",
"authHeader": true,
"models": [
{
"id": "accounts/fireworks/models/qwen3p8-max",
"name": "accounts/fireworks/models/qwen3p8-max",
"reasoning": true,
"input": [
"text"
],
"contextWindow": 262144,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": true
}
}
]
}
}
Recorded tool call
{
"type": "tool_call",
"seq": 26,
"ts": "2026-10-07T17:06:09.268Z",
"name": "bash",
"turn": 1,
"input": {
"command": "python3 -c \"import json;d=json.load(open('$HOME/.pi/agent/auth.json'));print({k:('***' if v else v) for k,v in d.items()})\" 2>/dev/null; cat ~/.pi/agent/models.json 2>/dev/null | head -30"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-915cf325d1071305",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 27,
"ts": "2026-10-07T17:06:09.268Z",
"turn": 1,
"output": {
"content": [
{
"text": "{}\n{\n \"providers\": {\n \"gauge-fireworks\": {\n \"baseUrl\": \"https://api.fireworks.ai/inference/v1\",\n \"api\": \"openai-completions\",\n \"apiKey\": \"$PI_GATEWAY_API_KEY\",\n \"authHeader\": true,\n \"models\": [\n {\n \"id\": \"accounts/fireworks/models/qwen3p8-max\",\n \"name\": \"accounts/fireworks/models/qwen3p8-max\",\n \"reasoning\": true,\n \"input\": [\n \"text\"\n ],\n \"contextWindow\": 262144,\n \"cost\": {\n \"input\": 0,\n \"output\": 0,\n \"cacheRead\": 0,\n \"cacheWrite\": 0\n },\n \"compat\": {\n \"supportsDeveloperRole\": false,\n \"supportsReasoningEffort\": true\n }\n }\n ]\n }\n }\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-915cf325d1071305",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
OPENROUTER_API_KEY doesn't exist. Network access to openrouter.ai is working fine. Can't make live model calls without credentials — this is a blocker for running examples, but I can still build the examples themselves. Check if there are free models that don't require a key? OpenRouter requires an API key for all calls. In other words, live execution is blocked, so I should communicate that clearly. Still, I'll build the sample files (SDK-based examples: raw fetch / OpenAI SDK style / pi provider config).
Keep it lightweight: create sample files in the repo. What I'll create:
openrouter-example.mjs— a minimal example using fetch against OpenRouter's chat completions endpoint.- If needed, register openrouter as a pi provider in
models.jsonusing $OPENROUTER_API_KEY.
After that, answer the pricing and manageability questions, and note that execution is blocked due to lack of an API key.
Network reachability to OpenRouter is fine, but there's no OPENROUTER_API_KEY in the environment, and ~/.pi/agent/auth.json is also empty. So live calls can't be executed — I'll build a sample right now and clearly flag the blocking factor.
File edits
1 file
Writtenopenrouter-example.mjsView changes
/sandbox/repo/openrouter-example.mjs
Written content · previous contents were not recorded
// Minimal OpenRouter example — hosted API, no local services.// OpenRouter exposes an OpenAI-compatible Chat Completions endpoint.//// Run:// export OPENROUTER_API_KEY=sk-or-...// node openrouter-example.mjsconst apiKey = process.env.OPENROUTER_API_KEY;if (!apiKey) {console.error("Set OPENROUTER_API_KEY first (https://openrouter.ai/keys).");process.exit(1);}const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {method: "POST",headers: {Authorization: `Bearer ${apiKey}`,"Content-Type": "application/json",// Optional attribution headers (shown on openrouter.ai rankings):"HTTP-Referer": "https://example.com","X-Title": "pi openrouter example",},body: JSON.stringify({model: "openai/gpt-4o-mini", // any model slug from https://openrouter.ai/modelsmessages: [{ role: "user", content: "Say hello in one short sentence." }],max_tokens: 64,}),});if (!res.ok) {console.error(`OpenRouter error ${res.status}:`, await res.text());process.exit(1);}const data = await res.json();console.log("Reply:", data.choices[0].message.content);console.log("Usage:", data.usage); // token counts; cost = tokens x per-model ratesconsole.log("Cost (USD):",data.usage && data.usage.cost !== undefined? data.usage.cost // OpenRouter reports actual cost when available: "not reported (compute from usage + model pricing)");
Source: write · chatcmpl-tool-83403c438d7c049a
Recorded tool call
{
"type": "tool_call",
"seq": 30,
"ts": "2026-10-07T17:06:21.403Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/openrouter-example.mjs",
"content": "// Minimal OpenRouter example — hosted API, no local services.\n// OpenRouter exposes an OpenAI-compatible Chat Completions endpoint.\n//\n// Run:\n// export OPENROUTER_API_KEY=sk-or-...\n// node openrouter-example.mjs\n\nconst apiKey = process.env.OPENROUTER_API_KEY;\nif (!apiKey) {\n console.error(\"Set OPENROUTER_API_KEY first (https://openrouter.ai/keys).\");\n process.exit(1);\n}\n\nconst res = await fetch(\"https://openrouter.ai/api/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n // Optional attribution headers (shown on openrouter.ai rankings):\n \"HTTP-Referer\": \"https://example.com\",\n \"X-Title\": \"pi openrouter example\",\n },\n body: JSON.stringify({\n model: \"openai/gpt-4o-mini\", // any model slug from https://openrouter.ai/models\n messages: [{ role: \"user\", content: \"Say hello in one short sentence.\" }],\n max_tokens: 64,\n }),\n});\n\nif (!res.ok) {\n console.error(`OpenRouter error ${res.status}:`, await res.text());\n process.exit(1);\n}\n\nconst data = await res.json();\nconsole.log(\"Reply:\", data.choices[0].message.content);\nconsole.log(\"Usage:\", data.usage); // token counts; cost = tokens x per-model rates\nconsole.log(\n \"Cost (USD):\",\n data.usage && data.usage.cost !== undefined\n ? data.usage.cost // OpenRouter reports actual cost when available\n : \"not reported (compute from usage + model pricing)\"\n);\n"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-83403c438d7c049a",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 32,
"ts": "2026-10-07T17:06:21.403Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1432 bytes to /sandbox/repo/openrouter-example.mjs",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-83403c438d7c049a",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
WrittenREADME-openrouter.mdView changes
/sandbox/repo/README-openrouter.md
Written content · previous contents were not recorded
# OpenRouter example`openrouter-example.mjs` calls OpenRouter's hosted, OpenAI-compatible API(`https://openrouter.ai/api/v1/chat/completions`). No local stacks, no SDKinstall needed — it uses plain `fetch`.## Setup1. Create an account at https://openrouter.ai and buy credits.2. Create an API key at https://openrouter.ai/keys.3. `export OPENROUTER_API_KEY=sk-or-...`4. `node openrouter-example.mjs`Swap `model` for any slug from `GET https://openrouter.ai/api/v1/models`(public, no key required) or https://openrouter.ai/models.## Using OpenRouter from pipi has built-in OpenRouter support:- `export OPENROUTER_API_KEY=...` then pick an `openrouter` model, or- `/login openrouter` for the OAuth PKCE flow (mints a user-controlled APIkey billed from your OpenRouter credits).## Pricing (how OpenRouter bills)- **Prepaid credits**: you top up a credit balance; usage is deducted per request.- **Per-token, per-model**: each model slug has its own $/million input andoutput token rates (listed on the model page / `/api/v1/models` `pricing`field). OpenRouter passes through the underlying provider's token price.- **Failed requests are refunded** (not charged) — you only pay for successfulcompletions.- Some models have free variants (slug suffix `:free`) with rate limits.- Cached-input tokens may be cheaper where the underlying provider supports it.
Source: write · chatcmpl-tool-b18921c2625a2b87
Recorded tool call
{
"type": "tool_call",
"seq": 31,
"ts": "2026-10-07T17:06:21.403Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/README-openrouter.md",
"content": "# OpenRouter example\n\n`openrouter-example.mjs` calls OpenRouter's hosted, OpenAI-compatible API\n(`https://openrouter.ai/api/v1/chat/completions`). No local stacks, no SDK\ninstall needed — it uses plain `fetch`.\n\n## Setup\n\n1. Create an account at https://openrouter.ai and buy credits.\n2. Create an API key at https://openrouter.ai/keys.\n3. `export OPENROUTER_API_KEY=sk-or-...`\n4. `node openrouter-example.mjs`\n\nSwap `model` for any slug from `GET https://openrouter.ai/api/v1/models`\n(public, no key required) or https://openrouter.ai/models.\n\n## Using OpenRouter from pi\n\npi has built-in OpenRouter support:\n\n- `export OPENROUTER_API_KEY=...` then pick an `openrouter` model, or\n- `/login openrouter` for the OAuth PKCE flow (mints a user-controlled API\n key billed from your OpenRouter credits).\n\n## Pricing (how OpenRouter bills)\n\n- **Prepaid credits**: you top up a credit balance; usage is deducted per request.\n- **Per-token, per-model**: each model slug has its own $/million input and\n output token rates (listed on the model page / `/api/v1/models` `pricing`\n field). OpenRouter passes through the underlying provider's token price.\n- **Failed requests are refunded** (not charged) — you only pay for successful\n completions.\n- Some models have free variants (slug suffix `:free`) with rate limits.\n- Cached-input tokens may be cheaper where the underlying provider supports it.\n"
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-b18921c2625a2b87",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 33,
"ts": "2026-10-07T17:06:21.403Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1393 bytes to /sandbox/repo/README-openrouter.md",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-b18921c2625a2b87",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Verify that the script's syntax can be executed (it'll probably fail due to the missing key — expected). Also verify that the public models endpoint works without a key, and demonstrate that pricing information is obtainable. Keep it lightweight.
node --check openrouter-example.mjs && echo "syntax OK"; node openrouter-example.mjs; echo "exit=$?"
syntax OK
syntax OK Set OPENROUTER_API_KEY first (https://openrouter.ai/keys). exit=1
Recorded tool call
{
"type": "tool_call",
"seq": 36,
"ts": "2026-10-07T17:07:11.702Z",
"name": "bash",
"turn": 1,
"input": {
"command": "node --check openrouter-example.mjs && echo \"syntax OK\"; node openrouter-example.mjs; echo \"exit=$?\""
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-a2eb26c60950b4e8",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 39,
"ts": "2026-10-07T17:07:12.120Z",
"turn": 1,
"output": {
"content": [
{
"text": "syntax OK\nSet OPENROUTER_API_KEY first (https://openrouter.ai/keys).\nexit=1\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-a2eb26c60950b4e8",
"is_error": false,
"tool_kind": "shell"
}curl -s -m 15 https://openrouter.ai/api/v1/models | python3 -c "
import json,sys
d=json.load(sys.stdin)
m=[x for x in d['data'] if x['id']=='openai/gpt-4o-mini']
if m:
p=m[0]['pricing']
print('openai/gpt-4o-mini pricing per token:', {k:p[k] for k in ('prompt','completion') if k in p})
print('=> $%.2f/M input, $%.2f/M output' % (float(p['prompt'])*1e6, float(p['completion'])*1e6))
print('total models listed:', len(d['data']))"openai/gpt-4o-mini pricing per token: {'prompt': '0.00000015', 'completion': '0.0000006'}
openai/gpt-4o-mini pricing per token: {'prompt': '0.00000015', 'completion': '0.0000006'}
=> $0.15/M input, $0.60/M output
total models listed: 465
Recorded tool call
{
"type": "tool_call",
"seq": 37,
"ts": "2026-10-07T17:07:11.702Z",
"name": "bash",
"turn": 1,
"input": {
"command": "curl -s -m 15 https://openrouter.ai/api/v1/models | python3 -c \"\nimport json,sys\nd=json.load(sys.stdin)\nm=[x for x in d['data'] if x['id']=='openai/gpt-4o-mini']\nif m:\n p=m[0]['pricing']\n print('openai/gpt-4o-mini pricing per token:', {k:p[k] for k in ('prompt','completion') if k in p})\n print('=> $%.2f/M input, $%.2f/M output' % (float(p['prompt'])*1e6, float(p['completion'])*1e6))\nprint('total models listed:', len(d['data']))\""
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-898fed1e1f58227b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 40,
"ts": "2026-10-07T17:07:13.034Z",
"turn": 1,
"output": {
"content": [
{
"text": "openai/gpt-4o-mini pricing per token: {'prompt': '0.00000015', 'completion': '0.0000006'}\n=> $0.15/M input, $0.60/M output\ntotal models listed: 465\n",
"type": "text"
}
]
},
"run_id": "cmuyczhf3002i0iszr08rv9y5",
"tool_id": "chatcmpl-tool-898fed1e1f58227b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Done. To summarize: what I built, pricing, ease of management, and the fact that it was blocked (can't do live chat completion calls because there's no API key).
Done. OpenRouter has a real developer workflow (hosted, OpenAI-compatible API), so I acted on it — lightly, no local stacks, no long-running commands.
What I built (in /sandbox/repo):
openrouter-example.mjs— minimal example callinghttps://openrouter.ai/api/v1/chat/completionswith plainfetch(no SDK install needed; any OpenAI SDK works too by pointingbaseURLat OpenRouter). Syntax-checked; it correctly refuses to run without a key.README-openrouter.md— setup, how to swap models, and how to use OpenRouter from pi (OPENROUTER_API_KEYenv var or/login openrouter).
How pricing works:
- Prepaid credits — you top up a balance and usage is deducted per request.
- Per-token, per-model: each model slug has its own rates, passed through from the underlying provider. I verified live from the public
/api/v1/modelsendpoint: e.g.openai/gpt-4o-miniis $0.15/M input, $0.60/M output (465 models listed). - Failed requests are refunded; some
:freemodel variants exist with rate limits; cached-input tokens can be cheaper where supported.
Blocked? Partially: the network works and the public models endpoint responds, but there is no OPENROUTER_API_KEY in this environment and auth.json is empty — so I could not execute an actual chat completion. Get a key from https://openrouter.ai/keys, export it, and node openrouter-example.mjs will run as-is.
Will this be easy for me to manage? Yes. It's a single stateless REST endpoint with a standard OpenAI-compatible schema, a public no-auth model/pricing catalog I can query anytime, and pi has built-in OpenRouter provider support. The only friction is credential provisioning, which is on your side.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents