Name the layers: agent providers and model providers

Record the naming decision in the glossary: bare "provider" splits into
agent provider (the harness: Claude Code, Codex, Paseo Agent) and model
provider (a model API attached to Paseo Agent). Bare "Agent" is the
runnable identity — an agent provider today, a named agent definition
tomorrow — resolved through one CLI namespace (--agent). Agent session
stays the running instance.

Retire "inference provider" across docs, UI strings, and identifiers
(PaseoAgentModelProvider, paseoAgentModelProviders). Labels and internal
names only — wire and config keys are untouched.
This commit is contained in:
Mohamed Boudra
2026-07-02 08:39:06 +02:00
parent 341af162b0
commit 7a6d2d86d7
14 changed files with 84 additions and 88 deletions
+1 -1
View File
@@ -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 inference providers, auth, OAuth limitation |
| [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/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 |
+9 -4
View File
@@ -6,7 +6,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
- **Workspace** — One concrete `cwd` on one daemon, with git state; belongs to exactly one project. Its `id` is opaque workspace identity; its `cwd` is the filesystem directory. UI: "Workspace". Code: `WorkspaceDescriptorPayload` (`packages/protocol/src/messages.ts:2178`). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label.
- **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality (`deriveWorkspaceKind`, `packages/server/src/server/workspace-registry-model.ts:158`), not stored from a user choice. Code: `PersistedWorkspaceKind` (`packages/server/src/server/workspace-registry-model.ts:8`). Don't confuse with **Isolation** (the create-time intent).
- **Isolation** — Create-time choice for a new workspace: reuse the existing checkout (**Local**) or cut a dedicated git worktree (**New worktree**). A transient setup input, also remembered as a create-form preference; it is not a workspace property. UI: "Isolation" control on the New Workspace screen. Code: `isolation` (`"local" | "worktree"`), `useWorkspaceIsolation` (`packages/app/src/screens/new-workspace-screen.tsx`); persisted as `FormPreferences.isolation` (`packages/app/src/create-agent-preferences/preferences.ts`). Distinct from **Workspace kind**, which is the git-derived property the intent produces (Local → `local_checkout` or `directory` by git-ness; New worktree → `worktree`). On the wire it is the create request's `source.kind` (`directory | worktree`, `packages/protocol/src/messages.ts:1693`).
- **Agent** — See **Agent session**. UI still says "Agent" / "New Agent" in places, but moving toward **Agent session** as the canonical term. Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`). Forbidden: "Task", "Job", "Run".
- **Agent** — A runnable identity the user picks to start work: today that is an **Agent provider** (Claude Code, Codex, Paseo Agent); direction: also a named **Agent definition** (`reviewer`) that pins provider, model, prompt, and tools. One CLI namespace: `paseo run --agent <id>` resolves agent provider ids first, then agent definition ids. The running instance is an **Agent session** — "New Agent" starts a new agent session. Forbidden: "Task", "Job", "Run".
- **Daemon** — Local Paseo server process; identified by `serverId`. UI: "Daemon" (system contexts only). Code: `serverId` in `ServerInfoStatusPayloadSchema` (`packages/protocol/src/messages.ts:1936`), `DaemonClient` (`packages/client/src/daemon-client.ts`).
- **Host** — Client-side connection profile pointing at a daemon; bundles one or more `HostConnection`s. UI: "Host" / "Add host" / "Switch host". Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Forbidden: "Connection" (means `HostConnection`, not host).
- **Project host entry** — One row in a project for a single (project, daemon) pair, aggregating that daemon's workspaces in the project. Internal. Code: `ProjectHostEntry` (`packages/app/src/utils/projects.ts:11`). Don't introduce "Checkout" as a synonym.
@@ -17,11 +17,13 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
- **Directory-backed surface** — A right-sidebar surface whose content is determined by the workspace's `cwd`, so two workspaces on the same directory see identical content: git diff/status, GitHub PR info, file preview/explorer contents. Keyed by `(serverId, cwd)`, never `workspaceId`. See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned).
- **Workspace-owned state** — Per-workspace state that never leaks to a same-`cwd` sibling: tabs, agents, terminals, panes, title, plus review drafts, diff-mode overrides, composer attachments, and file-explorer open/expand state. Keyed by `workspaceId` (`cwd` only as a fallback for old payloads). See [architecture.md](architecture.md#right-sidebar-boundary-directory-backed-vs-workspace-owned).
- **Workspace status bucket** — Aggregate activity signal for a workspace row. Same-`cwd` workspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped by `workspaceId`.
- **Agent session** — One running instance of an agent inside a workspace (one provider, one model, one cwd, one timeline). The conceptual unit; in the UI this opens as a tab. Moving toward this as the canonical term over "Agent". Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`).
- **Agent session** — One running instance of an agent inside a workspace (one agent provider, one model, one cwd, one timeline). The conceptual unit; in the UI this opens as a tab. Canonical term for the running instance (bare "Agent" is the identity being run, see **Agent**). Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`).
- **Agent definition** — A Paseo-owned markdown persona in `$PASEO_HOME/agents/*.md`: frontmatter `name`, `prompt`, `model`, `tools`, `permissions`; body is the prompt. Selected via `agents.paseo.defaultAgent` (`defaultProfile` is a legacy alias). Direction: definitions join the `--agent` namespace (`paseo run --agent reviewer`), so a definition id must not shadow an agent provider id. Code: `loadAgentDefinition` (`packages/server/src/server/agent/providers/paseo-agent/prompt-profiles.ts:62`), config keys (`packages/server/src/server/agent/providers/paseo-agent/config.ts:174`). Forbidden: "Profile", "Prompt profile" as synonyms.
- **Session** — Two senses: (a) per-client connection to a daemon, internal; (b) user-facing agent session, see **Agent session**. Code: `Session` (`packages/server/src/server/session.ts`) for (a). Don't confuse with: provider-side agent session log.
- **Profile** — Internal name for the persisted shape of a host. Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Never user-facing.
- **Provider** — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi, OMP). UI: "Provider". Code: `ProviderSnapshotEntry` (`packages/protocol/src/messages.ts:198`).
- **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`).
- **Agent provider** — The harness/backend that runs agents (Claude Code, Codex, Copilot, OpenCode, Pi, OMP, Paseo Agent with id `paseo`). UI: "Agent provider" (target label; surfaces still saying bare "Provider" migrate as touched). Wire and config keep `provider` — labels move, the protocol does not. Code: `AgentProvider` (`packages/protocol/src/agent-types.ts:3`), `ProviderSnapshotEntrySchema` (`packages/protocol/src/messages.ts:262`). Don't confuse with: **Model provider**. Forbidden: bare "Provider" as a new UI label — always qualify which layer.
- **Model provider** — A model API attached to the Paseo Agent provider: a type (`openrouter`, `openai`, `anthropic`, `openai-codex`, …) plus key/OAuth and models, keyed by instance name (`openrouter-main`, `chatgpt`). UI: "Model providers" (Paseo Agent settings). CLI: `paseo provider add` — the CLI `provider` noun means model provider. Config: `agents.paseo.providers` (`packages/server/src/server/agent/providers/paseo-agent/config.ts:169`); wire: `PaseoAgentProviderTypeSchema` (`packages/protocol/src/messages.ts:1924`). Don't confuse with: **Agent provider**. Forbidden: "Inference provider" (old docs term), bare "Provider" as a new UI label.
- **Model** — A specific LLM offered by a provider; for the Paseo Agent provider, addressed as `<modelProviderName>/<modelId>`. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`).
- **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`).
- **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`).
- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent).
@@ -39,5 +41,8 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
## Inconsistencies (documented, not papered over)
- Bare "Provider" survives as a UI label in the provider picker and settings; target is "Agent provider" / "Model providers" per the entries above. Migrate labels as surfaces are touched — no big-bang rename.
- CLI `paseo provider` describes itself as "Manage agent providers" (`packages/cli/src/commands/provider/index.ts:11`) but manages **model providers**. Fix the description; keep the noun.
- CLI `paseo run --provider <id>` selects an agent provider. Direction: add `--agent <id>` as the canonical flag resolving one namespace (agent provider ids, then agent definition ids), keep `--provider` as a COMPAT alias.
- CLI `--host <host>` description `"Daemon host target"` (`packages/cli/src/utils/command-options.ts:5`) blurs daemon/host; the app keeps them distinct.
- `WorkspaceDescriptorPayloadSchema.workspaceKind` accepts legacy `"checkout"` on the wire (`packages/protocol/src/messages.ts:2187`) while `PersistedWorkspaceKind` does not (`packages/server/src/server/workspace-registry-model.ts:8`).
+4 -4
View File
@@ -1,8 +1,8 @@
# Paseo Agent provider
Paseo Agent is a built-in provider that runs Pi's coding-agent harness **in process** (no `pi` CLI, no `~/.pi` discovery). It is configured entirely by Paseo-owned config under `agents.paseo` in `$PASEO_HOME/config.json`. The model backends are "inference providers": one or more typed entries, each pointing at an API with its own key and models.
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.
The provider id is **`paseo`** (the display name is "Paseo Agent"). Use it like any other provider, e.g. `paseo run --provider paseo --model <inferenceProviderName>/<modelId> ...`.
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 <modelProviderName>/<modelId> ...`.
This is a prototype. There is no app UI yet. OpenRouter and ChatGPT setup have CLI
paths; other provider setup is still config-file based.
@@ -102,7 +102,7 @@ is not present in the session's `mcpServers`, Paseo Agent logs a warning and con
{
"agents": {
"paseo": {
// Optional. "<inferenceProviderName>/<modelId>". Used when an agent is
// Optional. "<modelProviderName>/<modelId>". Used when an agent is
// started without an explicit model.
"defaultModel": "openrouter-main/anthropic/claude-3.7-sonnet",
@@ -113,7 +113,7 @@ is not present in the session's `mcpServers`, Paseo Agent logs a warning and con
// Legacy alias for defaultAgent. Still accepted for old configs.
"defaultProfile": "orchestrator",
// Inference providers, keyed by instance name. Names are free-form; you may
// 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": {
+1 -1
View File
@@ -58,7 +58,7 @@ export async function addOpenRouterProvider(
await expect(page.getByText(provider.apiKey, { exact: true })).toHaveCount(0);
}
export async function expectInferenceProviderListed(
export async function expectModelProviderListed(
page: Page,
expected: ExpectedProvider,
): Promise<void> {
@@ -2,7 +2,7 @@ import { test } from "./fixtures";
import {
addOpenRouterProvider,
cleanupPaseoAgentProviders,
expectInferenceProviderListed,
expectModelProviderListed,
openPaseoAgentSettings,
seedChatGptProvider,
} from "./helpers/paseo-agent";
@@ -18,7 +18,7 @@ test.describe("Paseo Agent provider configuration", () => {
providerNamesToCleanup.clear();
});
test("adds an OpenRouter inference provider from Settings", async ({ page }) => {
test("adds an OpenRouter model provider from Settings", async ({ page }) => {
providerNamesToCleanup.add(OPENROUTER_PROVIDER);
await openPaseoAgentSettings(page);
@@ -28,7 +28,7 @@ test.describe("Paseo Agent provider configuration", () => {
models: ["openai/gpt-4o-mini", "anthropic/claude-3.7-sonnet"],
});
await expectInferenceProviderListed(page, {
await expectModelProviderListed(page, {
name: OPENROUTER_PROVIDER,
providerType: "openrouter",
modelCount: 2,
@@ -36,13 +36,13 @@ test.describe("Paseo Agent provider configuration", () => {
});
});
test("shows a stored ChatGPT login as a read-only inference provider row", async ({ page }) => {
test("shows a stored ChatGPT login as a read-only model provider row", async ({ page }) => {
providerNamesToCleanup.add(CHATGPT_PROVIDER);
await seedChatGptProvider(CHATGPT_PROVIDER);
await openPaseoAgentSettings(page);
await expectInferenceProviderListed(page, {
await expectModelProviderListed(page, {
name: CHATGPT_PROVIDER,
providerType: "openai-codex",
modelCount: 1,
@@ -246,7 +246,7 @@ export function PaseoAgentSettingsSheet({
} else if (providers.length === 0) {
body = (
<View style={styles.stateBox}>
<Text style={styles.stateText}>No inference providers configured yet.</Text>
<Text style={styles.stateText}>No model providers configured yet.</Text>
</View>
);
} else {
@@ -83,7 +83,7 @@ export interface BuildProviderRegistryOptions {
paseoHome?: string;
/**
* Opaque Paseo Agent config blob. The registry only forwards it to the
* paseo-agent client factory; it never reads the nested inference providers.
* paseo-agent client factory; it never reads the nested model providers.
*/
paseoAgentConfig?: PaseoAgentConfig;
}
@@ -163,7 +163,7 @@ describe("PaseoAgentClient", () => {
}
});
it("is available only when config has a usable inference provider", async () => {
it("is available only when config has a usable model provider", async () => {
const withConfig = new PaseoAgentClient({ logger: createTestLogger(), config: makeConfig() });
expect(await withConfig.isAvailable()).toBe(true);
@@ -30,7 +30,7 @@ import {
type PaseoAgentConfig,
listPaseoAgentModels,
paseoAgentHasUsableModel,
paseoAgentInferenceProviders,
paseoAgentModelProviders,
parsePaseoAgentModelId,
resolvePaseoAgentModel,
} from "./config.js";
@@ -534,22 +534,17 @@ export class PaseoAgentClient implements AgentClient {
config: AgentSessionConfig,
_launchContext?: AgentLaunchContext,
): Promise<AgentSession> {
const inferenceProviders = await paseoAgentInferenceProviders(this.config);
if (inferenceProviders.length === 0) {
const modelProviders = await paseoAgentModelProviders(this.config);
if (modelProviders.length === 0) {
throw new Error(
"Paseo Agent has no configured inference providers. Add agents.paseo.providers to your Paseo config.",
"Paseo Agent has no configured model providers. Add agents.paseo.providers to your Paseo config.",
);
}
const availableAgents = this.loadAvailableAgentModes();
const agent = this.loadSelectedAgent(config.modeId);
this.verifyExpectedMcpServers(agent, config.mcpServers);
const model = resolvePaseoAgentModel(
this.config,
config.model,
inferenceProviders,
agent?.model,
);
const model = resolvePaseoAgentModel(this.config, config.model, modelProviders, agent?.model);
const thinkingLevel = normalizeThinkingLevel(config.thinkingOptionId) ?? undefined;
const composedPrompt = composePromptParts({
agent,
@@ -567,7 +562,7 @@ export class PaseoAgentClient implements AgentClient {
// OAuth providers (ChatGPT/Codex) use a Paseo-owned, file-backed AuthStorage so Pi
// reads the stored credential and persists refreshed tokens (rotation) back to it.
const usesOAuth = inferenceProviders.some((provider) => provider.oauth);
const usesOAuth = modelProviders.some((provider) => provider.oauth);
const authStorage = usesOAuth
? createPaseoAgentAuthStorage(envForPaseoHome(this.paseoHome))
: undefined;
@@ -583,7 +578,7 @@ export class PaseoAgentClient implements AgentClient {
const handle = await createPaseoAgentSession({
cwd: config.cwd,
agentDir: resolveIsolatedAgentDir(),
inferenceProviders,
modelProviders,
...(model ? { model } : {}),
...(thinkingLevel ? { thinkingLevel } : {}),
...(authStorage ? { authStorage } : {}),
@@ -5,7 +5,7 @@ import {
encodePaseoAgentModelId,
listPaseoAgentModels,
paseoAgentHasUsableModel,
paseoAgentInferenceProviders,
paseoAgentModelProviders,
parsePaseoAgentModelId,
resolvePaseoAgentModel,
type PaseoAgentConfig,
@@ -34,7 +34,7 @@ describe("PaseoAgentConfigSchema", () => {
expect(() => PaseoAgentConfigSchema.parse({ providers: {}, unexpected: true })).toThrow();
});
it("rejects an unknown inference provider type", () => {
it("rejects an unknown model provider type", () => {
expect(() =>
PaseoAgentConfigSchema.parse({
providers: { p: { type: "mystery", options: { models: [{ id: "m" }] } } },
@@ -42,7 +42,7 @@ describe("PaseoAgentConfigSchema", () => {
).toThrow();
});
it("requires at least one model per inference provider", () => {
it("requires at least one model per model provider", () => {
expect(() =>
PaseoAgentConfigSchema.parse({
providers: { p: { type: "openrouter", options: { models: [] } } },
@@ -114,9 +114,9 @@ describe("listPaseoAgentModels", () => {
});
});
describe("paseoAgentInferenceProviders (per-type defaults)", () => {
describe("paseoAgentModelProviders (per-type defaults)", () => {
it("applies openrouter defaults (base url, api, model fields)", async () => {
const [provider] = await paseoAgentInferenceProviders(configWith());
const [provider] = await paseoAgentModelProviders(configWith());
expect(provider.name).toBe("openrouter-main");
expect(provider.config.baseUrl).toBe("https://openrouter.ai/api/v1");
expect(provider.config.apiKey).toBe("sk-test");
@@ -136,7 +136,7 @@ describe("paseoAgentInferenceProviders (per-type defaults)", () => {
anthropic: { type: "anthropic", options: { models: [{ id: "claude-x" }] } },
},
});
const [provider] = await paseoAgentInferenceProviders(config);
const [provider] = await paseoAgentModelProviders(config);
expect(provider.config.baseUrl).toBe("https://api.anthropic.com");
expect(provider.config.apiKey).toBe("$ANTHROPIC_API_KEY");
expect(provider.config.models?.[0]?.api).toBe("anthropic-messages");
@@ -155,7 +155,7 @@ describe("paseoAgentInferenceProviders (per-type defaults)", () => {
},
},
});
const [provider] = await paseoAgentInferenceProviders(config);
const [provider] = await paseoAgentModelProviders(config);
expect(provider.config.baseUrl).toBe("https://opencode.ai/zen/v1");
expect(provider.config.models?.[0]?.api).toBe("openai-completions");
expect(provider.config.models?.[1]?.api).toBe("anthropic-messages");
@@ -177,7 +177,7 @@ describe("paseoAgentInferenceProviders (per-type defaults)", () => {
},
},
});
const [provider] = await paseoAgentInferenceProviders(config);
const [provider] = await paseoAgentModelProviders(config);
expect(provider.config.api).toBe("google-generative-ai");
expect(provider.config.authHeader).toBe(true);
expect(provider.config.headers).toEqual({ "x-extra": "1" });
@@ -288,8 +288,8 @@ describe("openai-codex (ChatGPT subscription) provider", () => {
).toThrow();
});
it("maps to a codex inference provider with an oauth marker and no api key", async () => {
const [provider] = await paseoAgentInferenceProviders(codexConfig({}), {});
it("maps to a codex model provider with an oauth marker and no api key", async () => {
const [provider] = await paseoAgentModelProviders(codexConfig({}), {});
expect(provider.name).toBe("chatgpt");
expect(provider.oauth).toEqual({ kind: "openai-codex" });
expect(provider.config.apiKey).toBeUndefined();
@@ -300,7 +300,7 @@ describe("openai-codex (ChatGPT subscription) provider", () => {
it("carries an advanced self-supplied refresh token resolved from an env var", async () => {
const config = codexConfig({ refreshToken: "$CODEX_REFRESH_TOKEN" });
const [provider] = await paseoAgentInferenceProviders(config, {
const [provider] = await paseoAgentModelProviders(config, {
CODEX_REFRESH_TOKEN: "rt-env",
});
expect(provider.oauth).toEqual({ kind: "openai-codex", refreshToken: "rt-env" });
@@ -5,18 +5,18 @@ import {
isRefreshTokenExpressionConfigured,
resolveRefreshTokenExpression,
} from "./oauth-credentials.js";
import type { PaseoAgentInferenceProvider, PaseoAgentModelReference } from "./pi-services.js";
import type { PaseoAgentModelProvider, PaseoAgentModelReference } from "./pi-services.js";
import { findEnvReferences } from "./env-references.js";
export const PASEO_AGENT_PROVIDER = "paseo";
// Dedicated Paseo-owned config for the Paseo Agent provider. This is the single
// schema for `agents.paseo`; inference-provider fields are intentionally NOT
// schema for `agents.paseo`; model-provider fields are intentionally NOT
// merged into the shared strict ProviderOverrideSchema, and only this provider
// (via the helpers below) consumes it. This module imports no Pi runtime code so
// it stays cheap to load from persisted-config parsing.
//
// Inference providers are typed by `type`. Each known type carries sensible
// Model providers are typed by `type`. Each known type carries sensible
// defaults (base URL, Pi wire `api`, and the env var its API key is read from),
// so a user only needs to give an `apiKey` (or the env var) and one or more model
// ids. `openai-compatible` covers any OpenAI Chat Completions endpoint (incl.
@@ -139,7 +139,7 @@ const PaseoAgentProviderOptionsSchema = z
})
.strict();
const PaseoAgentInferenceProviderSchema = z
const PaseoAgentModelProviderSchema = z
.object({
type: z.enum(PROVIDER_TYPES),
options: PaseoAgentProviderOptionsSchema,
@@ -151,14 +151,14 @@ const PaseoAgentInferenceProviderSchema = z
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options", "baseUrl"],
message: `Inference provider type "${entry.type}" requires options.baseUrl.`,
message: `Model provider type "${entry.type}" requires options.baseUrl.`,
});
}
if (!defaults.api && !entry.options.api) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["options", "api"],
message: `Inference provider type "${entry.type}" requires options.api.`,
message: `Model provider type "${entry.type}" requires options.api.`,
});
}
// `openai-codex` needs no credential field here: auth comes from the Paseo-owned
@@ -168,20 +168,20 @@ const PaseoAgentInferenceProviderSchema = z
export const PaseoAgentConfigSchema = z
.object({
// Optional default model as "<inferenceProviderName>/<modelId>".
// Optional default model as "<modelProviderName>/<modelId>".
defaultModel: z.string().min(1).optional(),
// Optional default agent definition from $PASEO_HOME/agents/<name>.md.
defaultAgent: z.string().min(1).optional(),
// Legacy alias for defaultAgent.
defaultProfile: z.string().min(1).optional(),
// Inference providers keyed by instance name. Multiple entries may share a
// Model providers keyed by instance name. Multiple entries may share a
// type while pointing at different APIs/base URLs/models.
providers: z.record(z.string(), PaseoAgentInferenceProviderSchema).optional(),
providers: z.record(z.string(), PaseoAgentModelProviderSchema).optional(),
})
.strict();
export type PaseoAgentConfig = z.infer<typeof PaseoAgentConfigSchema>;
type PaseoAgentInferenceProviderEntry = z.infer<typeof PaseoAgentInferenceProviderSchema>;
type PaseoAgentModelProviderEntry = z.infer<typeof PaseoAgentModelProviderSchema>;
const DEFAULT_CONTEXT_WINDOW = 128_000;
const DEFAULT_MAX_TOKENS = 16_384;
@@ -194,14 +194,12 @@ interface ResolvedProviderSettings {
authHeader?: boolean;
}
function entries(config: PaseoAgentConfig): [string, PaseoAgentInferenceProviderEntry][] {
function entries(config: PaseoAgentConfig): [string, PaseoAgentModelProviderEntry][] {
return Object.entries(config.providers ?? {});
}
/** Apply per-type defaults to a raw inference provider entry. */
function resolveProviderSettings(
entry: PaseoAgentInferenceProviderEntry,
): ResolvedProviderSettings {
/** Apply per-type defaults to a raw model provider entry. */
function resolveProviderSettings(entry: PaseoAgentModelProviderEntry): ResolvedProviderSettings {
const defaults = resolvePaseoAgentProviderTypeDefaults(entry.type);
const apiKey = entry.options.apiKey ?? (defaults.envVar ? `$${defaults.envVar}` : undefined);
return {
@@ -232,12 +230,12 @@ function isAuthConfigured(value: string | undefined, env: NodeJS.ProcessEnv): bo
return referencedVars.every((name) => Boolean(env[name]));
}
/** Encode the Paseo-facing model id from an inference provider + Pi model id. */
/** Encode the Paseo-facing model id from an model provider + Pi model id. */
export function encodePaseoAgentModelId(providerName: string, modelId: string): string {
return `${providerName}/${modelId}`;
}
/** Parse a Paseo-facing model id back into its inference provider + Pi model id. */
/** Parse a Paseo-facing model id back into its model provider + Pi model id. */
export function parsePaseoAgentModelId(modelId: string): PaseoAgentModelReference | null {
const slash = modelId.indexOf("/");
if (slash <= 0 || slash === modelId.length - 1) {
@@ -246,7 +244,7 @@ export function parsePaseoAgentModelId(modelId: string): PaseoAgentModelReferenc
return { provider: modelId.slice(0, slash), id: modelId.slice(slash + 1) };
}
function toPiModels(entry: PaseoAgentInferenceProviderEntry, settings: ResolvedProviderSettings) {
function toPiModels(entry: PaseoAgentModelProviderEntry, settings: ResolvedProviderSettings) {
return entry.options.models.map((model) => {
const api = model.api ?? settings.api;
return {
@@ -263,16 +261,16 @@ function toPiModels(entry: PaseoAgentInferenceProviderEntry, settings: ResolvedP
}
/**
* Map Paseo-owned config into the in-memory inference providers the Pi seam expects.
* Map Paseo-owned config into the in-memory model providers the Pi seam expects.
* `openai-codex` entries carry an oauth marker instead of an API key. Their credential
* normally lives in the Paseo-owned store (populated by `paseo login chatgpt`); an
* advanced `options.refreshToken` may supply the user's own token instead.
*/
export async function paseoAgentInferenceProviders(
export async function paseoAgentModelProviders(
config: PaseoAgentConfig,
env: NodeJS.ProcessEnv = process.env,
): Promise<PaseoAgentInferenceProvider[]> {
const providers: PaseoAgentInferenceProvider[] = [];
): Promise<PaseoAgentModelProvider[]> {
const providers: PaseoAgentModelProvider[] = [];
for (const [name, entry] of entries(config)) {
const settings = resolveProviderSettings(entry);
@@ -329,7 +327,7 @@ export function listPaseoAgentModels(config: PaseoAgentConfig): AgentModelDefini
}
/**
* An inference provider is usable when it has at least one model and auth is configured.
* An model provider is usable when it has at least one model and auth is configured.
* For API-key types that means a resolvable key (literal, set env var, or command). For
* `openai-codex`, auth comes from the Paseo-owned store (checked via `isOAuthAuthed`,
* keyed by provider instance name) or, as an advanced override, a resolvable
@@ -364,7 +362,7 @@ export function paseoAgentHasUsableModel(
export function resolvePaseoAgentModel(
config: PaseoAgentConfig,
requestedModelId: string | null | undefined,
registeredProviders: PaseoAgentInferenceProvider[] = paseoAgentModelInventory(config),
registeredProviders: PaseoAgentModelProvider[] = paseoAgentModelInventory(config),
agentDefaultModelId?: string | null,
): PaseoAgentModelReference | undefined {
if (requestedModelId) {
@@ -384,7 +382,7 @@ export function resolvePaseoAgentModel(
return firstRegisteredModel(registeredProviders);
}
function paseoAgentModelInventory(config: PaseoAgentConfig): PaseoAgentInferenceProvider[] {
function paseoAgentModelInventory(config: PaseoAgentConfig): PaseoAgentModelProvider[] {
return entries(config).map(([name, entry]) => {
const settings = resolveProviderSettings(entry);
return { name, config: { models: toPiModels(entry, settings) } };
@@ -402,7 +400,7 @@ function firstModelId(config: PaseoAgentConfig): string | undefined {
}
function hasRegisteredModel(
providers: PaseoAgentInferenceProvider[],
providers: PaseoAgentModelProvider[],
model: PaseoAgentModelReference,
): boolean {
return providers.some(
@@ -413,7 +411,7 @@ function hasRegisteredModel(
}
function firstRegisteredModel(
providers: PaseoAgentInferenceProvider[],
providers: PaseoAgentModelProvider[],
): PaseoAgentModelReference | undefined {
for (const provider of providers) {
const first = provider.config.models?.[0];
@@ -8,11 +8,11 @@ import { createToolPermissionPolicy } from "./agent-permissions.js";
import {
type CreatePaseoAgentSessionOptions,
type PaseoAgentInferenceProvider,
type PaseoAgentModelProvider,
createPaseoAgentSession,
} from "./pi-services.js";
function codexInferenceProvider(): PaseoAgentInferenceProvider {
function codexModelProvider(): PaseoAgentModelProvider {
return {
name: "chatgpt",
config: {
@@ -46,7 +46,7 @@ function toolCallContext(toolName: string): BeforeToolCallContext {
} as BeforeToolCallContext;
}
function fakeInferenceProvider(): PaseoAgentInferenceProvider {
function fakeModelProvider(): PaseoAgentModelProvider {
return {
name: FAKE_PROVIDER,
config: {
@@ -101,12 +101,12 @@ describe("createPaseoAgentSession (no-discovery spike)", () => {
return {
cwd,
agentDir,
inferenceProviders: [fakeInferenceProvider()],
modelProviders: [fakeModelProvider()],
model: { provider: FAKE_PROVIDER, id: FAKE_MODEL_ID },
};
}
it("creates a session from an in-memory inference provider and selects its model", async () => {
it("creates a session from an in-memory model provider and selects its model", async () => {
const { session, modelRegistry } = await createPaseoAgentSession(baseOptions());
expect(session).toBeDefined();
@@ -160,11 +160,11 @@ describe("createPaseoAgentSession (no-discovery spike)", () => {
expect(existsSync(join(cwd, ".pi"))).toBe(false);
});
it("rejects a model that no inference provider registered", async () => {
it("rejects a model that no model provider registered", async () => {
await expect(
createPaseoAgentSession({
...baseOptions(),
inferenceProviders: [],
modelProviders: [],
}),
).rejects.toThrow(/not registered/);
});
@@ -261,14 +261,12 @@ describe("createPaseoAgentSession (no-discovery spike)", () => {
});
it("registers a codex provider and seeds the advanced refresh-token override", async () => {
const codex = codexInferenceProvider();
const codex = codexModelProvider();
const { session, modelRegistry } = await createPaseoAgentSession({
cwd,
agentDir,
model: { provider: "chatgpt", id: "gpt-5.3-codex" },
inferenceProviders: [
{ ...codex, oauth: { kind: "openai-codex", refreshToken: "rt-test-only" } },
],
modelProviders: [{ ...codex, oauth: { kind: "openai-codex", refreshToken: "rt-test-only" } }],
});
expect(session.model?.provider).toBe("chatgpt");
@@ -289,7 +287,7 @@ describe("createPaseoAgentSession (no-discovery spike)", () => {
authStorage,
model: { provider: "chatgpt", id: "gpt-5.3-codex" },
// No oauth.refreshToken marker — the credential comes from the Paseo store.
inferenceProviders: [{ ...codexInferenceProvider(), oauth: { kind: "openai-codex" } }],
modelProviders: [{ ...codexModelProvider(), oauth: { kind: "openai-codex" } }],
});
const available = modelRegistry.getAvailable();
@@ -36,7 +36,7 @@ export type PiProviderConfig = Parameters<ModelRegistry["registerProvider"]>[1];
type PiAuthData = Parameters<typeof AuthStorage.inMemory>[0];
type PiSettings = Parameters<typeof SettingsManager.inMemory>[0];
/** OAuth wiring for an inference provider (currently only ChatGPT/Codex). */
/** OAuth wiring for an model provider (currently only ChatGPT/Codex). */
export interface PaseoAgentOAuth {
kind: "openai-codex";
/**
@@ -47,7 +47,7 @@ export interface PaseoAgentOAuth {
refreshToken?: string;
}
export interface PaseoAgentInferenceProvider {
export interface PaseoAgentModelProvider {
/** Instance name, e.g. "openrouter-main". Used as the Pi provider key. */
name: string;
/** Typed Pi provider config: baseUrl, apiKey, models, api, etc. */
@@ -70,8 +70,8 @@ export interface CreatePaseoAgentSessionOptions {
* from or written to it during creation.
*/
agentDir: string;
/** Inference providers (model backends) registered entirely in memory. */
inferenceProviders: PaseoAgentInferenceProvider[];
/** Model providers (model backends) registered entirely in memory. */
modelProviders: PaseoAgentModelProvider[];
/** Explicit model selection. When omitted, Pi falls back to its own resolution. */
model?: PaseoAgentModelReference;
thinkingLevel?: ThinkingLevel;
@@ -187,7 +187,7 @@ export async function createPaseoAgentSession(
// Seed any oauth marker that carries a refresh token (the advanced/manual override).
// The product path leaves this empty — the credential is already in the Paseo store.
// Empty `access` + `expires: 0` forces a refresh on the first request.
for (const provider of options.inferenceProviders) {
for (const provider of options.modelProviders) {
if (provider.oauth?.kind === "openai-codex" && provider.oauth.refreshToken) {
authStorage.set(provider.name, {
type: "oauth",
@@ -200,7 +200,7 @@ export async function createPaseoAgentSession(
const modelRegistry = ModelRegistry.inMemory(authStorage);
for (const provider of options.inferenceProviders) {
for (const provider of options.modelProviders) {
const config =
provider.oauth?.kind === "openai-codex"
? { ...provider.config, oauth: openaiCodexOAuthProvider }
@@ -222,7 +222,7 @@ export async function createPaseoAgentSession(
: undefined;
if (options.model && !model) {
throw new Error(
`Paseo Agent: model ${options.model.provider}/${options.model.id} is not registered by any inference provider`,
`Paseo Agent: model ${options.model.provider}/${options.model.id} is not registered by any model provider`,
);
}
@@ -242,7 +242,7 @@ describe("PersistedConfigSchema agent provider runtime settings", () => {
});
});
test("accepts the dedicated agents.paseo inference provider config", () => {
test("accepts the dedicated agents.paseo model provider config", () => {
const parsed = PersistedConfigSchema.parse({
agents: {
paseo: {