mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Open the model provider type wire schema
A closed z.enum inside the strict get_providers payload meant any provider type added daemon-side failed the entire WS envelope parse on older clients, silently dropping the response and timing out the settings sheet. Make providerType an open string on the wire; the daemon's config schema keeps the closed set and setProvider rejects unknown types with a clear error. Also correct the paseoAgentConfig COMPAT version, the provider CLI descriptions, and the stale no-app-UI doc claim.
This commit is contained in:
@@ -4,8 +4,9 @@ Paseo Agent is a built-in agent provider that runs Pi's coding-agent harness **i
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
> 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
|
||||
|
||||
@@ -8,7 +8,9 @@ import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
export function createProviderCommand(
|
||||
dependencies: Parameters<typeof runAddOpenRouterCommand>[3] = {},
|
||||
): Command {
|
||||
const provider = new Command("provider").description("Manage agent providers");
|
||||
const provider = new Command("provider").description(
|
||||
"Manage agent providers (ls, models) and Paseo Agent model providers (add)",
|
||||
);
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
provider.command("ls").description("List available providers and status"),
|
||||
@@ -22,7 +24,7 @@ export function createProviderCommand(
|
||||
.option("--thinking", "Include thinking option IDs for each model"),
|
||||
).action(withOutput(runModelsCommand));
|
||||
|
||||
const add = provider.command("add").description("Configure a provider");
|
||||
const add = provider.command("add").description("Configure a Paseo Agent model provider");
|
||||
addJsonAndDaemonHostOptions(addOpenRouterOptions(add.command("openrouter"))).action(
|
||||
withOutput<Awaited<ReturnType<typeof runAddOpenRouterCommand>>["data"], [string]>(
|
||||
(name, options, command) => runAddOpenRouterCommand(name, options, command, dependencies),
|
||||
|
||||
@@ -19,6 +19,29 @@ describe("Paseo Agent config RPC schemas", () => {
|
||||
expect(parsed.providerType).toBe("openrouter");
|
||||
});
|
||||
|
||||
test("parses a provider type this client has never heard of (new daemon, old client)", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "config.paseo_agent.get_providers.response",
|
||||
payload: {
|
||||
requestId: "req-get-future",
|
||||
defaultModel: null,
|
||||
providers: [
|
||||
{
|
||||
name: "kimi-main",
|
||||
providerType: "kimi-coding",
|
||||
models: [{ id: "kimi-k3" }],
|
||||
auth: { kind: "api_key", configured: true, source: "env" },
|
||||
available: true,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.payload.providers[0]?.providerType).toBe("kimi-coding");
|
||||
});
|
||||
|
||||
test("parses redacted provider responses without raw secret fields", () => {
|
||||
const parsed = SessionOutboundMessageSchema.parse({
|
||||
type: "config.paseo_agent.get_providers.response",
|
||||
|
||||
@@ -1922,15 +1922,11 @@ export const ListProviderFeaturesRequestMessageSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
const PaseoAgentProviderTypeSchema = z.enum([
|
||||
"openrouter",
|
||||
"openai",
|
||||
"anthropic",
|
||||
"opencode",
|
||||
"openai-compatible",
|
||||
"openai-codex",
|
||||
"custom",
|
||||
]);
|
||||
// Open on the wire on purpose: the daemon's paseo-agent config schema owns the
|
||||
// closed set of known types. A new type added daemon-side must not break an
|
||||
// older client's envelope parse (protocol contract: never narrow, old clients
|
||||
// keep parsing new daemons).
|
||||
const PaseoAgentProviderTypeSchema = z.string().min(1);
|
||||
|
||||
const PaseoAgentProviderModelConfigSchema = z
|
||||
.object({
|
||||
@@ -1978,15 +1974,7 @@ export const PaseoAgentProviderAuthStateSchema = z
|
||||
export const RedactedPaseoAgentProviderConfigSchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
providerType: z.enum([
|
||||
"openrouter",
|
||||
"openai",
|
||||
"anthropic",
|
||||
"opencode",
|
||||
"openai-compatible",
|
||||
"openai-codex",
|
||||
"custom",
|
||||
]),
|
||||
providerType: PaseoAgentProviderTypeSchema,
|
||||
baseUrl: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
models: z.array(PaseoAgentProviderModelConfigSchema),
|
||||
@@ -2468,7 +2456,7 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
daemonSelfUpdate: z.boolean().optional(),
|
||||
// COMPAT(agentForkContext): added in v0.1.102, remove gate after 2026-12-28.
|
||||
agentForkContext: z.boolean().optional(),
|
||||
// COMPAT(paseoAgentConfig): added in v0.1.85, remove gate after 2026-11-30.
|
||||
// COMPAT(paseoAgentConfig): added in v0.1.103, remove gate after 2027-01-02.
|
||||
paseoAgentConfig: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -55,6 +55,22 @@ describe("PaseoAgentConfigService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects an unknown provider type with a clear error and persists nothing", () => {
|
||||
const service = new PaseoAgentConfigService({
|
||||
paseoHome: home,
|
||||
logger: createTestLogger(),
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
service.setProvider({
|
||||
name: "future-main",
|
||||
providerType: "kimi-coding",
|
||||
options: { apiKey: "sk-test", models: [{ id: "kimi-k3" }] },
|
||||
}),
|
||||
).toThrow(/Unknown model provider type "kimi-coding". Known types: openrouter/);
|
||||
expect(loadPersistedConfig(home).agents?.paseo?.providers).toBeUndefined();
|
||||
});
|
||||
|
||||
test("preserves shared config fields when writing agents.paseo", () => {
|
||||
const logger = createTestLogger();
|
||||
savePersistedConfig(
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import {
|
||||
PaseoAgentConfigSchema,
|
||||
type PaseoAgentConfig,
|
||||
type PaseoAgentProviderType,
|
||||
isPaseoAgentProviderType,
|
||||
knownPaseoAgentProviderTypes,
|
||||
resolvePaseoAgentProviderTypeDefaults,
|
||||
} from "./config.js";
|
||||
import { hasStoredOAuthCredential, storeCodexOAuthCredential } from "./oauth-store.js";
|
||||
@@ -29,7 +30,7 @@ interface PaseoAgentConfigServiceOptions {
|
||||
|
||||
interface SetProviderInput {
|
||||
name: string;
|
||||
providerType: PaseoAgentProviderType;
|
||||
providerType: string;
|
||||
options: {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
@@ -163,6 +164,11 @@ export class PaseoAgentConfigService {
|
||||
}
|
||||
|
||||
setProvider(input: SetProviderInput): RedactedPaseoAgentProviderConfig {
|
||||
if (!isPaseoAgentProviderType(input.providerType)) {
|
||||
throw new Error(
|
||||
`Unknown model provider type "${input.providerType}". Known types: ${knownPaseoAgentProviderTypes().join(", ")}. Update the host if this type is newer than it.`,
|
||||
);
|
||||
}
|
||||
const next = this.updateConfig((current) =>
|
||||
PaseoAgentConfigSchema.parse({
|
||||
...current,
|
||||
|
||||
@@ -44,6 +44,16 @@ const PROVIDER_TYPES = [
|
||||
|
||||
export type PaseoAgentProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
|
||||
// The wire schema is an open string (protocol back-compat); the daemon owns
|
||||
// the closed set. Use this to gate incoming provider types with a clear error.
|
||||
export function isPaseoAgentProviderType(value: string): value is PaseoAgentProviderType {
|
||||
return (PROVIDER_TYPES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function knownPaseoAgentProviderTypes(): readonly string[] {
|
||||
return PROVIDER_TYPES;
|
||||
}
|
||||
|
||||
export interface PaseoAgentProviderTypeDefault {
|
||||
/** Pi wire protocol. `undefined` for `custom`, where the user must pick one. */
|
||||
api?: string;
|
||||
|
||||
@@ -1225,7 +1225,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
daemonSelfUpdate: true,
|
||||
// COMPAT(agentForkContext): added in v0.1.102, remove gate after 2026-12-28.
|
||||
agentForkContext: true,
|
||||
// COMPAT(paseoAgentConfig): added in v0.1.85, remove gate after 2026-11-30.
|
||||
// COMPAT(paseoAgentConfig): added in v0.1.103, remove gate after 2027-01-02.
|
||||
paseoAgentConfig: true,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user