From 2ba7562dff5bcb6a5ca74c8bfdb2a260cb5949ca Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 2 Jul 2026 12:06:02 +0200 Subject: [PATCH] fix(paseo-agent): use working OAuth catalog model Live proof showed the previous OAuth catalog default was rejected at session start. Switch the catalog, docs, fixtures, and regenerated schema to the working default while keeping provider config generic. --- CLAUDE.md | 2 +- docs/paseo-agent.md | 505 +++++--- packages/app/e2e/helpers/paseo-agent.ts | 2 +- .../src/messages.paseo-agent-config.test.ts | 2 +- .../agent/providers/paseo-agent/catalog.ts | 2 +- .../paseo-agent/config-service.test.ts | 2 +- .../providers/paseo-agent/config.test.ts | 8 +- .../public/schemas/paseo.config.v1.json | 1105 +++++++++-------- 8 files changed, 918 insertions(+), 710 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c92a4c546..38503a9e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ At the start of non-trivial work, list `docs/` and skim anything relevant to the | [docs/file-icons.md](docs/file-icons.md) | Material icon theme integration for the file explorer | | [docs/providers.md](docs/providers.md) | Adding a new agent provider end-to-end | | [docs/service-proxy.md](docs/service-proxy.md) | Service proxy: exposing workspace scripts at public URLs, DNS setup, reverse proxy config | -| [docs/paseo-agent.md](docs/paseo-agent.md) | Paseo Agent provider (id `paseo`): in-process Pi harness, `agents.paseo` typed model providers, auth, OAuth limitation | +| [docs/paseo-agent.md](docs/paseo-agent.md) | Paseo Agent provider (id `paseo`): model-provider catalog, generic config, OAuth store, CLI/app setup | | [docs/development.md](docs/development.md) | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP | | [docs/rpc-namespacing.md](docs/rpc-namespacing.md) | WebSocket RPC naming convention — dotted namespaces and `.request`/`.response` pairs | | [docs/terminal-performance.md](docs/terminal-performance.md) | Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage | diff --git a/docs/paseo-agent.md b/docs/paseo-agent.md index 36aabbd9a..7a82be788 100644 --- a/docs/paseo-agent.md +++ b/docs/paseo-agent.md @@ -1,19 +1,313 @@ # Paseo Agent provider -Paseo Agent is a built-in agent 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 **model providers** (see [glossary.md](glossary.md)): one or more typed entries, each pointing at an API with its own key and models. +Paseo Agent is the built-in agent provider that runs Pi's coding-agent harness **in +process** (no `pi` CLI, no `~/.pi` discovery). Its provider id is **`paseo`** and its +model backends are configured under `agents.paseo` in `$PASEO_HOME/config.json`. -The provider id is **`paseo`** (the display name is "Paseo Agent"). Use it like any other agent provider, e.g. `paseo run --provider paseo --model / ...`. +Use it like any other agent provider: -This is a prototype. The app Settings sheet can add OpenRouter model providers; -ChatGPT login and OpenRouter setup also have CLI paths. Other model provider -types are still config-file based. +```bash +paseo run --provider paseo --model / "Reply with pong" +``` + +Example: `--model openrouter/openai/gpt-4o-mini` selects the `openrouter` provider +instance and the `openai/gpt-4o-mini` model exposed by that instance. > 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 +> smoking, otherwise stale `dist` may reject or omit `agents.paseo` behavior. Always pass > `--host ` to CLI smoke commands so they hit your isolated daemon, not the real > daemon on `:6767`. +## Provider catalog + +Paseo Agent model providers are catalog-driven. `catalog.ts` is the source of truth for +which provider types the daemon knows how to configure. A catalog entry is data only: +id, label, wire API, base URL, optional headers, auth metadata, and optional default +models. The app picker, CLI setup, config RPCs, runtime provider resolution, and auth +state all use that same data. + +The current catalog contains four entries: + +| id | label | api | base URL | auth | +| ------------- | ---------------- | ------------------------ | --------------------------------- | ----------------------------- | +| `openrouter` | OpenRouter | `openai-completions` | `https://openrouter.ai/api/v1` | API key, `OPENROUTER_API_KEY` | +| `chatgpt` | ChatGPT | `openai-codex-responses` | `https://chatgpt.com/backend-api` | OAuth, `openai-codex` flow | +| `kimi` | Kimi Coding Plan | `anthropic-messages` | `https://api.kimi.com/coding` | API key, `KIMI_API_KEY` | +| `opencode-go` | OpenCode Go | `openai-completions` | `https://opencode.ai/zen/go/v1` | API key, `OPENCODE_API_KEY` | + +Only `chatgpt` ships with a default model (`gpt-5.4-mini`). API-key providers currently +require at least one explicit `--model` or `options.models[]` entry. + +## Config shape + +`agents.paseo.providers` is structurally generic. Provider instance names are free-form +keys; provider types are catalog ids. That means you can use the catalog id as the +default instance name, or create several instances of the same type with different +models, keys, or base URLs. + +```jsonc +{ + "agents": { + "paseo": { + "defaultAgent": "orchestrator", + "defaultModel": "openrouter-main/openai/gpt-4o-mini", + "providers": { + "openrouter-main": { + "type": "openrouter", + "options": { + "apiKey": "$OPENROUTER_API_KEY", + "models": [ + { "id": "openai/gpt-4o-mini", "label": "GPT-4o mini" }, + { "id": "anthropic/claude-3.7-sonnet", "reasoning": true }, + ], + }, + }, + "chatgpt": { + "type": "chatgpt", + "options": { + "models": [{ "id": "gpt-5.4-mini", "reasoning": true }], + }, + }, + "kimi": { + "type": "kimi", + "options": { + "apiKey": "$KIMI_API_KEY", + "models": [{ "id": "k2p7" }], + }, + }, + }, + }, + }, +} +``` + +The provider entry shape is: + +```jsonc +{ + "type": "openrouter", + "options": { + "apiKey": "$OPENROUTER_API_KEY", + "baseUrl": "https://...", + "api": "openai-completions", + "headers": { "X-Example": "value" }, + "authHeader": true, + "refreshToken": "$CHATGPT_REFRESH_TOKEN", + "models": [ + { + "id": "provider/model-id", + "label": "Readable label", + "api": "anthropic-messages", + "reasoning": true, + "contextWindow": 128000, + "maxTokens": 16384, + }, + ], + }, +} +``` + +Most options are overrides for the catalog entry: + +- `apiKey` may be omitted for API-key providers. Omitted means "use the catalog env var" + such as `OPENROUTER_API_KEY`. +- `apiKey` may also be a literal key, `$ENV`, `${ENV}`, or `!command` expression. Paseo + mirrors Pi's config-value semantics: literals and commands count as configured; env + references count only when every referenced env var is set in the daemon environment. +- `baseUrl`, `api`, `headers`, and `authHeader` override or extend catalog defaults. +- `models[]` exposes the model ids usable as `/`. A model may + override `api` when a single backend serves mixed protocols. +- `refreshToken` is an advanced OAuth seed path. Prefer the OAuth store described below. + +Env references make config portable: `config.json` can be copied between machines while +the secret stays in that machine's daemon environment or keychain command. + +`defaultModel` is optional and uses the same `/` form. An +explicit session model wins, then the selected agent definition's model, then +`agents.paseo.defaultModel`, then Pi's first available model. + +`defaultProfile` is still accepted as a legacy alias for `defaultAgent`. + +## Authentication + +API-key providers use the catalog auth metadata plus the configured `apiKey` expression. +Redacted provider responses include an optional `auth` state: + +- `Connected` means the key or credential is configured and at least one model is exposed. +- `Needs attention` means the instance exists but the auth expression does not currently + resolve, the OAuth store binding does not match, or another auth precondition is missing. +- `not configured` is used by older/no-auth responses. + +Secrets are not returned in catalog responses, redacted provider responses, or CLI table +output. + +### OAuth store + +OAuth credentials live in Paseo's store, not in another tool's auth file: + +```text +$PASEO_HOME/paseo-agent/auth.json +``` + +The store is created through Pi's `AuthStorage`. The parent directory is private and the +file is written mode `0600`; Pi also re-chmods on write. During a Paseo Agent session, +Pi reads the credential, refreshes expired access tokens, and persists refresh-token +rotation back into the same Paseo-owned file. + +Stored OAuth credentials are bound to the provider instance's `{ flow, baseUrl }`. If the +catalog flow or configured base URL changes, the old credential is left on disk but the +provider reports `Needs attention` until it is authorized again. This prevents a token +for one OAuth target from silently being used against another. + +The OAuth implementation uses Pi's OAuth registry. Catalog OAuth entries are limited to +flows that registry knows about: `openai-codex`, `anthropic`, and `github-copilot`. +The current catalog only uses `openai-codex` for `chatgpt`. + +## CLI setup + +The provider CLI talks to the selected daemon. Always pass `--host` when smoking an +isolated daemon. + +Actual help text: + +```text +Usage: paseo provider add [options] [id] + +Configure a Paseo Agent model provider + +Arguments: + id Catalog provider id; omit to choose interactively + +Options: + --name Provider instance name (default: provider id) + --model Model ID to expose (repeatable, comma-separated; + defaults to catalog models) (default: []) + --api-key-stdin Read API key from stdin + --device-code Use daemon-run device-code OAuth instead of browser + OAuth + --json Output in JSON format + --host Daemon host target: host:port or + tcp://host:port?ssl=true&password=secret (default: + local socket/pipe, then localhost:6767) + -h, --help display help for command +``` + +```text +Usage: paseo provider ls [options] + +List configured Paseo Agent model providers + +Options: + --json Output in JSON format + --host Daemon host target: host:port or + tcp://host:port?ssl=true&password=secret (default: local + socket/pipe, then localhost:6767) + -h, --help display help for command +``` + +```text +Usage: paseo provider rm [options] + +Remove a Paseo Agent model provider + +Arguments: + name Provider instance name + +Options: + --json Output in JSON format + --host Daemon host target: host:port or + tcp://host:port?ssl=true&password=secret (default: local + socket/pipe, then localhost:6767) + -h, --help display help for command +``` + +Examples: + +```bash +printf '%s\n' "$OPENROUTER_API_KEY" | + paseo provider add \ + openrouter \ + --api-key-stdin \ + --model openai/gpt-4o-mini \ + --host 127.0.0.1:7911 +``` + +```bash +paseo provider add kimi \ + --model k2p7 \ + --host 127.0.0.1:7911 +# Press Enter at the API-key prompt to store "$KIMI_API_KEY". +``` + +```bash +paseo provider add chatgpt --device-code --host 127.0.0.1:7911 +``` + +Without an `[id]`, `provider add` prints the catalog and prompts for a selection. For +API-key providers it writes the configured key expression into `config.json`. For OAuth +providers it first writes the provider config, then stores the OAuth credential in the +daemon's Paseo-owned auth store. + +`provider add` is idempotent for the same instance name: running it again updates the +existing entry instead of creating a duplicate. `provider rm ` removes only that +provider instance and clears `defaultModel` if it pointed at the removed instance. + +## App setup + +The app uses the same catalog and config RPCs as the CLI. In Settings, open the host +settings, then the Paseo Agent provider section. The app fetches the catalog from the +connected daemon, shows catalog entries in the picker, and renders either an API-key +form or an OAuth sign-in flow from the entry's `auth.kind`. + +Configured rows show the redacted provider state from the daemon: label, provider type, +models, availability, and auth state. The app gates this UI on +`server_info.features.paseoAgentCatalog`; older daemons show an update-host affordance +instead of trying to synthesize the feature through older RPCs. + +## Wire surface + +The catalog surface is gated by: + +```text +server_info.features.paseoAgentCatalog +``` + +RPC names use dotted namespaces: + +| request | response | +| --------------------------------------------------- | ---------------------------------------------------- | +| `config.paseo_agent.get_catalog.request` | `config.paseo_agent.get_catalog.response` | +| `config.paseo_agent.get_providers.request` | `config.paseo_agent.get_providers.response` | +| `config.paseo_agent.set_provider.request` | `config.paseo_agent.set_provider.response` | +| `config.paseo_agent.remove_provider.request` | `config.paseo_agent.remove_provider.response` | +| `config.paseo_agent.oauth.start.request` | `config.paseo_agent.oauth.start.response` | +| `config.paseo_agent.oauth.complete.request` | `config.paseo_agent.oauth.complete.response` | +| `config.paseo_agent.oauth.store_credential.request` | `config.paseo_agent.oauth.store_credential.response` | + +Protocol strings are intentionally open. Provider ids, auth kinds, OAuth flow names, and +wire API names are strings, not closed enums. Old clients can parse new catalog entries, +and new clients can show "update host" only when the daemon lacks the catalog feature. + +## Adding a catalog entry + +Adding a new model-provider type should be a data change: + +1. Add one entry to `PASEO_AGENT_PROVIDER_CATALOG` in + `packages/server/src/server/agent/providers/paseo-agent/catalog.ts`. +2. Set `id`, `label`, `api`, `baseUrl`, `auth`, and any provider-level `headers`. +3. Add default `models[]` only when there is a safe, broadly usable default. Otherwise + leave it empty so CLI/app users must choose a model id. +4. Use `auth: { kind: "api_key", envVar: "..." }` for key-backed providers. +5. Use `auth: { kind: "oauth", flow: "..." }` only for flows supported by Pi's OAuth + registry (`openai-codex`, `anthropic`, `github-copilot`). +6. Add or update focused tests around catalog copying, provider resolution, auth state, + and CLI/app rendering if the new entry exercises a new shape. + +Do not add provider-specific branches in the runtime, CLI, or app. The catalog entry is +what unlocks CLI setup, app setup, redacted provider state, config persistence, and model +addressing. + ## MCP tools Paseo Agent bridges `AgentSessionConfig.mcpServers` into Pi custom tools, so the @@ -25,7 +319,7 @@ back to the model. Connections are torn down on session close. Servers that fail 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 +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`. ## Agent definitions @@ -40,7 +334,7 @@ agents. Reusable partials can live anywhere under `$PASEO_HOME/agents`. "agents": { "paseo": { "defaultAgent": "orchestrator", - "defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet", + "defaultModel": "openrouter-main/openai/gpt-4o-mini", "providers": {}, }, }, @@ -55,7 +349,7 @@ name: Orchestrator description: Coordinates work through Paseo-managed agents prompt: extend mcp: [paseo] -model: openrouter-main/anthropic/claude-3.7-sonnet +model: openrouter-main/openai/gpt-4o-mini tools: [read, grep, paseo__list_agents, paseo__create_agent] permissions: - tool: paseo__archive_* @@ -96,196 +390,3 @@ tool preflight hook, so the policy applies to built-in, custom, and bridged MCP `mcp: [paseo]` is an expectation check, not a new injection mechanism. The normal daemon MCP injection still supplies the actual server; if an agent declares an MCP server that is not present in the session's `mcpServers`, Paseo Agent logs a warning and continues. - -## Config shape - -```jsonc -{ - "agents": { - "paseo": { - // Optional. "/". Used when an agent is - // started without an explicit model. - "defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet", - - // Optional. Loads $PASEO_HOME/agents/orchestrator.md by default for new - // Paseo Agent sessions. - "defaultAgent": "orchestrator", - - // Legacy alias for defaultAgent. Still accepted for old configs. - "defaultProfile": "orchestrator", - - // Model 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 `), 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`). - -## CLI setup - -Configure an OpenRouter provider through the selected daemon: - -```bash -export OPENROUTER_API_KEY=... -paseo provider add openrouter openrouter-main \ - --model anthropic/claude-3.7-sonnet \ - --host localhost:7777 -``` - -For shell-history-safe key entry, pipe the key instead: - -```bash -printf '%s\n' "$OPENROUTER_API_KEY" | - paseo provider add openrouter openrouter-main \ - --api-key-stdin \ - --model anthropic/claude-3.7-sonnet \ - --host localhost:7777 -``` - -`paseo login chatgpt --host ` runs browser OAuth on the CLI machine, then sends -the returned credential to the selected daemon. The credential is stored in that daemon's -`$PASEO_HOME/paseo-agent/auth.json`; token values are not printed. `--device-code` is -currently local-only and is rejected when combined with `--host` until a daemon-run -device-code RPC exists. diff --git a/packages/app/e2e/helpers/paseo-agent.ts b/packages/app/e2e/helpers/paseo-agent.ts index 1130fabe8..f3d36d663 100644 --- a/packages/app/e2e/helpers/paseo-agent.ts +++ b/packages/app/e2e/helpers/paseo-agent.ts @@ -101,7 +101,7 @@ export async function seedChatGptProvider(providerName: string): Promise { name: providerName, providerType: "chatgpt", options: { - models: [{ id: "gpt-5.3-codex", reasoning: true }], + models: [{ id: "gpt-5.4-mini", reasoning: true }], }, }); await client.storePaseoAgentOAuthCredential({ diff --git a/packages/protocol/src/messages.paseo-agent-config.test.ts b/packages/protocol/src/messages.paseo-agent-config.test.ts index 2890110e3..9c8e4f310 100644 --- a/packages/protocol/src/messages.paseo-agent-config.test.ts +++ b/packages/protocol/src/messages.paseo-agent-config.test.ts @@ -219,7 +219,7 @@ describe("Paseo Agent config RPC schemas", () => { name: "chatgpt", providerType: "openai-codex", options: { - models: [{ id: "gpt-5.3-codex", reasoning: true }], + models: [{ id: "gpt-5.4-mini", reasoning: true }], }, }); diff --git a/packages/server/src/server/agent/providers/paseo-agent/catalog.ts b/packages/server/src/server/agent/providers/paseo-agent/catalog.ts index f57222983..0ab02b4e3 100644 --- a/packages/server/src/server/agent/providers/paseo-agent/catalog.ts +++ b/packages/server/src/server/agent/providers/paseo-agent/catalog.ts @@ -55,7 +55,7 @@ export const PASEO_AGENT_PROVIDER_CATALOG = [ api: "openai-codex-responses", baseUrl: "https://chatgpt.com/backend-api", auth: { kind: "oauth", flow: "openai-codex" }, - models: [{ id: "gpt-5.3-codex", reasoning: true }], + models: [{ id: "gpt-5.4-mini", reasoning: true }], }, { id: "kimi", diff --git a/packages/server/src/server/agent/providers/paseo-agent/config-service.test.ts b/packages/server/src/server/agent/providers/paseo-agent/config-service.test.ts index 8e6ab0dab..f67c3d513 100644 --- a/packages/server/src/server/agent/providers/paseo-agent/config-service.test.ts +++ b/packages/server/src/server/agent/providers/paseo-agent/config-service.test.ts @@ -258,7 +258,7 @@ describe("PaseoAgentConfigService", () => { expect.objectContaining({ name: "chatgpt", providerType: "chatgpt", - models: [{ id: "gpt-5.3-codex", reasoning: true }], + models: [{ id: "gpt-5.4-mini", reasoning: true }], auth: { kind: "oauth", configured: true, source: "stored" }, available: true, }), diff --git a/packages/server/src/server/agent/providers/paseo-agent/config.test.ts b/packages/server/src/server/agent/providers/paseo-agent/config.test.ts index 103c7db97..006646b70 100644 --- a/packages/server/src/server/agent/providers/paseo-agent/config.test.ts +++ b/packages/server/src/server/agent/providers/paseo-agent/config.test.ts @@ -107,7 +107,7 @@ describe("listPaseoAgentModels", () => { const models = listPaseoAgentModels( PaseoAgentConfigSchema.parse({ providers: { chatgpt: { type: "chatgpt" } } }), ); - expect(models.map((m) => m.id)).toEqual(["chatgpt/gpt-5.3-codex"]); + expect(models.map((m) => m.id)).toEqual(["chatgpt/gpt-5.4-mini"]); }); it("marks the configured default model", () => { @@ -179,7 +179,7 @@ describe("paseoAgentModelProviders", () => { expect(provider.config.apiKey).toBeUndefined(); expect(provider.config.api).toBe("openai-codex-responses"); expect(provider.config.baseUrl).toBe("https://chatgpt.com/backend-api"); - expect(provider.config.models?.[0]?.id).toBe("gpt-5.3-codex"); + expect(provider.config.models?.[0]?.id).toBe("gpt-5.4-mini"); }); it("lets instance models override catalog default models", async () => { @@ -205,7 +205,7 @@ describe("paseoAgentModelProviders", () => { }), ); expect(provider.oauth).toEqual({ flow: "openai-codex" }); - expect(provider.config.models?.[0]?.id).toBe("gpt-5.3-codex"); + expect(provider.config.models?.[0]?.id).toBe("gpt-5.4-mini"); }); it("rejects unknown provider types at runtime with known ids", async () => { @@ -295,7 +295,7 @@ describe("resolvePaseoAgentModel", () => { PaseoAgentConfigSchema.parse({ providers: { chatgpt: { type: "chatgpt" } } }), null, ), - ).toEqual({ provider: "chatgpt", id: "gpt-5.3-codex" }); + ).toEqual({ provider: "chatgpt", id: "gpt-5.4-mini" }); }); it("ignores an implicit default whose provider is not registered", () => { diff --git a/packages/website/public/schemas/paseo.config.v1.json b/packages/website/public/schemas/paseo.config.v1.json index 0c6511926..1d7f9bcda 100644 --- a/packages/website/public/schemas/paseo.config.v1.json +++ b/packages/website/public/schemas/paseo.config.v1.json @@ -1,174 +1,206 @@ { - "$ref": "#/definitions/PaseoConfigV1", - "definitions": { - "PaseoConfigV1": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "$schema": { + "type": "string" + }, + "version": { + "type": "number", + "const": 1 + }, + "daemon": { "type": "object", "properties": { - "$schema": { + "listen": { "type": "string" }, - "version": { - "type": "number", - "const": 1 + "hostnames": { + "anyOf": [ + { + "type": "boolean", + "const": true + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] }, - "daemon": { + "allowedHosts": { + "anyOf": [ + { + "type": "boolean", + "const": true + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "trustedProxies": { + "anyOf": [ + { + "type": "boolean", + "const": true + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "mcp": { "type": "object", "properties": { + "enabled": { + "type": "boolean" + }, + "injectIntoAgents": { + "type": "boolean" + } + }, + "additionalProperties": {} + }, + "browserTools": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "additionalProperties": {} + }, + "autoArchiveAfterMerge": { + "type": "boolean" + }, + "enableTerminalAgentHooks": { + "type": "boolean" + }, + "appendSystemPrompt": { + "type": "string" + }, + "terminalProfiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "type": "string" + } + }, + "required": ["id", "name", "command"], + "additionalProperties": {} + } + }, + "cors": { + "type": "object", + "properties": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "relay": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "endpoint": { + "type": "string" + }, + "publicEndpoint": { + "type": "string" + }, + "useTls": { + "type": "boolean" + }, + "publicUseTls": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "serviceProxy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, "listen": { "type": "string" }, - "hostnames": { - "anyOf": [ - { - "type": "boolean", - "const": true - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "allowedHosts": { - "anyOf": [ - { - "type": "boolean", - "const": true - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "trustedProxies": { - "description": "Express trusted proxy setting for X-Forwarded-* headers. Defaults to [\"loopback\"]. Use true only when the final trusted proxy overwrites client-supplied forwarded headers.", - "anyOf": [ - { - "type": "boolean", - "const": true - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "mcp": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "injectIntoAgents": { - "type": "boolean" - } - }, - "additionalProperties": true - }, - "autoArchiveAfterMerge": { - "type": "boolean" - }, - "enableTerminalAgentHooks": { - "type": "boolean" - }, - "appendSystemPrompt": { - "type": "string" - }, - "cors": { - "type": "object", - "properties": { - "allowedOrigins": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "relay": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "endpoint": { - "type": "string" - }, - "publicEndpoint": { - "type": "string" - }, - "useTls": { - "type": "boolean" - }, - "publicUseTls": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "serviceProxy": { - "type": "object", - "properties": { - "enabled": { - "description": "Compatibility shim. false suppresses optional service proxy listen/public layers only; localhost proxying remains enabled.", - "type": "boolean" - }, - "listen": { - "description": "Optional service-only listener address. Presence enables the standalone service proxy listener.", - "type": "string" - }, - "publicBaseUrl": { - "description": "Optional public base URL for service host aliases.", - "type": "string", - "format": "uri" - } - }, - "additionalProperties": false - }, - "auth": { - "type": "object", - "properties": { - "password": { - "type": "string", - "pattern": "^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$" - } - }, - "additionalProperties": false + "publicBaseUrl": { + "type": "string", + "format": "uri" } }, "additionalProperties": false }, - "app": { + "auth": { "type": "object", "properties": { - "baseUrl": { - "type": "string" + "password": { + "type": "string", + "pattern": "^\\$2[aby]\\$\\d{2}\\$[./A-Za-z0-9]{53}$" } }, "additionalProperties": false - }, - "worktrees": { + } + }, + "additionalProperties": false + }, + "app": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string" + } + }, + "additionalProperties": false + }, + "providers": { + "type": "object", + "properties": { + "openai": { "type": "object", "properties": { - "root": { + "apiKey": { "type": "string", "minLength": 1 - } - }, - "additionalProperties": false - }, - "providers": { - "type": "object", - "properties": { - "openai": { + }, + "baseUrl": { + "type": "string", + "minLength": 1 + }, + "stt": { "type": "object", "properties": { "apiKey": { @@ -178,42 +210,18 @@ "baseUrl": { "type": "string", "minLength": 1 - }, - "stt": { - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "minLength": 1 - }, - "baseUrl": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "tts": { - "type": "object", - "properties": { - "apiKey": { - "type": "string", - "minLength": 1 - }, - "baseUrl": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false } }, "additionalProperties": false }, - "local": { + "tts": { "type": "object", "properties": { - "modelsDir": { + "apiKey": { + "type": "string", + "minLength": 1 + }, + "baseUrl": { "type": "string", "minLength": 1 } @@ -223,203 +231,338 @@ }, "additionalProperties": false }, - "agents": { + "local": { + "type": "object", + "properties": { + "modelsDir": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "worktrees": { + "type": "object", + "properties": { + "root": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "agents": { + "type": "object", + "properties": { + "providers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "extends": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "command": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "env": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "params": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "models": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "thinkingOptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + } + }, + "required": ["id", "label"] + } + } + }, + "required": ["id", "label"] + } + }, + "additionalModels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "thinkingOptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + } + }, + "required": ["id", "label"] + } + } + }, + "required": ["id", "label"] + } + }, + "disallowedTools": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "order": { + "type": "number" + } + } + } + }, + "metadataGeneration": { "type": "object", "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "minLength": 1 + }, + "model": { + "type": "string", + "minLength": 1 + }, + "thinkingOptionId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["provider"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "paseo": { + "type": "object", + "properties": { + "defaultModel": { + "type": "string", + "minLength": 1 + }, + "defaultAgent": { + "type": "string", + "minLength": 1 + }, + "defaultProfile": { + "type": "string", + "minLength": 1 + }, "providers": { "type": "object", + "propertyNames": { + "type": "string" + }, "additionalProperties": { "type": "object", "properties": { - "extends": { - "type": "string" + "type": { + "type": "string", + "minLength": 1 }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "command": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "minItems": 1 - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "models": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "label": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string" - }, - "isDefault": { - "type": "boolean" - }, - "thinkingOptions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "isDefault": { - "type": "boolean" - } - }, - "required": ["id", "label"], - "additionalProperties": false - } - } - }, - "required": ["id", "label"], - "additionalProperties": false - } - }, - "additionalModels": { - "type": "array", - "items": { - "$ref": "#/definitions/PaseoConfigV1/properties/agents/properties/providers/additionalProperties/properties/models/items" - } - }, - "disallowedTools": { - "type": "array", - "items": { - "type": "string" - } - }, - "enabled": { - "type": "boolean" - }, - "order": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "paseo": { - "type": "object", - "properties": { - "defaultModel": { - "type": "string", - "minLength": 1 - }, - "defaultProfile": { - "type": "string", - "minLength": 1 - }, - "providers": { - "type": "object", - "additionalProperties": { + "options": { + "default": {}, "type": "object", "properties": { - "type": { + "apiKey": { "type": "string", - "enum": [ - "openrouter", - "openai", - "anthropic", - "opencode", - "openai-compatible", - "openai-codex", - "custom" - ] + "minLength": 1 }, - "options": { + "baseUrl": { + "type": "string", + "format": "uri" + }, + "api": { + "type": "string", + "minLength": 1 + }, + "headers": { "type": "object", - "properties": { - "apiKey": { - "type": "string", - "minLength": 1 - }, - "baseUrl": { - "type": "string", - "format": "uri" - }, - "api": { - "type": "string", - "minLength": 1 - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "authHeader": { + "type": "boolean" + }, + "refreshToken": { + "type": "string", + "minLength": 1 + }, + "models": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "label": { + "type": "string", + "minLength": 1 + }, + "api": { + "type": "string", + "minLength": 1 + }, + "reasoning": { + "type": "boolean" + }, + "contextWindow": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxTokens": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 } }, - "authHeader": { - "type": "boolean" - }, - "refreshToken": { - "type": "string", - "minLength": 1 - }, - "models": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "label": { - "type": "string", - "minLength": 1 - }, - "api": { - "type": "string", - "minLength": 1 - }, - "reasoning": { - "type": "boolean" - }, - "contextWindow": { - "type": "integer", - "exclusiveMinimum": 0 - }, - "maxTokens": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["id"], - "additionalProperties": false - }, - "minItems": 1 - } - }, - "required": ["models"], - "additionalProperties": false + "required": ["id"], + "additionalProperties": false + } } }, - "required": ["type", "options"], "additionalProperties": false } + }, + "required": ["type"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "features": { + "type": "object", + "properties": { + "dictation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "stt": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "language": { + "type": "string", + "minLength": 1 + }, + "confidenceThreshold": { + "type": "number" } }, "additionalProperties": false @@ -427,134 +570,107 @@ }, "additionalProperties": false }, - "features": { + "voiceMode": { "type": "object", "properties": { - "dictation": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "stt": { - "type": "object", - "properties": { - "provider": { - "allOf": [ - { - "type": "string" - }, - { - "type": "string", - "enum": ["openai", "local"] - } - ] - }, - "model": { - "type": "string", - "minLength": 1 - }, - "language": { - "type": "string", - "minLength": 1 - }, - "confidenceThreshold": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "enabled": { + "type": "boolean" }, - "voiceMode": { + "llm": { "type": "object", "properties": { - "enabled": { - "type": "boolean" + "provider": { + "type": "string" }, - "llm": { - "type": "object", - "properties": { - "provider": { - "type": "string" - }, - "model": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "stt": { - "type": "object", - "properties": { - "provider": { - "$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider" - }, - "model": { - "type": "string", - "minLength": 1 - }, - "language": { - "type": "string", - "minLength": 1 - } - }, - "additionalProperties": false - }, - "turnDetection": { - "type": "object", - "properties": { - "provider": { - "$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider" - } - }, - "additionalProperties": false - }, - "tts": { - "type": "object", - "properties": { - "provider": { - "$ref": "#/definitions/PaseoConfigV1/properties/features/properties/dictation/properties/stt/properties/provider" - }, - "model": { - "type": "string", - "minLength": 1 - }, - "voice": { - "type": "string", - "enum": ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] - }, - "speakerId": { - "type": "integer" - }, - "speed": { - "type": "number" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "webUi": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "distDir": { + "model": { "type": "string", "minLength": 1 } }, "additionalProperties": false + }, + "stt": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "language": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "turnDetection": { + "type": "object", + "properties": { + "provider": { + "type": "string" + } + }, + "additionalProperties": false + }, + "tts": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "model": { + "type": "string", + "minLength": 1 + }, + "voice": { + "type": "string", + "enum": ["alloy", "echo", "fable", "onyx", "nova", "shimmer"] + }, + "speakerId": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "speed": { + "type": "number" + } + }, + "additionalProperties": false } }, "additionalProperties": false }, - "log": { + "webUi": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "distDir": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": ["trace", "debug", "info", "warn", "error", "fatal"] + }, + "format": { + "type": "string", + "enum": ["pretty", "json"] + }, + "console": { "type": "object", "properties": { "level": { @@ -564,42 +680,32 @@ "format": { "type": "string", "enum": ["pretty", "json"] + } + }, + "additionalProperties": false + }, + "file": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": ["trace", "debug", "info", "warn", "error", "fatal"] }, - "console": { + "path": { + "type": "string", + "minLength": 1 + }, + "rotate": { "type": "object", "properties": { - "level": { - "$ref": "#/definitions/PaseoConfigV1/properties/log/properties/level" - }, - "format": { - "$ref": "#/definitions/PaseoConfigV1/properties/log/properties/format" - } - }, - "additionalProperties": false - }, - "file": { - "type": "object", - "properties": { - "level": { - "$ref": "#/definitions/PaseoConfigV1/properties/log/properties/level" - }, - "path": { + "maxSize": { "type": "string", "minLength": 1 }, - "rotate": { - "type": "object", - "properties": { - "maxSize": { - "type": "string", - "minLength": 1 - }, - "maxFiles": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "additionalProperties": false + "maxFiles": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 } }, "additionalProperties": false @@ -611,5 +717,6 @@ "additionalProperties": false } }, - "$schema": "http://json-schema.org/draft-07/schema#" + "additionalProperties": false, + "title": "PaseoConfigV1" }