@counterposition/skill-pi 0.87.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/package.json +15 -4
- package/skills/pi/README.md +8 -7
- package/skills/pi/SKILL.md +14 -7
- package/skills/pi/evals/evals.json +81 -0
- package/skills/pi/examples/classifier-gate.ts +46 -0
- package/skills/pi/examples/nested-tools.ts +52 -0
- package/skills/pi/examples/structured-tool.ts +61 -0
- package/skills/pi/references/codemode.md +92 -0
- package/skills/pi/references/extensions.md +37 -4
- package/skills/pi/references/mcp.md +137 -0
- package/skills/pi/references/packages.md +11 -5
- package/skills/pi/references/patterns.md +34 -0
- package/skills/pi/references/providers.md +105 -14
- package/skills/pi/references/sdk.md +80 -12
- package/skills/pi/references/settings.md +35 -16
|
@@ -103,6 +103,7 @@ Useful agent events:
|
|
|
103
103
|
- `before_provider_headers` (Pi 0.80.4) — mutate `event.headers` in place after outgoing HTTP headers are assembled: set a key to a string to add/override, `null` to delete. Runs once per provider request; retries reuse the same headers
|
|
104
104
|
- `before_provider_request`
|
|
105
105
|
- `after_provider_response` — inspect the provider HTTP status and headers before stream consumption
|
|
106
|
+
- `provider_stream_event` (Pi 0.99.0) — notification-only view of each parsed provider stream event before Pi normalizes it (`provider`, `api`, `model`, `data`). Treat `data` as read-only; handlers are awaited in stream order, so keep them fast. Not persisted
|
|
106
107
|
- `model_select`
|
|
107
108
|
- `thinking_level_select` — observe interactive thinking-level changes
|
|
108
109
|
- `ui_prompt_start` / `ui_prompt_end` (Pi 0.84.4) — distinguish active agent work from time spent waiting on blocking `ctx.ui` prompts (`kind`: `select`/`confirm`/`input`/`editor`/`custom`)
|
|
@@ -121,6 +122,8 @@ Useful tool events:
|
|
|
121
122
|
- `tool_result`
|
|
122
123
|
- `tool_execution_end`
|
|
123
124
|
|
|
125
|
+
Calls another tool makes through `ctx.executeTool()` (codemode scripts, orchestrating tools) fire the same tool events, so permission gates see them; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
|
|
126
|
+
|
|
124
127
|
Since Pi 0.86.0, tool-call `arguments` and result `details` must be JSON-compatible values (no `Date`, `Map`, class instances, `undefined` array slots); use `details: undefined` when there is nothing structured to keep. Tools registered without a parameter schema are rejected at registration.
|
|
125
128
|
|
|
126
129
|
Tool results may include `terminate: true` to end the current tool batch without an automatic follow-up LLM turn — useful for tools that produce a structured final answer (see `examples/extensions/structured-output.ts` in the Pi repo). Since Pi 0.84.1, blocked `tool_call` results may also return `terminate: true`; the agent stops early only when every finalized result in the batch is terminating. Tools that make nested LLM calls can return their combined `Usage` as `usage` on the tool result — Pi persists it and includes it in footer, `/session`, and RPC session totals (Pi 0.81.0), and `tool_result` handlers may inspect or replace that value.
|
|
@@ -137,6 +140,7 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
137
140
|
- `ctx.sessionManager` (read-only; `buildContextEntries()` returns active-branch entries with compaction applied, Pi 0.80.4)
|
|
138
141
|
- `ctx.modelRegistry` / `ctx.model` / `ctx.thinkingLevel` / `ctx.scopedModels` (Pi 0.83.0) — `ctx.modelRegistry` is the synchronous extension-facing facade; its `refresh()` became `Promise<void>` in Pi 0.80.8 (await it before synchronous registry reads). Since Pi 0.84.x it also exposes `getProvider(id)` (effective pi-ai provider) and `getProviderAuth(id)` (API key, headers, base URL, provider-scoped env — no loaded model required). `ctx.scopedModels` is the read-only session model scope resolved from `--models`/`enabledModels` (`{ model, thinkingLevel? }[]`, empty when unscoped); prefer it over enumerating `getAvailable()` for model pickers
|
|
139
142
|
- `ctx.modelRegistry.stream()` / `streamSimple()` / `complete()` (Pi 0.86.0) — nested model calls through configured providers with request-time auth; prefer `streamSimple()` for provider-neutral calls, pass `ctx.signal`, and report the combined `usage` on the tool result
|
|
143
|
+
- `ctx.modelRegistry.findOfType(type, provider, id)`, `getModelsOfType()`, `getAvailableOfType()`, and `classify(model, { state, questions }, { signal })` (Pi 0.99.0), plus `generateImages(model, { input }, options?)` (Pi 1.0.0) — classifier and image models live beside chat models; neither call rejects, so check `result.stopReason`, and report the returned `usage` on the tool result (see `references/providers.md`)
|
|
140
144
|
- `ctx.hasUI` — `true` in TUI and RPC modes
|
|
141
145
|
- `ctx.signal` — the active agent abort signal (or `undefined` when idle); pass it to `fetch`/model calls for abort-aware nested work
|
|
142
146
|
- `ctx.isIdle()` / `ctx.hasPendingMessages()`
|
|
@@ -146,6 +150,8 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
146
150
|
- `ctx.getSystemPrompt()`
|
|
147
151
|
- `ctx.isProjectTrusted()` — the effective project trust decision, including temporary `--approve`/`--no-approve` decisions
|
|
148
152
|
|
|
153
|
+
Tool `execute()` receives `ExtensionToolContext`, which adds `ctx.tools` (the tools this call can reach) and `ctx.executeTool(name, args, { signal?, onUpdate? })` (Pi 0.99.0). `executeTool()` runs validation and the `tool_call`/`tool_result` hooks like a model call and never rejects: unknown tools, validation errors, blocks, and thrown errors come back as `{ isError: true }`, including after an abort. The signal defaults to the calling tool's. Nested usage is added to the calling tool's result, so report only the tool's own `usage`.
|
|
154
|
+
|
|
149
155
|
Use `CONFIG_DIR_NAME` (exported from `@earendil-works/pi-coding-agent`) instead of hardcoding `.pi` when building project config paths, e.g. `join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json")`.
|
|
150
156
|
|
|
151
157
|
Command handlers receive `ExtensionCommandContext`, which extends the above with session-control methods that would deadlock from event handlers: `ctx.getSystemPromptOptions()` (inspect the base system-prompt inputs), `ctx.waitForIdle()`, `ctx.newSession()`, `ctx.fork()`, `ctx.switchSession()`, `ctx.navigateTree()`, and `ctx.reload()`.
|
|
@@ -181,12 +187,27 @@ Important rules:
|
|
|
181
187
|
|
|
182
188
|
1. Truncate large output.
|
|
183
189
|
2. Respect `signal.aborted`.
|
|
184
|
-
3. Throw on failure instead of returning fake success.
|
|
185
|
-
4. Use the file-mutation queue for write/edit style tools.
|
|
190
|
+
3. Throw on failure instead of returning fake success, or return `isError: true` when the failure still carries data. A returned object without `isError` counts as success.
|
|
191
|
+
4. Use the file-mutation queue (`withFileMutationQueue()`) for write/edit style tools.
|
|
186
192
|
5. Put reconstructable state in `details`; it persists in session history.
|
|
187
193
|
|
|
194
|
+
## Tool Exposure & Structured Results
|
|
195
|
+
|
|
196
|
+
Pi 0.99.0 added fields for tools that other tools, codemode scripts, and permission extensions consume ([docs](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/extensions.md#tool-exposure), [types](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/core/extensions/types.ts)):
|
|
197
|
+
|
|
198
|
+
- `outputSchema` (TypeBox) plus `structuredContent` on every result: the model still reads `content`; codemode scripts receive `structuredContent` instead of text. Without `outputSchema`, scripts get the text. `structuredContent` must be JSON-compatible. A `tool_result` handler that replaces `content` must also return `structuredContent`, or it is dropped.
|
|
199
|
+
- `isError: true` on a returned result: the model sees an error, while `details` and `structuredContent` are kept for the UI and scripts.
|
|
200
|
+
- `exposure`: `direct` (default), `model-only`, `codemode`, `deferred`, or `hidden`. `direct` and `model-only` tools activate on registration; the others do not (`defaultActive: false` also keeps a `direct` tool inactive until `--tools`/`defaultTools`/`setActiveTools()` names it). Tools cannot be unregistered: re-register with `exposure: "hidden"` to withdraw one. The table of what each value declares and allows is in `references/codemode.md`.
|
|
201
|
+
- `namespace: { name, description?, instructions? }` groups related tools, as MCP servers do. Codemode lists a namespace under one heading with its short `description`. `instructions` (Pi 0.99.2) holds longer usage guidance that is not listed; scripts read it with `describeNamespace(name)`.
|
|
202
|
+
- `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, with MCP meanings. Missing hints mean "not read-only, may be destructive, open world". They are unverified; mark writes neither read-only nor casually idempotent.
|
|
203
|
+
- `prepareLoadout(loadout)`: for tools that orchestrate others. Runs whenever the active tools change with `declared`, `callable`, and `registered` tools, and returns replacement `descriptions` and `hiddenDeclarations` (active tools left out of requests but still callable). The built-in `codemode` uses only this, `exposure`, and `ctx.executeTool()`.
|
|
204
|
+
|
|
205
|
+
`pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`. `pi.setActiveTools()` ignores unknown and `hidden` names. Compilable examples: `examples/structured-tool.ts` and `examples/nested-tools.ts`.
|
|
206
|
+
|
|
188
207
|
## Dynamic Tool Loading
|
|
189
208
|
|
|
209
|
+
For tools the model should find on demand, prefer `exposure: "deferred"` with the built-in `tool_search` (Pi 0.99.0), or `exposure: "codemode"` for tools scripts call. `tool_search` is inactive by default: enable it with `"defaultTools": ["+tool_search"]` or `--tools`, or let a connected MCP server with `deferred` exposure turn it on. Without it, the model can reach `deferred` tools only through codemode. The manual loader pattern below still works.
|
|
210
|
+
|
|
190
211
|
Pi 0.80.7 lets an extension register many tools while keeping only a small initial set active, then add more during execution — cache-friendly on models with native deferred loading. Lifecycle:
|
|
191
212
|
|
|
192
213
|
1. Register every tool with `pi.registerTool()` (all appear in `pi.getAllTools()`).
|
|
@@ -206,7 +227,7 @@ Cache tips: keep the loader active for the whole session; add rather than replac
|
|
|
206
227
|
- `{ type: "grammar", variants: { openai_lark?: string, openai_regex?: string } }` — OpenAI Lark/regex grammar encodings
|
|
207
228
|
- Set `false` to explicitly disable for that tool
|
|
208
229
|
|
|
209
|
-
Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway).
|
|
230
|
+
Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway). With `strict: "prefer"`, unsupported combinations fall back to normal function tools; with `strict: "require"`, the request fails when the model has no strict tools or the schema uses keywords strict mode rejects. Since Pi 0.99.2 Anthropic strict tools also reject keywords such as `minimum`/`maximum`, so such `prefer` tools are sent non-strict. Since Pi 0.86.0, strict-prefer JSON-schema sampling is on by default for the built-in `read`, `bash`, `powershell`, `edit`, and `write` tools (no `PI_EXPERIMENTAL` needed); re-register a definition with `constrainedSampling: false` to opt out.
|
|
210
231
|
|
|
211
232
|
## Display Transformers
|
|
212
233
|
|
|
@@ -311,6 +332,14 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
311
332
|
});
|
|
312
333
|
```
|
|
313
334
|
|
|
335
|
+
## MCP Servers
|
|
336
|
+
|
|
337
|
+
`pi.registerMcpServer(name, config)` adds a server for the current session with the `mcp.json` entry shape; `pi.unregisterMcpServer(name)` closes it. Registrations are not saved, and an `mcp.json` server with the same name wins. Extensions that implement their own MCP client read `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`. See `references/mcp.md`.
|
|
338
|
+
|
|
339
|
+
## Virtual Models
|
|
340
|
+
|
|
341
|
+
`pi.registerVirtualModel({ provider, id, name, thinkingLevels?, route(request, ctx) })` (experimental, Pi 0.99.0) adds a selectable model that picks a physical model and thinking level for every request. See `references/providers.md#virtual-models`.
|
|
342
|
+
|
|
314
343
|
## Provider Integration
|
|
315
344
|
|
|
316
345
|
Extensions can register or override providers:
|
|
@@ -341,6 +370,8 @@ pi.unregisterProvider("my-provider");
|
|
|
341
370
|
|
|
342
371
|
Dynamic providers can implement `refreshModels(context)` (Pi 0.80.8) for model discovery: Pi calls it during catalog refresh (`/model`, `pi update --models`) and publishes the returned list; its models replace extension-provided `models`. Since Pi 0.84.0 the context is read-snapshot + generation-checked publication: read `context.stored` (the persisted provider snapshot) instead of `context.store`, and persist through `context.publish({ update?, persist? })` — `persist` omitted leaves storage unchanged, a `ModelsStoreEntry` writes it, `persist: null` deletes it. Config-form callbacks that only return models remain unchanged.
|
|
343
372
|
|
|
373
|
+
A config-form `models` list replaces the provider's models across chat, image, and classifier operations (Pi 0.99.0). Omitted `type` means `"chat"`; image and classifier entries need an explicit `type` plus implementations keyed by `api` through `images`/`classifiers` (see `references/providers.md`).
|
|
374
|
+
|
|
344
375
|
Since Pi 0.81.0, extensions can also register a complete pi-ai `Provider` via `pi.registerProvider(createProvider({...}))` — native auth (`login`/`resolve`), `getModels`, `refreshModels`, `filterModels`, and custom streaming. The registered provider becomes the composition base; `models.json` overrides still apply above it. Prefer this when you need real authentication or stream behavior rather than just a model list. The legacy config form (`name`, `baseUrl`, `apiKey`, `models`, `oauth`, `streamSimple`) remains supported. OAuth `refreshToken(credentials, signal)` callbacks must accept and honor the abort signal since Pi 0.84.0.
|
|
345
376
|
|
|
346
377
|
If you need auth for a specific model request, use:
|
|
@@ -355,7 +386,9 @@ Custom `streamSimple` implementations receive a normalized `TranscriptContext` s
|
|
|
355
386
|
|
|
356
387
|
## Handy Patterns
|
|
357
388
|
|
|
358
|
-
- Permission gates via `tool_call`
|
|
389
|
+
- Permission gates via `tool_call` (they also see nested calls from codemode; see `references/codemode.md`)
|
|
390
|
+
- Data tools for codemode via `outputSchema` + `structuredContent`
|
|
391
|
+
- Orchestrating tools via `ctx.executeTool()` with `exposure: "model-only"`
|
|
359
392
|
- Prompt injection via `before_agent_start` (`systemPromptOptions.sections`)
|
|
360
393
|
- Model-context pruning via `context`, or durable omission via `context_edit` entries from `turn_end`/`agent_before_settle`
|
|
361
394
|
- Branch or compaction customization via `session_before_tree` / `session_before_compact`
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# MCP Servers
|
|
2
|
+
|
|
3
|
+
Pi connects to MCP servers through a built-in extension. There is no package to install. Checked against Pi v1.0.0: [mcp.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/mcp.md), [cli.md#mcp-commands](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/cli.md#mcp-commands), [extensions.md#mcp-servers](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/extensions.md#mcp-servers).
|
|
4
|
+
|
|
5
|
+
## Configure
|
|
6
|
+
|
|
7
|
+
| File | Scope |
|
|
8
|
+
|------|-------|
|
|
9
|
+
| `~/.pi/agent/mcp.json` | Global. Put personal servers and servers with credentials here |
|
|
10
|
+
| `.pi/mcp.json` | Project. Read only after the project is trusted, because stdio servers run commands |
|
|
11
|
+
|
|
12
|
+
A project entry replaces a global entry with the same name. The format is the usual `mcpServers` object, so Claude Desktop, Claude Code, and Cursor entries copy over unchanged:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"filesystem": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
20
|
+
},
|
|
21
|
+
"docs": {
|
|
22
|
+
"url": "https://example.com/mcp",
|
|
23
|
+
"headers": { "Authorization": "Bearer ${DOCS_TOKEN}" },
|
|
24
|
+
"description": "Search and read the product documentation",
|
|
25
|
+
"exposure": "direct"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- stdio: `command` (one executable, not a shell string), `args`, `env`, `cwd`. Relative `cwd` resolves against the session directory; a leading `~/` works in `command`, args, and `cwd`.
|
|
32
|
+
- HTTP: `url`, `headers`, `oauth`. Only streamable HTTP; `"type": "sse"` is rejected (try the server's `/mcp` endpoint instead of `/sse`).
|
|
33
|
+
- `type` is optional (`stdio`, `http`, or `streamable-http`); `command` implies stdio and `url` implies HTTP.
|
|
34
|
+
- `env` and `headers` values accept `${NAME}` and `!command`. A command must produce the whole value: `"Authorization": "!echo Bearer $(gh auth token)"`.
|
|
35
|
+
- `timeout` is seconds per request (default 60); progress notifications reset it. `enabled: false` keeps the entry without connecting.
|
|
36
|
+
- `description` (Pi 0.99.2): one sentence on what the server offers. It lists the server in the system prompt's `mcp_servers` section, ranks its tools in tool search, and is returned by codemode's `describeNamespace()`. Without it, Pi uses the first line of the server instructions once connected.
|
|
37
|
+
- Server names use letters, digits, `_`, `-`. Tools are named `mcp__<server>__<tool>`, with every character other than letters, digits, and `_` replaced by `_` (Pi 0.99.2: `mcp__my-server__x` is now `mcp__my_server__x`; update permission rules that name the old spelling). Tools of one server whose names then collide all get a hash suffix. Server names that differ only in `-` and `_` count as the same server; a second one is rejected.
|
|
38
|
+
- Invalid entries are skipped and reported; other servers still connect.
|
|
39
|
+
|
|
40
|
+
Converting other clients: VS Code uses `servers` + `inputs` (move under `mcpServers`, replace `${input:...}` with `${NAME}`); Codex uses TOML `[mcp_servers.<name>]` (same fields as JSON); opencode uses `"type": "local"` with an array `command` (split into `command`/`args`), `environment` for `env`, and `{env:NAME}` for `${NAME}`.
|
|
41
|
+
|
|
42
|
+
## Set Up From a Shell
|
|
43
|
+
|
|
44
|
+
These commands work outside a session, so an agent can run them through `bash`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
|
|
48
|
+
pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN --exposure direct --description "Product docs"
|
|
49
|
+
pi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp # -l writes .pi/mcp.json
|
|
50
|
+
pi mcp list # connects to every enabled server; exits 1 while anything is wrong
|
|
51
|
+
pi mcp login sentry # OAuth: opens the browser and waits for the user to approve
|
|
52
|
+
pi mcp remove docs # keeps stored OAuth credentials
|
|
53
|
+
pi mcp logout sentry
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`pi mcp add` also takes `--oauth-client-id`, `--oauth-client-secret`, `--oauth-callback-port`, and `--oauth-client-name`. It does not connect; verify with `pi mcp list`. A running session picks up added or changed servers after `/reload` or a new session.
|
|
57
|
+
|
|
58
|
+
In a session, `/mcp` opens the server manager (state, tools, errors, sign in/out, reconnect, exposure, enable/disable). Changes are written back to the `mcp.json` that defines the server. Outside the TUI, `/mcp` prints status and `/mcp login|logout|reconnect <server>` run directly.
|
|
59
|
+
|
|
60
|
+
Pi connects every enabled server in the background when a session starts (Pi 0.99.2). The first prompt waits up to 10 seconds only for servers with `direct` tools. Other servers are waited for when needed: a codemode script waits for the servers it names (`mcp__<server>`), and `searchTools()`, `ALL_TOOLS`, `tool_search`, and the resource tools wait for all of them. Server log notifications go to `~/.pi/agent/mcp.log`.
|
|
61
|
+
|
|
62
|
+
### Authentication
|
|
63
|
+
|
|
64
|
+
- OAuth is used only for HTTP servers whose config has no `Authorization` header. Tokens live in `~/.pi/agent/mcp-auth.json`, stored per server name and URL (Pi 1.0.0), so two names for the same URL sign in with different accounts.
|
|
65
|
+
- `oauth: { clientId?, clientSecret?, callbackPort?, callbackUrl?, scope?, clientName?, authServerMetadataUrl? }`:
|
|
66
|
+
- `clientId`/`clientSecret` set a pre-registered client for servers without dynamic client registration.
|
|
67
|
+
- `callbackPort` alone gives `http://127.0.0.1:<port>/callback`; a `callbackUrl` (HTTP on a loopback host) without a port gets `callbackPort` or a free one.
|
|
68
|
+
- `scope`: space-separated scopes to request when the server does not advertise the ones it needs.
|
|
69
|
+
- `clientName` (Pi 0.99.2): the name sent at client registration (default `pi`), for servers that only accept known clients, e.g. `"Claude Code"`. Sign out first to register again under a new name.
|
|
70
|
+
- `authServerMetadataUrl` (Pi 1.0.0): the authorization server metadata document to use instead of discovery, for servers that advertise a wrong authorization server or none. Trusted as configured; HTTPS except on loopback.
|
|
71
|
+
- Pi rejects an authorization response whose `iss` names another authorization server (RFC 9207), and step-up sign-in after `insufficient_scope` keeps the scopes already granted (Pi 1.0.0).
|
|
72
|
+
- `"auth": { "provider": "<provider>" }` (Pi 0.99.2) sends a `/login` provider's current token as the bearer token instead of MCP OAuth, read on every request. Allowed only in the global `mcp.json` and from extensions, and HTTPS except on loopback. After a Radius sign-in, `/login` offers to add the Radius MCP server this way.
|
|
73
|
+
|
|
74
|
+
## Exposure
|
|
75
|
+
|
|
76
|
+
`exposure` decides how the model reaches a server's tools:
|
|
77
|
+
|
|
78
|
+
| Value | Declared to the model | Callable from codemode | Notes |
|
|
79
|
+
|-------|----------------------|------------------------|-------|
|
|
80
|
+
| `codemode` (default) | No | Yes, found with `searchTools()`, `describeNamespace()`, or `ALL_TOOLS`; not listed in the `codemode` description | Pi activates `codemode` when such a server connects |
|
|
81
|
+
| `deferred` | After `tool_search` loads them | Yes | Pi activates `tool_search`; for large servers without codemode |
|
|
82
|
+
| `direct` | Yes, like built-in tools | Yes | |
|
|
83
|
+
| `hidden` | No | No | Registered but unreachable |
|
|
84
|
+
|
|
85
|
+
`toolExposure` overrides single tools by name or `*` pattern (exact name wins, then first matching pattern):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"github": {
|
|
91
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
92
|
+
"exposure": "deferred",
|
|
93
|
+
"toolExposure": { "search_code": "direct", "get_*": "codemode", "delete_*": "hidden" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`codemode-deferred` is an alias for `codemode` since Pi 0.99.2. Servers with `codemode` or `deferred` tools are listed in an `mcp_servers` system prompt section with how their tools are reached and a one-line summary. Pi updates it when a prompt starts and appends a changed section to the conversation, so neither tool descriptions nor the cached prefix change when servers connect.
|
|
100
|
+
|
|
101
|
+
Undeclared tools (`codemode`, `deferred`) are reachable through either tool: codemode scripts can call a `deferred` server's tools, and `tool_search` can load a `codemode` server's tools. Set top-level `"autoEnableCodemode": false` in `mcp.json` to stop Pi from activating `codemode`. Pi warns once when neither `codemode` nor `tool_search` is active, because the tools are then unreachable.
|
|
102
|
+
|
|
103
|
+
## Results
|
|
104
|
+
|
|
105
|
+
- Text results over 20KB reach the model with the middle cut out; the full text goes to a temp file named in the result.
|
|
106
|
+
- Codemode scripts receive the whole `CallToolResult` (`content`, `structuredContent`, `isError`). A result with `isError` resolves in scripts and is reported to the model as an error for direct calls. `image(result.content[0])` forwards an image block. Server instructions are not part of any tool description; scripts read them with `describeNamespace("mcp__<server>")`.
|
|
107
|
+
- Servers with resources add `list_mcp_resources`, `list_mcp_resource_templates`, and `read_mcp_resource` (marked read-only).
|
|
108
|
+
- Tool calls are never retried (the server may have run them); resource reads retry once on 408/429/5xx.
|
|
109
|
+
|
|
110
|
+
## Permissions
|
|
111
|
+
|
|
112
|
+
Every MCP call goes through Pi's tool pipeline, so `tool_call` and `tool_result` handlers (permission gates included) see MCP tools. Calls from codemode scripts are nested calls; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions). `pi.getAllTools()` reports the server's `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` annotations. They are unverified hints; see [Tool Exposure](extensions.md#tool-exposure--structured-results).
|
|
113
|
+
|
|
114
|
+
## Servers From Extensions
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira", exposure: "codemode", description: "Jira issues" });
|
|
118
|
+
pi.unregisterMcpServer("jira");
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- Same config shape as an `mcpServers` entry. Registrations are not saved: register on every load.
|
|
122
|
+
- Registered during load: connects on `session_start` with the `mcp.json` servers. Registered later: connects right away.
|
|
123
|
+
- An `mcp.json` server with the same name wins. Another extension's name, an invalid name, or an invalid config throws.
|
|
124
|
+
- `pi mcp` shell commands do not load extensions and only see `mcp.json` servers.
|
|
125
|
+
- Alternative MCP extensions read registrations with `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`.
|
|
126
|
+
|
|
127
|
+
## Turning It Off or Replacing It
|
|
128
|
+
|
|
129
|
+
- `"extensions": ["-builtin:mcp"]` in settings, or `pi config` → Built-in, disables the built-in support. `pi mcp` shell commands still work.
|
|
130
|
+
- `--no-extensions` disables built-ins too; `pi -ne -e builtin:mcp` keeps only MCP.
|
|
131
|
+
- An extension that registers the `/mcp` command (for example `pi-mcp-adapter`) replaces the built-in support: `mcp.json` is then not read in sessions.
|
|
132
|
+
|
|
133
|
+
## SDK
|
|
134
|
+
|
|
135
|
+
SDK sessions do not load built-in extensions. Add `createMcpExtension()`, plus `createCodemodeExtension()` for `codemode` servers and `createToolSearchExtension()` for `deferred` servers, then call `session.bindExtensions()` so `session_start` connects the servers. See `references/sdk.md`.
|
|
136
|
+
|
|
137
|
+
The standalone client is published as `@earendil-works/pi-mcp` (stdio and streamable HTTP transports, OAuth, an in-memory testing transport). It has no Pi session dependency.
|
|
@@ -94,16 +94,21 @@ If there is no `pi` manifest, Pi auto-discovers:
|
|
|
94
94
|
|
|
95
95
|
## Dependencies
|
|
96
96
|
|
|
97
|
+
Checked against Pi v1.0.0 [packages.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/packages.md#declare-dependencies).
|
|
98
|
+
|
|
97
99
|
- Normal runtime dependencies belong in `dependencies`
|
|
98
|
-
- Pi
|
|
100
|
+
- Pi supplies these to extensions; declare them as peer deps with `"*"` ranges and never bundle them:
|
|
99
101
|
- `@earendil-works/pi-ai`
|
|
100
102
|
- `@earendil-works/pi-agent-core`
|
|
101
103
|
- `@earendil-works/pi-coding-agent`
|
|
102
104
|
- `@earendil-works/pi-tui`
|
|
103
105
|
- `typebox`
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
- Git package installs
|
|
106
|
+
- Do not list those host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping and create duplicate classes and registries; since Pi 0.99.0 Pi reports an extension warning for this manifest.
|
|
107
|
+
- Pi suppresses automatic peer installation for managed npm packages and for git packages installed with npm (`--omit=dev --legacy-peer-deps`), pnpm (`--prod`, no auto-installed peers), or Bun (`--omit=dev --omit=peer`). Local path packages are not installed or modified; their dependency tree is the author's responsibility.
|
|
108
|
+
- Git package installs omit dev dependencies — runtime needs go in `dependencies`, not `devDependencies`
|
|
109
|
+
- Installed packages load with separate module roots. Do not rely on two packages sharing one dependency instance, or on one package resolving another package's undeclared dependency.
|
|
110
|
+
- Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.
|
|
111
|
+
- Pin dev dependencies to the Pi line you test against (e.g. `^1.0.0`) so typecheck/tests validate against what users run; keep peers `"*"` so installs stay compatible across Pi versions. `"*"` does not prove compatibility: document the minimum supported Pi version and feature-check newer APIs (for example `ctx.executeTool`, `pi.registerMcpServer`) if you support older hosts.
|
|
107
112
|
|
|
108
113
|
## Package Filtering
|
|
109
114
|
|
|
@@ -134,4 +139,5 @@ Rules:
|
|
|
134
139
|
|
|
135
140
|
- Project settings override global settings for the same package identity — unless the project entry has `autoload: false`, in which case it is applied as a delta over the global entry (Pi 0.80.4)
|
|
136
141
|
- Identity is package name for npm, repo URL for git, and resolved absolute path for local sources
|
|
137
|
-
- `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed)
|
|
142
|
+
- `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed). Since Pi 0.99.0 its Built-in section also disables the built-in `mcp`, `llama.cpp`, `codemode`, and `tool-search` extensions, stored as `-builtin:<name>` in `extensions`
|
|
143
|
+
- The `codemode`, `tool-search`, and `mcp` built-ins are replaceable: an extension that registers one of their tools, commands, or flags (for example `/mcp`, `codemode`, or `tool_search`) replaces that built-in, and Pi warns
|
|
@@ -19,6 +19,11 @@ Practical recipes for extending Pi. Each pattern is a self-contained example.
|
|
|
19
19
|
13. [File Watcher / Trigger](#file-watcher--trigger)
|
|
20
20
|
14. [Input Transform](#input-transform)
|
|
21
21
|
15. [Tool Override](#tool-override)
|
|
22
|
+
16. [Structured Tool](#structured-tool)
|
|
23
|
+
17. [Nested Tool Calls](#nested-tool-calls)
|
|
24
|
+
18. [Classifier Gate](#classifier-gate)
|
|
25
|
+
|
|
26
|
+
Patterns 16–18 are full files under `examples/` that type-check against Pi 1.0.0 in this skill's package; read them rather than retyping.
|
|
22
27
|
|
|
23
28
|
---
|
|
24
29
|
|
|
@@ -43,6 +48,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
48
|
}
|
|
44
49
|
```
|
|
45
50
|
|
|
51
|
+
The handler also runs for `bash` calls made by codemode scripts; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
|
|
52
|
+
|
|
46
53
|
## Protected Paths
|
|
47
54
|
|
|
48
55
|
Block writes to sensitive files:
|
|
@@ -313,3 +320,30 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
313
320
|
}
|
|
314
321
|
});
|
|
315
322
|
```
|
|
323
|
+
|
|
324
|
+
## Structured Tool
|
|
325
|
+
|
|
326
|
+
`examples/structured-tool.ts` registers a read-only data tool:
|
|
327
|
+
|
|
328
|
+
- `outputSchema` plus `structuredContent` on every result, so codemode scripts get `{ files, missing }` as an object while the model reads a text summary.
|
|
329
|
+
- `annotations: { readOnlyHint: true, openWorldHint: false }` for permission extensions, and a `namespace` for grouping in the `codemode` description.
|
|
330
|
+
- `isError: true` when no file was found: the model sees an error, scripts still get the data.
|
|
331
|
+
- Only `ENOENT` is treated as "missing"; other errors are rethrown.
|
|
332
|
+
|
|
333
|
+
## Nested Tool Calls
|
|
334
|
+
|
|
335
|
+
`examples/nested-tools.ts` registers `read_many`, which reads several files in parallel through `ctx.executeTool("read", ...)`:
|
|
336
|
+
|
|
337
|
+
- `exposure: "model-only"`: the model can call it, other tools and codemode cannot, so it cannot recurse.
|
|
338
|
+
- Checks `ctx.tools` first: an inactive `direct` tool is not callable.
|
|
339
|
+
- Each nested call runs every `tool_call` gate (see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions)). `executeTool()` never rejects, and aborted calls come back as `isError`, so the tool calls `signal?.throwIfAborted()` after the batch to report the abort as an abort rather than as failed reads.
|
|
340
|
+
- Streams progress through `onUpdate`.
|
|
341
|
+
|
|
342
|
+
## Classifier Gate
|
|
343
|
+
|
|
344
|
+
`examples/classifier-gate.ts` asks a classifier model whether a `bash` command is destructive before it runs:
|
|
345
|
+
|
|
346
|
+
- Finds Jev with `ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest")` and calls `ctx.modelRegistry.classify(..., { signal: ctx.signal })`.
|
|
347
|
+
- `classify()` never rejects: it checks `stopReason` (`"aborted"` blocks, `"error"` falls through to confirmation) and validates the `bool` answer's probability.
|
|
348
|
+
- Fails closed: a missing model, an error, an out-of-range answer, or a probability above the strict `MAX_ALLOWED_PROBABILITY` (0.2; tune it) asks the user, and blocks when `ctx.hasUI` is false.
|
|
349
|
+
- Uses `satisfies ClassifierContext["questions"]` so the question literal keeps its narrow types.
|
|
@@ -6,12 +6,13 @@ Pi supports subscription-based providers via OAuth, API-key providers via env va
|
|
|
6
6
|
|
|
7
7
|
Use `/login` in interactive mode, then select a provider. The `/login` selector is fuzzy-searchable and shows where each entry's auth comes from (`--api-key`, env var, custom provider) without leaking the secret.
|
|
8
8
|
|
|
9
|
-
- Anthropic Claude Pro / Max — third-party usage draws from extra usage and is billed per token (suppress the warning via `warnings.anthropicExtraUsage`)
|
|
10
|
-
- OpenAI
|
|
9
|
+
- Anthropic Claude Pro / Max — third-party usage draws from extra usage and is billed per token (suppress the warning via `warnings.anthropicExtraUsage`). Since Pi 1.0.0 `/login anthropic` offers browser login (default) or **copy code** login, which shows the authorization code on Anthropic's page for pasting into Pi when the browser runs on another machine
|
|
10
|
+
- OpenAI with a ChatGPT subscription (Pi 0.99.0) — `/login openai` offers **Sign in with ChatGPT**, an OAuth login that uses the subscription with the OpenAI API on the regular `openai` provider. Pi stores a stable `deviceId` for it in global settings. Subscription usage-limit errors are not retried
|
|
11
|
+
- OpenAI Codex (legacy) — the older `openai-codex` provider, renamed in Pi 0.99.0 and superseded by Sign in with ChatGPT. Its `/login` still offers browser auth or a device-code flow; its default model is `gpt-6.1-sol` (Pi 0.99.1). Prefer Sign in with ChatGPT on `openai` for new setups
|
|
11
12
|
- GitHub Copilot
|
|
12
13
|
- OpenRouter (Pi 0.82.0) — `/login openrouter` runs a PKCE flow that mints a user-controlled API key billed from OpenRouter credits; on headless/SSH hosts paste the redirect URL or authorization code into the prompt (Pi 0.83.0). `OPENROUTER_API_KEY` still works via **Use an API key**
|
|
13
14
|
- xAI (Grok/X subscription, Pi 0.80.8) — `/login xai` then **Use a subscription** (device-code OAuth); `XAI_API_KEY` remains available via **Use an API key**
|
|
14
|
-
- Radius (Pi 0.80.8) — a dynamic `pi-messages` gateway; `/login radius` stores OAuth tokens, and custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` plus a gateway `baseUrl`. Since Pi 0.86.0 a public Radius catalog ships for immediate/offline selection (cached and live gateway catalogs overlay it); custom gateways use their own catalog
|
|
15
|
+
- Radius (Pi 0.80.8) — a dynamic `pi-messages` gateway; `/login radius` stores OAuth tokens, and custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` plus a gateway `baseUrl`. Since Pi 0.86.0 a public Radius catalog ships for immediate/offline selection (cached and live gateway catalogs overlay it); custom gateways use their own catalog. Since Pi 1.0.0 `/login` lists **Sign in with Radius** at the top level and, after sign-in, offers to add the Radius MCP server to the global `mcp.json` with `"auth": { "provider": "radius" }`
|
|
15
16
|
- Meta Muse (Pi 0.86.1) — `/login meta` with automatic Model API key refresh; `META_API_KEY` also works
|
|
16
17
|
|
|
17
18
|
`/login <provider>` with autocomplete works since Pi 0.80.4, and login methods are provider-owned since 0.80.8 (registered pi-ai providers expose their own auth options and status in `/login`). Use `/logout` to clear stored OAuth credentials. Pi 0.71.0 removed built-in Google Gemini CLI and Google Antigravity providers. `pi auth check [provider|model]` (Pi 0.84.1) preflights credential readiness; `pi auth print-api-key` / `pi auth print-bearer-token` (Pi 0.83.0) export resolved credentials to external clients, refreshing OAuth as needed.
|
|
@@ -43,6 +44,7 @@ Since Pi 0.80.8, built-in catalogs are complemented by dynamic ones: `/model` re
|
|
|
43
44
|
| OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` |
|
|
44
45
|
| Amazon Bedrock | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |
|
|
45
46
|
| Radius | `RADIUS_API_KEY` | `radius` |
|
|
47
|
+
| TypeSafe (classifier models only) | `TYPESAFE_API_KEY` | `typesafe` |
|
|
46
48
|
| Hugging Face | `HF_TOKEN` | `huggingface` |
|
|
47
49
|
| Fireworks | `FIREWORKS_API_KEY` | `fireworks` |
|
|
48
50
|
| Together AI | `TOGETHER_API_KEY` | `together` |
|
|
@@ -56,6 +58,8 @@ Since Pi 0.80.8, built-in catalogs are complemented by dynamic ones: `/model` re
|
|
|
56
58
|
| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |
|
|
57
59
|
| Xiaomi MiMo Token Plan (CN/AMS/SGP) | `XIAOMI_TOKEN_PLAN_{CN,AMS,SGP}_API_KEY` | `xiaomi-token-plan-{cn,ams,sgp}` |
|
|
58
60
|
|
|
61
|
+
With no Anthropic key or token set, Pi 0.99.2+ uses Anthropic workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, and `ANTHROPIC_IDENTITY_TOKEN_FILE` are set (plus optional `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID`). The Anthropic SDK exchanges and refreshes the token, re-reading the identity token file, so keep that file fresh for long sessions.
|
|
62
|
+
|
|
59
63
|
## Auth File
|
|
60
64
|
|
|
61
65
|
`~/.pi/agent/auth.json` stores API keys and OAuth tokens with `0600` permissions:
|
|
@@ -86,12 +90,14 @@ Entries may include an `env` object (Pi 0.79.5) for provider-scoped environment
|
|
|
86
90
|
|
|
87
91
|
## Credential Resolution Order
|
|
88
92
|
|
|
89
|
-
1. `--api-key`
|
|
93
|
+
1. `--api-key` (runtime override)
|
|
90
94
|
2. `auth.json`
|
|
91
|
-
3.
|
|
92
|
-
4.
|
|
95
|
+
3. `apiKey` from `models.json`
|
|
96
|
+
4. Environment variables or ambient cloud credentials
|
|
97
|
+
|
|
98
|
+
Provider extensions can define their own authentication behavior.
|
|
93
99
|
|
|
94
|
-
##
|
|
100
|
+
## Provider-Specific Setup
|
|
95
101
|
|
|
96
102
|
### Azure OpenAI
|
|
97
103
|
|
|
@@ -151,12 +157,11 @@ Use `models.json` for OpenAI-compatible, Anthropic-compatible, Google-compatible
|
|
|
151
157
|
|
|
152
158
|
```json
|
|
153
159
|
{
|
|
154
|
-
"providers":
|
|
155
|
-
{
|
|
156
|
-
"name": "ollama",
|
|
160
|
+
"providers": {
|
|
161
|
+
"ollama": {
|
|
157
162
|
"baseUrl": "http://localhost:11434/v1",
|
|
158
163
|
"api": "openai-completions",
|
|
159
|
-
"apiKey": "
|
|
164
|
+
"apiKey": "ollama",
|
|
160
165
|
"models": [
|
|
161
166
|
{
|
|
162
167
|
"id": "llama3.2",
|
|
@@ -175,10 +180,12 @@ Use `models.json` for OpenAI-compatible, Anthropic-compatible, Google-compatible
|
|
|
175
180
|
}
|
|
176
181
|
]
|
|
177
182
|
}
|
|
178
|
-
|
|
183
|
+
}
|
|
179
184
|
}
|
|
180
185
|
```
|
|
181
186
|
|
|
187
|
+
`providers` is an object keyed by provider ID, not an array; the optional `name` field is only a display label. A `models` entry adds or replaces a model with the same ID on that provider; use `modelOverrides` to change metadata of an existing built-in or extension model.
|
|
188
|
+
|
|
182
189
|
`apiKey` is optional (Pi 0.80.0): omit it when auth comes from `/login`, `auth.json`, or CLI `--api-key`. Like `auth.json` keys, `apiKey` and `headers` values support `!command` execution and `$ENV_VAR` interpolation; plain uppercase strings are literals. Keyless local servers (e.g. Ollama) should keep a dummy value so their models appear in `/model`.
|
|
183
190
|
|
|
184
191
|
Common `api` values:
|
|
@@ -206,7 +213,7 @@ Common `api` values:
|
|
|
206
213
|
- `compat.thinkingFormat` supports OpenAI-compatible reasoning variants: `openrouter` sends `reasoning: { effort }`, `together` sends `reasoning: { enabled }` plus `reasoning_effort` when supported, `qwen-chat-template` targets local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`, `chat-template` (Pi 0.79.9) sends configurable `chat_template_kwargs` via `compat.chatTemplateKwargs` — e.g. `{ "thinking": { "$var": "thinking.enabled" } }` for DeepSeek models behind vLLM/Hugging Face chat templates — and `baseten` (Pi 0.84.0) sends `chat_template_args` via `compat.chatTemplateArgs`.
|
|
207
214
|
- **Breaking (Pi 0.80.7):** `compat.sendSessionIdHeader` was removed. Session affinity is now controlled by `compat.sessionAffinityFormat` (`"openai"` sends `session_id`/`x-client-request-id`, `"openai-nosession"` omits the underscore-containing `session_id` header, `"openrouter"` sends `x-session-id`; default auto-detected). Replace `sendSessionIdHeader: false` with `sessionAffinityFormat: "openai-nosession"`. `sendSessionAffinityHeaders` still gates the behavior for `openai-completions`.
|
|
208
215
|
- `compat.deferredToolsMode: "kimi"` (Pi 0.80.9) enables Kimi's deferred tool serialization; `compat.supportsToolReferences` / `compat.supportsToolSearch` enable native dynamic tool loading on verified custom endpoints, and `compat.supportsStrictTools` / `compat.supportsOpenAIGrammarTools` / per-model `constrainedSampling` gate constrained sampling (see `references/extensions.md`). `compat.supportsFinishReason: false` (Pi 0.84.0) tells pi to infer stop/toolUse for OpenAI-compatible streams that omit `finish_reason`.
|
|
209
|
-
- Advanced `compat` flags exist for proxy quirks (`cacheControlFormat` — now also covering tool-result text content, `supportsReasoningEffort`, `supportsLongCacheRetention`, `supportsEagerToolInputStreaming`, `supportsStrictMode`). See [Pi `docs/models.md`](https://github.com/earendil-works/pi/blob/
|
|
216
|
+
- Advanced `compat` flags exist for proxy quirks (`cacheControlFormat` — now also covering tool-result text content, `supportsReasoningEffort`, `supportsLongCacheRetention`, `supportsEagerToolInputStreaming`, `supportsStrictMode`). See [Pi `docs/models.md`](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/models.md) when a proxy rejects pi's defaults.
|
|
210
217
|
|
|
211
218
|
### Context Overflow Recovery
|
|
212
219
|
|
|
@@ -214,12 +221,96 @@ Pi can auto-compact and retry when a provider fails with a recognized context-wi
|
|
|
214
221
|
|
|
215
222
|
### llama.cpp
|
|
216
223
|
|
|
217
|
-
Pi supports the llama.cpp router server (Pi 0.81.0): configure with `/login llama.cpp`, search/download Hugging Face models and load/unload with live progress via `/llama`, then select loaded models in `/model`. The model catalog persists across restarts (fixed in Pi 0.82.1). See [Pi `docs/llama-cpp.md`](https://github.com/earendil-works/pi/blob/
|
|
224
|
+
Pi supports the llama.cpp router server (Pi 0.81.0): configure with `/login llama.cpp`, search/download Hugging Face models and load/unload with live progress via `/llama`, then select loaded models in `/model`. The model catalog persists across restarts (fixed in Pi 0.82.1). Every llama.cpp chat model is also listed as a classifier model (see [Classifier Models](#classifier-models)). `"extensions": ["-builtin:llama.cpp"]` removes the provider and `/llama`. See [Pi `docs/llama-cpp.md`](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/llama-cpp.md).
|
|
225
|
+
|
|
226
|
+
## Model Types
|
|
227
|
+
|
|
228
|
+
Since Pi 0.99.0 catalogs hold typed models: `type` is `"chat"` (the default when omitted), `"image"`, or `"classifier"`. Chat-facing reads (`getModels()`, `getAvailable()`, `ctx.modelRegistry.getAll()`, `/model`) stay chat-only. `ModelRuntime`, `ctx.modelRegistry`, and pi-ai `Models` all have `getModelsOfType(type, provider?)`, `getModelOfType(type, provider, id)`, and async `getAvailableOfType(type, provider?)`. Only `ModelRuntime` and `Models` have `getAllModels()` and `getAllAvailable()` for every type; `ctx.modelRegistry` adds `findOfType(type, provider, id)` instead. pi-ai exports `isModelType()` and `getModelType()` for mixed lists. One upstream ID may have separate chat and image entries. Sources: [models.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/models.md#use-classifier-models), [pi-ai types](https://github.com/earendil-works/pi/blob/v1.0.0/packages/ai/src/types.ts).
|
|
229
|
+
|
|
230
|
+
### Classifier Models
|
|
231
|
+
|
|
232
|
+
Classifiers do not chat. They answer typed questions about JSON state with probabilities, and do not appear in `/model`.
|
|
233
|
+
|
|
234
|
+
| Provider | Model IDs | Auth |
|
|
235
|
+
|----------|-----------|------|
|
|
236
|
+
| `typesafe` | `jev-latest` | `TYPESAFE_API_KEY` |
|
|
237
|
+
| `openrouter` | `typesafe/jev-1.13`, `~typesafe/jev-latest` | `OPENROUTER_API_KEY` or `/login` |
|
|
238
|
+
| `cloudflare-workers-ai` | `typesafe/jev` | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` |
|
|
239
|
+
| `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
|
|
240
|
+
| `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
|
|
241
|
+
| llama.cpp router | every loaded chat model ID | `/login llama.cpp` |
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const jev = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
|
|
245
|
+
if (jev) {
|
|
246
|
+
const result = await ctx.modelRegistry.classify(
|
|
247
|
+
jev,
|
|
248
|
+
{
|
|
249
|
+
state: { message: "The change works, thanks." },
|
|
250
|
+
questions: {
|
|
251
|
+
approved: {
|
|
252
|
+
type: "bool",
|
|
253
|
+
instructions: "Does the user approve of the result?",
|
|
254
|
+
criteria: { true: "Approval", false: "No approval" },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{ signal: ctx.signal },
|
|
259
|
+
);
|
|
260
|
+
const answer = result.stopReason === "stop" ? result.answers.approved : undefined;
|
|
261
|
+
if (answer?.type === "bool") console.log(answer.probability);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- Question types: `choice` (`criteria` maps labels to descriptions; answer has `choice`, `probabilities`, `confidence`), `bool` (`criteria: { true, false }`; answer has `probability`), and `score` (`criteria` is an ordered list; answer has `score`, `confidence`). Pi translates `bool` to TypeSafe's wire format.
|
|
266
|
+
- `classify()` never rejects. Failures and aborts return `stopReason: "error"` or `"aborted"` with `errorMessage`. Check `stopReason` and each answer's `type` before use, and validate probabilities yourself if a decision depends on them (for example finite and within `[0, 1]`).
|
|
267
|
+
- `result.usage` carries token counts and catalog cost when the service reports them; TypeSafe's direct `jev-latest` has no catalog price.
|
|
268
|
+
- `jev-latest` moves with the service. Pin a versioned ID such as `openrouter`'s `typesafe/jev-1.13` when thresholds were tuned against one model.
|
|
269
|
+
- Codemode scripts reach classifiers through `models.classify()` (see `references/codemode.md`). The SDK equivalent is `modelRuntime.classify()`. A llama.cpp classifier accepts a per-request `temperature` to soften overconfident label probabilities.
|
|
270
|
+
|
|
271
|
+
A gate that fails closed on classifier errors is in `examples/classifier-gate.ts`.
|
|
272
|
+
|
|
273
|
+
### Image Generation
|
|
274
|
+
|
|
275
|
+
`ModelRuntime.generateImages(model, { input }, options?)` (Pi 0.99.0) generates images with runtime-resolved auth; list image models with `getModelsOfType("image")`. OpenRouter image models such as `google/gemini-2.5-flash-image` appear under the `openrouter` provider with its credential. Like `classify()`, it returns an error result rather than rejecting. Image models do not appear in `/model`: extensions call `ctx.modelRegistry.generateImages()` and codemode scripts call `models.generateImages()` (both Pi 1.0.0). Codemode adds a script's image usage to its result; an extension tool must report the returned `usage` on its own tool result, combined across calls. `input` takes text blocks plus `{ type: "image", data, mimeType }` blocks to edit or use as references; output images are base64 blocks and are not saved to disk.
|
|
276
|
+
|
|
277
|
+
In pi-ai, image models live in the same catalog as chat models: `builtinModels()` includes them, `models.getModelOfType("image", provider, id)` finds one (type `ImageModel`), `models.generateImages()` runs it, and `createProvider({ models, images })` registers image implementations. The separate `*Images*` API was removed in pi-ai 0.99.0.
|
|
278
|
+
|
|
279
|
+
## Virtual Models
|
|
280
|
+
|
|
281
|
+
A virtual model (experimental, Pi 0.99.0) is a selectable entry that routes each request to a physical model. It is for routing by task, cost, or conversation state, not for permission decisions. Source: [virtual-models.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/virtual-models.md), [`jev-router.ts`](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/examples/extensions/jev-router.ts).
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
pi.registerVirtualModel({
|
|
285
|
+
provider: "router",
|
|
286
|
+
id: "auto",
|
|
287
|
+
name: "Auto",
|
|
288
|
+
thinkingLevels: ["low", "high"],
|
|
289
|
+
route(request, ctx) {
|
|
290
|
+
const sticky = request.failed ?? request.previous;
|
|
291
|
+
if (request.reason !== "user" && sticky) {
|
|
292
|
+
return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? "medium" };
|
|
293
|
+
}
|
|
294
|
+
const id = request.thinkingLevel === "high" ? "claude-sonnet-4-5" : "claude-haiku-4-5";
|
|
295
|
+
const model = ctx.modelRegistry.find("anthropic", id);
|
|
296
|
+
if (!model) throw new Error(`anthropic/${id} is not in the catalog`);
|
|
297
|
+
return { model, thinkingLevel: "medium" };
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- `route()` runs before every request with `reason` (`user`, `continuation`, `retry`, `direct`), `previous`, `failed`, `state`, `messages`, and `signal`. Returning `previous` for continuations and `failed` for retries keeps prompt caches and thinking signatures valid.
|
|
303
|
+
- Returned `state` (JSON) is stored on the session branch and comes back as `request.state`; `direct` requests (compaction summaries, `ctx.modelRegistry.streamSimple()`) have none.
|
|
304
|
+
- The route must be a physical model with credentials; throwing or returning a virtual model ends the request with an error.
|
|
305
|
+
- Selection (`ctx.model`, `model_change`) names the virtual model; each assistant message records the physical `provider`/`model`/`thinkingLevel`. The footer shows both; `/session` lists cost per physical model.
|
|
306
|
+
- `pi.unregisterVirtualModel(provider, id)` removes one (`unregisterProvider()` does not). SDK code can call `modelRuntime.registerVirtualModel(definition)`.
|
|
218
307
|
|
|
219
308
|
## Custom Providers via Extensions
|
|
220
309
|
|
|
221
310
|
Use extensions when you need custom streaming, custom headers, OAuth/device-flow logic, or want to override an existing provider's `baseUrl`/`headers`:
|
|
222
311
|
|
|
312
|
+
A config-form `models` list replaces the provider's models across chat, image, and classifier operations; image and classifier entries need an explicit `type` and implementations keyed by `api` through `images` and `classifiers`. Registering only `baseUrl` or `headers` keeps the built-in models. See [custom-provider.md](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/custom-provider.md).
|
|
313
|
+
|
|
223
314
|
```typescript
|
|
224
315
|
// Full registration
|
|
225
316
|
pi.registerProvider("my-provider", {
|