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" }