Add Paseo Agent provider

This commit is contained in:
Mohamed Boudra
2026-07-02 08:38:27 +02:00
parent 2263469342
commit c9cd414571
37 changed files with 3864 additions and 5 deletions
+186
View File
@@ -0,0 +1,186 @@
# Paseo Agent provider
Paseo Agent is a built-in provider that runs Pi's coding-agent harness **in process** (no `pi` CLI, no `~/.pi` discovery). It is configured entirely by Paseo-owned config under `agents.paseo` in `$PASEO_HOME/config.json`. The model backends are "inference providers": one or more typed entries, each pointing at an API with its own key and models.
The provider id is **`paseo`** (the display name is "Paseo Agent"). Use it like any other provider, e.g. `paseo run --provider paseo --model <inferenceProviderName>/<modelId> ...`.
This is a prototype. There is no UI yet — config-file only.
> Smoke note: the daemon supervisor runs from `packages/server/dist`. After changing
> provider/config code, run `npm run build:server` (or run a source/dev daemon) before
> smoking, otherwise a stale `dist` may reject the `agents.paseo` config. Always pass
> `--host <addr>` to CLI smoke commands so they hit your isolated daemon, not the real
> daemon on `:6767`.
## MCP tools
Paseo Agent bridges `AgentSessionConfig.mcpServers` into Pi custom tools, so the
daemon-injected `paseo` MCP server (and any other configured MCP server) is available to
the model. On session start the provider connects to each server, lists its tools, and
registers them as Pi tools named `<serverName>__<toolName>`; tool input schemas (JSON
Schema) are converted to TypeBox, calls are proxied to the MCP server, and results map
back to the model. Connections are torn down on session close. Servers that fail to
connect or list are logged and skipped rather than failing the session.
Transports: HTTP (streamable) is the primary path (the injected `paseo` server is HTTP);
SSE and stdio transports are also wired via the MCP SDK. No extra config is needed — MCP
servers come from Paseo's normal injection/config, not from `agents.paseo`.
## Config shape
```jsonc
{
"agents": {
"paseo": {
// Optional. "<inferenceProviderName>/<modelId>". Used when an agent is
// started without an explicit model.
"defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet",
// Inference providers, keyed by instance name. Names are free-form; you may
// run several entries of the same type against different APIs/keys/models.
"providers": {
"openrouter-main": {
"type": "openrouter",
"options": {
// apiKey may be omitted to fall back to the type's env var
// (here OPENROUTER_API_KEY). It may also be a literal, an env
// reference like "$OPENROUTER_API_KEY" / "${OPENROUTER_API_KEY}",
// or a "!command" that prints the key.
"models": [
{ "id": "anthropic/claude-3.7-sonnet", "label": "Claude 3.7", "reasoning": true },
{ "id": "openai/gpt-4o", "label": "GPT-4o" },
],
},
},
},
},
},
}
```
## Supported types
Each type supplies sensible defaults so you usually only provide an API key (or its
env var) and model ids.
| type | wire `api` | default base URL | default key env var |
| ------------------- | --------------------------- | --------------------------------- | --------------------- |
| `openrouter` | `openai-completions` | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` |
| `openai` | `openai-responses` | `https://api.openai.com/v1` | `OPENAI_API_KEY` |
| `anthropic` | `anthropic-messages` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` |
| `opencode` | `openai-completions` | `https://opencode.ai/zen/v1` | `OPENCODE_API_KEY` |
| `openai-compatible` | `openai-completions` | _(required: `options.baseUrl`)_ | _(none — set apiKey)_ |
| `openai-codex` | `openai-codex-responses` | `https://chatgpt.com/backend-api` | _(OAuth — see below)_ |
| `custom` | _(required: `options.api`)_ | _(required: `options.baseUrl`)_ | _(none — set apiKey)_ |
Per-entry overrides live in `options`: `baseUrl`, `api`, `apiKey`, `headers`,
`authHeader` (send `Authorization: Bearer <apiKey>`), and `models[]`. Each model may
override `api` (e.g. an `anthropic-messages` model behind an otherwise
`openai-completions` provider), plus `label`, `reasoning`, `contextWindow`, `maxTokens`.
### OpenCode Zen / Go (OpenAI-compatible)
OpenCode Zen models speak either `openai-completions` or `anthropic-messages`. Use the
`opencode` type for Zen, or `openai-compatible` with an explicit base URL for Go, and
override the per-model `api` where a model is Anthropic-family:
```jsonc
{
"type": "opencode",
"options": {
"models": [
{ "id": "big-pickle" },
{ "id": "claude-sonnet", "api": "anthropic-messages" }
]
}
}
// OpenCode Go:
{
"type": "openai-compatible",
"options": {
"baseUrl": "https://opencode.ai/zen/go/v1",
"apiKey": "$OPENCODE_API_KEY",
"models": [{ "id": "glm-5" }]
}
}
```
Pi attaches its own `x-opencode-*` attribution headers automatically when the base URL
is on `opencode.ai`, so you do not set those yourself.
### `custom` escape hatch
When a backend needs a wire protocol the named types don't cover, use `custom` and set
`options.api` to a Pi wire protocol (e.g. `google-generative-ai`, `mistral-conversations`,
`openai-codex-responses`) plus `options.baseUrl`. This is a thin pass-through, not a place
to embed raw Pi internals.
## Authentication
- **API key / env var / command** — works for every type. Omit `apiKey` to use the
type's default env var; or set a literal, a `$ENV` reference, or a `!command`.
- A provider only counts as "available" (and its models listable for use) when its
resolved key is actually present — a literal value, a set env var, or a command.
### ChatGPT / OpenAI subscription (OAuth) — `openai-codex`
The `openai-codex` type uses a ChatGPT/OpenAI **subscription** via OAuth instead of an
API key, against `https://chatgpt.com/backend-api` with the `openai-codex-responses` wire
API. Paseo **owns** the auth: `paseo login chatgpt` runs Pi's browser PKCE/callback
OAuth flow by default, stores the credential in a Paseo-controlled file, and lets Pi
refresh/rotate it there. Paseo does **not** read ChatGPT/Codex/OpenCode/Pi or any other
tool's auth files.
Config — just declare the provider and its models (no credential field):
```jsonc
{
"agents": {
"paseo": {
"providers": {
"chatgpt": {
"type": "openai-codex",
"options": { "models": [{ "id": "gpt-5.3-codex", "reasoning": true }] },
},
},
},
},
}
```
> Use a model id that ChatGPT-account Codex supports — e.g. `gpt-5.3-codex` (live-verified),
> or another Pi codex id like `gpt-5.2`, `gpt-5.4`, `gpt-5.4-mini`. The non-subscription id
> `gpt-5-codex` is **not** accepted on a ChatGPT account (the backend returns a 400
> "model is not supported when using Codex with a ChatGPT account").
Then log in once (the credential is stored under the `chatgpt` provider instance):
```bash
paseo login chatgpt
# Opens your browser to approve (OAuth PKCE + local callback on 127.0.0.1:1455).
# If the browser can't open, the URL is printed to copy; you can also paste the code.
```
Headless machines (no browser) can use the device-code fallback:
```bash
paseo login chatgpt --device-code # prints a URL + code to enter on another device
```
Both store the credential at `$PASEO_HOME/paseo-agent/auth.json` (mode `0600`, created by
Pi's AuthStorage; pass `--home` to target a specific Paseo home). On each session the
provider loads that credential, and Pi refreshes expired access tokens — persisting any
rotated refresh token **back into Paseo's own file**. A codex provider counts as
"available" once a credential is stored. Token values are never logged or printed.
> **Rotation is handled within Paseo.** Because Paseo owns the store and Pi writes
> refreshed tokens back to it, rotation does not break Paseo. Run `paseo login chatgpt`
> again any time to re-authorize (e.g. after a long idle period or an explicit logout).
**Advanced / manual override (not the normal path).** If you already hold your own refresh
token you may set `options.refreshToken` to a literal, `"$ENV"`/`"${ENV}"`, or `"!command"`.
It is seeded into the Paseo store at session start. This is for power users/automation; the
normal path is `paseo login chatgpt`. Paseo still never reads another tool's auth files.
Other OAuth providers (Anthropic Pro/Max, Copilot) remain unwired; for those you can pass a
pre-obtained bearer token via `apiKey`/env where accepted (e.g. `ANTHROPIC_OAUTH_TOKEN`).