pi-sub2api
Pi extension for a sub2api gateway: dynamic model registration and a /usage command for balance and cost.
Package details
Install pi-sub2api from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-sub2api- Package
pi-sub2api- Version
0.2.4- Published
- Jul 26, 2026
- Downloads
- 1,018/mo · 403/wk
- Author
- pacoyang
- License
- MIT
- Types
- extension
- Size
- 25 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-sub2api
A pi extension for a sub2api gateway.
- Fetches the model list from the gateway at startup and registers it as the
sub2apiprovider. - Adds a
/usagecommand showing balance and today/total cost.
Install
pi install npm:pi-sub2api
Or try it for a single run without installing:
pi -e npm:pi-sub2api
Configure
All configuration lives in ~/.pi/agent/auth.json under the provider name sub2api. No shell
environment variables are needed.
{
"sub2api": {
"type": "api_key",
"key": "sk-...",
"env": { "SUB2API_BASE_URL": "https://your-sub2api-host" }
}
}
chmod 600 ~/.pi/agent/auth.json
key— an API key generated in the sub2api dashboard.env.SUB2API_BASE_URL— the gateway base URL, without a trailing/v1. Defaults tohttp://localhost:8080.env.SUB2API_PROTOCOL— optional, and normally left out. Pins every model to one wire protocol instead of letting the extension choose. See Protocol.
pi resolves the key from auth.json["sub2api"] for inference; the extension reads the same entry
for its own /v1/models and /v1/usage calls. SUB2API_KEY / SUB2API_BASE_URL /
SUB2API_PROTOCOL environment variables are used as a fallback if the auth.json entry is
absent.
Protocol
sub2api dispatches on the platform of the API key's group, not on the request path. An
Anthropic-account key serves /v1/messages natively and translates /v1/chat/completions down to
it; an OpenAI-account key does the reverse. The non-native path costs a translation hop.
You do not have to configure this. Left alone, the extension reads each model id and registers it on the protocol native to it:
| Model id | pi api |
Endpoint |
|---|---|---|
claude-* |
anthropic-messages |
POST {BASE}/v1/messages |
| everything else | openai-completions |
POST {BASE}/v1/chat/completions |
A model list of only Claude ids therefore ends up entirely on the Anthropic protocol, a list of
only GPT ids entirely on the OpenAI one, and a mixed list split between them — one provider, two
protocols. This is the auto behaviour, and it is what you get when SUB2API_PROTOCOL is unset.
Set it only to overrule that guess. Values are pi's own api names, since that is what they become:
SUB2API_PROTOCOL |
Effect |
|---|---|
unset, or auto |
per model, per the table above |
anthropic-messages |
every model on POST {BASE}/v1/messages |
openai-completions |
every model on POST {BASE}/v1/chat/completions |
openai-responses |
every model on POST {BASE}/v1/responses |
Forcing changes only the endpoint and payload format — per-model context and output limits still follow the model.
openai-responses
On an OpenAI group the gateway's own upstream is the Responses API — DeriveUpstreamEndpoint
sends every OpenAI request to /v1/responses regardless of how it arrived. So the default
openai-completions is itself a conversion: the reply comes back with a resp_… id and
"object": "chat.completion.chunk", having been translated down from a Responses stream.
Going in as openai-responses skips that, and buys one thing the chat path cannot give:
openai-completions |
openai-responses |
|
|---|---|---|
| Thinking shown | yes, via reasoning_content |
yes, via reasoning items |
| Reasoning replayed next turn | no | yes — reasoning.encrypted_content |
pi stores each reasoning item and feeds it back on the following turn, so a GPT-5 model keeps its reasoning chain across a tool loop instead of restarting it every time.
It is opt-in rather than the default because an API-key account whose upstream lacks the Responses
API is served on the raw chat path instead (resolveOpenAIUpstreamEndpoint), and that setup has
not been tested here. To use it:
"env": {
"SUB2API_BASE_URL": "https://your-sub2api-host",
"SUB2API_PROTOCOL": "openai-responses"
}
When to force it
The default rule infers the backend platform from the model name, so forcing is for the cases where the name does not match the backend.
Force anthropic-messages when an Anthropic group publishes model ids without claude in
them — aliases like sonnet-latest or my-coding-model. Nothing fails: the request goes to
/v1/chat/completions and the gateway translates it back to Anthropic format. But the hop is
wasted, and it is the lossier direction — thinking blocks and cache control survive the native
path better.
Force openai-completions when the reverse happens. Each side has a group flag that closes
its non-native path outright:
claude_code_only— rejects/v1/chat/completionswith a 403. Harmless here: such a group is Anthropic-platform and serves Claude ids, which already go to/v1/messages.allow_messages_dispatch— an OpenAI group rejects/v1/messagesunless this is on, and it defaults to off.
Only the second one can bite, and only for a key whose model list carries Claude ids while its
group is OpenAI-platform. That takes a deliberate setup: /v1/models publishes the keys of each
account's model_mapping, so an operator has to have mapped something like
"claude-sonnet-4-5": "gpt-5.5" onto an OpenAI account. (The group-level
messages_dispatch_model_config that normally points Claude Code at OpenAI accounts does not
appear in the model list, so the usual setup is unaffected.)
When it does happen the first request fails loudly and says exactly why:
403 {"type":"permission_error","message":"This group does not allow /v1/messages dispatch"}
Fix it by setting SUB2API_PROTOCOL to openai-completions, or by turning on
allow_messages_dispatch for the group.
Confirming the split
The startup line reports which protocol each model landed on:
pi --list-models
[sub2api] registered 12 model(s) — anthropic-messages: claude-opus-4-7, claude-sonnet-4-6 | openai-completions: gpt-5.5, gpt-5.4
After forcing, only one side should remain. Note that this line is only readable in
--list-models and print mode — the interactive TUI clears the screen as it starts.
Use
pi --list-models # models appear under the sub2api provider
pi --provider sub2api --model gpt-5.5
pi --provider sub2api --model claude-opus-4-7
In an interactive session, /usage prints balance and cost:
sub2api usage
balance -67.01 USD
today $0.27 · 9 req · 57,826 tok
total $184.21 · 5,965 req · 105,534,943 tok
top models (by cost)
gpt-5.5 $0.27 · 52,300 tok · 4 req
Notes
- API type: the OpenAI side defaults to
openai-completions;openai-responsesis available and is the gateway's native path.openai-codex-responsesis not wired up: pi builds its URL as{baseUrl}/codex/responses, so it needs{BASE}/backend-api/codexas the base rather than{BASE}/v1. That route does answer, so supporting it is possible — it just needs its own base URL. - Cost: pi's per-token
costis set to0, because sub2api's/v1/modelsreturns no pricing. Cost comes from the gateway's own accounting via/usage, not from pi's estimate. - Model list: embedding and image-generation models are filtered out; only chat-capable models are registered.
- Thinking levels: on the OpenAI path, reasoning models expose
off/low/medium/high/xhigh;minimalis marked unsupported because the upstream rejectsreasoning_effort: "minimal". - Model metadata: the gateway's model list reports no per-model limits, so
contextWindowandmaxTokenscome from a table keyed on the model id — Claude generations mirror pi's built-in Anthropic values, and everything else gets 400000/128000. Claude ids newer than that table get their generation's context window with a conservative output cap: an under-declaredmaxTokensonly shortens replies, while an over-declared one is a hard 400.
License
MIT