refactor(paseo-agent): derive catalog from Pi

Keep Paseo catalog entries as curated provider refs while deriving model metadata, endpoints, and auth defaults from Pi at runtime.
This commit is contained in:
Mohamed Boudra
2026-07-02 12:40:21 +02:00
parent 2ba7562dff
commit d6e4e5f3ce
8 changed files with 433 additions and 390 deletions
@@ -1,80 +1,56 @@
export interface PaseoAgentCatalogModel {
[key: string]: unknown;
id: string;
label?: string;
api?: string;
reasoning?: boolean;
contextWindow?: number;
maxTokens?: number;
}
import type { KnownProvider } from "@earendil-works/pi-ai";
export interface PaseoAgentApiKeyAuth {
[key: string]: unknown;
export interface PaseoAgentKeyAuthHint {
kind: "api_key";
envVar: string;
envVar?: string;
keyUrl?: string;
placeholder?: string;
hint?: string;
}
export interface PaseoAgentOAuthAuth {
[key: string]: unknown;
export interface PaseoAgentOAuthAuthHint {
kind: "oauth";
flow: string;
flow?: string;
}
export type PaseoAgentCatalogAuth = PaseoAgentApiKeyAuth | PaseoAgentOAuthAuth;
export type PaseoAgentCatalogAuthHint = PaseoAgentKeyAuthHint | PaseoAgentOAuthAuthHint;
export interface PaseoAgentCatalogEntry {
[key: string]: unknown;
export interface PaseoAgentCatalogRef {
id: string;
piProvider: KnownProvider;
label: string;
iconName?: string;
docsUrl?: string;
api: string;
baseUrl: string;
headers?: Record<string, string>;
compat?: Record<string, unknown>;
auth: PaseoAgentCatalogAuth;
models: PaseoAgentCatalogModel[];
auth?: PaseoAgentCatalogAuthHint;
defaultModels?: boolean;
}
export const PASEO_AGENT_PROVIDER_CATALOG = [
{
id: "openrouter",
piProvider: "openrouter",
label: "OpenRouter",
api: "openai-completions",
baseUrl: "https://openrouter.ai/api/v1",
auth: { kind: "api_key", envVar: "OPENROUTER_API_KEY" },
models: [],
defaultModels: false,
},
{
id: "chatgpt",
piProvider: "openai-codex",
label: "ChatGPT",
iconName: "openai",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
auth: { kind: "oauth", flow: "openai-codex" },
models: [{ id: "gpt-5.4-mini", reasoning: true }],
},
{
id: "kimi",
piProvider: "kimi-coding",
label: "Kimi Coding Plan",
api: "anthropic-messages",
baseUrl: "https://api.kimi.com/coding",
headers: { "User-Agent": "KimiCLI/1.5" },
auth: { kind: "api_key", envVar: "KIMI_API_KEY" },
models: [],
},
{
id: "opencode-go",
piProvider: "opencode-go",
label: "OpenCode Go",
api: "openai-completions",
baseUrl: "https://opencode.ai/zen/go/v1",
auth: { kind: "api_key", envVar: "OPENCODE_API_KEY" },
models: [],
},
] as const satisfies readonly PaseoAgentCatalogEntry[];
] as const satisfies readonly PaseoAgentCatalogRef[];
const PASEO_AGENT_PROVIDER_ALIASES: Record<string, string> = {
"openai-codex": "chatgpt",
@@ -82,7 +58,7 @@ const PASEO_AGENT_PROVIDER_ALIASES: Record<string, string> = {
export function resolvePaseoAgentCatalogEntry(
providerType: string,
): PaseoAgentCatalogEntry | undefined {
): PaseoAgentCatalogRef | undefined {
const canonicalId = PASEO_AGENT_PROVIDER_ALIASES[providerType] ?? providerType;
return PASEO_AGENT_PROVIDER_CATALOG.find((entry) => entry.id === canonicalId);
}
@@ -95,7 +71,7 @@ export function unknownPaseoAgentProviderTypeMessage(providerType: string): stri
return `Unknown model provider type "${providerType}". Known provider ids: ${knownPaseoAgentCatalogIds().join(", ")}. Update the host if this provider is newer than it.`;
}
export function requirePaseoAgentCatalogEntry(providerType: string): PaseoAgentCatalogEntry {
export function requirePaseoAgentCatalogEntry(providerType: string): PaseoAgentCatalogRef {
const entry = resolvePaseoAgentCatalogEntry(providerType);
if (!entry) {
throw new Error(unknownPaseoAgentProviderTypeMessage(providerType));
@@ -1,6 +1,7 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModels } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { createTestLogger } from "../../../../test-utils/test-logger.js";
@@ -9,6 +10,17 @@ import { PaseoAgentConfigService } from "./config-service.js";
import { PaseoAgentConfigSchema } from "./config.js";
import { paseoAgentAuthStoragePath, storeOAuthCredential } from "./oauth-store.js";
function piCatalogModels(provider: Parameters<typeof getModels>[0]) {
return getModels(provider).map((model) => ({
id: model.id,
label: model.name,
api: model.api,
reasoning: model.reasoning,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
}));
}
describe("PaseoAgentConfigService", () => {
let home: string;
@@ -156,6 +168,27 @@ describe("PaseoAgentConfigService", () => {
expect(loadPersistedConfig(home).agents?.paseo?.providers?.chatgpt?.type).toBe("chatgpt");
});
test("does not persist Pi catalog defaults as instance model overrides", () => {
const service = new PaseoAgentConfigService({
paseoHome: home,
logger: createTestLogger(),
});
const chatgptCatalog = service.getCatalog().find((entry) => entry.id === "chatgpt");
if (!chatgptCatalog) {
throw new Error("missing chatgpt catalog entry");
}
service.setProvider({
name: "chatgpt",
providerType: "chatgpt",
options: { models: chatgptCatalog.models },
});
expect(loadPersistedConfig(home).agents?.paseo?.providers?.chatgpt?.options.models).toBe(
undefined,
);
});
test("preserves shared config fields when writing agents.paseo", () => {
const logger = createTestLogger();
savePersistedConfig(
@@ -258,7 +291,7 @@ describe("PaseoAgentConfigService", () => {
expect.objectContaining({
name: "chatgpt",
providerType: "chatgpt",
models: [{ id: "gpt-5.4-mini", reasoning: true }],
models: piCatalogModels("openai-codex"),
auth: { kind: "oauth", configured: true, source: "stored" },
available: true,
}),
@@ -11,16 +11,17 @@ import {
type PersistedConfig,
} from "../../../persisted-config.js";
import {
isPaseoAgentDefaultModelSelection,
PaseoAgentConfigSchema,
paseoAgentCatalogManifests,
type PaseoAgentConfig,
type PaseoAgentCatalogManifestEntry,
type PaseoAgentProviderModelConfig,
resolvePaseoAgentCatalogAuth,
resolvePaseoAgentProviderModels,
resolvePaseoAgentProviderSettings,
} from "./config.js";
import {
PASEO_AGENT_PROVIDER_CATALOG,
type PaseoAgentCatalogEntry,
requirePaseoAgentCatalogEntry,
} from "./catalog.js";
import { requirePaseoAgentCatalogEntry, type PaseoAgentCatalogRef } from "./catalog.js";
import {
getStoredOAuthCredentialState,
storeOAuthCredential,
@@ -45,14 +46,7 @@ interface SetProviderInput {
api?: string;
headers?: Record<string, string>;
authHeader?: boolean;
models?: Array<{
id: string;
label?: string;
api?: string;
reasoning?: boolean;
contextWindow?: number;
maxTokens?: number;
}>;
models?: PaseoAgentProviderModelConfig[];
};
}
@@ -91,16 +85,27 @@ function authStateForApiKey(
return { kind: "api_key", configured: true, source: "literal" };
}
function copyCatalogEntry(entry: PaseoAgentCatalogEntry): PaseoAgentCatalogEntry {
function copyCatalogEntry(entry: PaseoAgentCatalogManifestEntry): PaseoAgentCatalogManifestEntry {
return {
...entry,
...(entry.headers ? { headers: { ...entry.headers } } : {}),
...(entry.compat ? { compat: { ...entry.compat } } : {}),
auth: { ...entry.auth },
models: entry.models.map((model) => ({ ...model })),
};
}
function providerOptionsForPersist(
options: SetProviderInput["options"],
catalogEntry: PaseoAgentCatalogRef,
): SetProviderInput["options"] {
if (!isPaseoAgentDefaultModelSelection(options.models, catalogEntry)) {
return options;
}
const rest = { ...options };
delete rest.models;
return rest;
}
function oauthBindingForSettings(
flow: string,
settings: ReturnType<typeof resolvePaseoAgentProviderSettings>,
@@ -125,10 +130,11 @@ function redactedProviders(
): RedactedPaseoAgentProviderConfig[] {
return Object.entries(config.providers ?? {}).map(([name, entry]) => {
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
const authManifest = resolvePaseoAgentCatalogAuth(catalogEntry);
const settings = resolvePaseoAgentProviderSettings(entry, catalogEntry);
const models = resolvePaseoAgentProviderModels(entry, catalogEntry);
let auth: PaseoAgentProviderAuthState;
if (catalogEntry.auth.kind === "oauth") {
if (authManifest.kind === "oauth") {
const hasRefreshToken =
entry.options.refreshToken &&
isRefreshTokenExpressionConfigured(entry.options.refreshToken, env);
@@ -138,7 +144,7 @@ function redactedProviders(
const stored = getStoredOAuthCredentialState(
name,
env,
oauthBindingForSettings(catalogEntry.auth.flow, settings),
oauthBindingForSettings(authManifest.flow, settings),
);
if (stored.present && stored.bindingMatches) {
auth = { kind: "oauth", configured: true, source: "stored" };
@@ -154,7 +160,7 @@ function redactedProviders(
}
}
} else {
auth = authStateForApiKey(entry.options.apiKey, catalogEntry.auth.envVar, env);
auth = authStateForApiKey(entry.options.apiKey, authManifest.envVar, env);
}
const provider: RedactedPaseoAgentProviderConfig = {
name,
@@ -196,8 +202,8 @@ export class PaseoAgentConfigService {
this.onConfigChanged = options.onConfigChanged;
}
getCatalog(): PaseoAgentCatalogEntry[] {
return PASEO_AGENT_PROVIDER_CATALOG.map(copyCatalogEntry);
getCatalog(): PaseoAgentCatalogManifestEntry[] {
return paseoAgentCatalogManifests().map(copyCatalogEntry);
}
getProviders(): { defaultModel: string | null; providers: RedactedPaseoAgentProviderConfig[] } {
@@ -217,7 +223,7 @@ export class PaseoAgentConfigService {
...current.providers,
[input.name]: {
type: catalogEntry.id,
options: input.options,
options: providerOptionsForPersist(input.options, catalogEntry),
},
},
}),
@@ -247,11 +253,12 @@ export class PaseoAgentConfigService {
throw new Error(`Paseo Agent provider '${providerName}' is not configured.`);
}
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
if (catalogEntry.auth.kind !== "oauth") {
const authManifest = resolvePaseoAgentCatalogAuth(catalogEntry);
if (authManifest.kind !== "oauth") {
throw new Error(`Paseo Agent provider '${providerName}' does not use OAuth.`);
}
const settings = resolvePaseoAgentProviderSettings(entry, catalogEntry);
return oauthBindingForSettings(catalogEntry.auth.flow, settings);
return oauthBindingForSettings(authManifest.flow, settings);
}
storeOAuthCredential(
@@ -265,14 +272,15 @@ export class PaseoAgentConfigService {
throw new Error(`Paseo Agent provider '${providerName}' is not configured.`);
}
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
if (catalogEntry.auth.kind !== "oauth") {
const authManifest = resolvePaseoAgentCatalogAuth(catalogEntry);
if (authManifest.kind !== "oauth") {
throw new Error(`Paseo Agent provider '${providerName}' does not use OAuth.`);
}
const settings = resolvePaseoAgentProviderSettings(entry, catalogEntry);
storeOAuthCredential({
providerInstance: providerName,
credential,
binding: binding ?? oauthBindingForSettings(catalogEntry.auth.flow, settings),
binding: binding ?? oauthBindingForSettings(authManifest.flow, settings),
env: this.env,
});
this.onConfigChanged?.(config);
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { getModels } from "@earendil-works/pi-ai";
import {
PaseoAgentConfigSchema,
@@ -11,6 +12,10 @@ import {
type PaseoAgentConfig,
} from "./config.js";
function piModelIds(provider: Parameters<typeof getModels>[0]): string[] {
return getModels(provider).map((model) => model.id);
}
function configWith(overrides?: Partial<PaseoAgentConfig>): PaseoAgentConfig {
return PaseoAgentConfigSchema.parse({
providers: {
@@ -107,7 +112,9 @@ describe("listPaseoAgentModels", () => {
const models = listPaseoAgentModels(
PaseoAgentConfigSchema.parse({ providers: { chatgpt: { type: "chatgpt" } } }),
);
expect(models.map((m) => m.id)).toEqual(["chatgpt/gpt-5.4-mini"]);
expect(models.map((m) => m.id)).toEqual(
piModelIds("openai-codex").map((modelId) => `chatgpt/${modelId}`),
);
});
it("marks the configured default model", () => {
@@ -153,7 +160,7 @@ describe("paseoAgentModelProviders", () => {
expect(provider.config.baseUrl).toBe("https://api.kimi.com/coding");
expect(provider.config.apiKey).toBe("$KIMI_API_KEY");
expect(provider.config.api).toBe("anthropic-messages");
expect(provider.config.headers).toEqual({ "User-Agent": "KimiCLI/1.5" });
expect(provider.config.models?.[0]?.headers).toEqual({ "User-Agent": "KimiCLI/1.5" });
expect(provider.config.models?.[0]?.api).toBe("anthropic-messages");
});
@@ -179,7 +186,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.4-mini");
expect(provider.config.models?.map((model) => model.id)).toEqual(piModelIds("openai-codex"));
});
it("lets instance models override catalog default models", async () => {
@@ -205,7 +212,7 @@ describe("paseoAgentModelProviders", () => {
}),
);
expect(provider.oauth).toEqual({ flow: "openai-codex" });
expect(provider.config.models?.[0]?.id).toBe("gpt-5.4-mini");
expect(provider.config.models?.map((model) => model.id)).toEqual(piModelIds("openai-codex"));
});
it("rejects unknown provider types at runtime with known ids", async () => {
@@ -295,7 +302,7 @@ describe("resolvePaseoAgentModel", () => {
PaseoAgentConfigSchema.parse({ providers: { chatgpt: { type: "chatgpt" } } }),
null,
),
).toEqual({ provider: "chatgpt", id: "gpt-5.4-mini" });
).toEqual({ provider: "chatgpt", id: piModelIds("openai-codex")[0] });
});
it("ignores an implicit default whose provider is not registered", () => {
@@ -1,3 +1,6 @@
import { findEnvKeys, getModels, type Api, type Model } from "@earendil-works/pi-ai";
import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
import type { PaseoAgentCatalogEntry as PaseoAgentCatalogManifestEntry } from "@getpaseo/protocol/messages";
import { z } from "zod";
import type { AgentModelDefinition } from "../../agent-sdk-types.js";
@@ -8,9 +11,9 @@ import {
import type { OAuthCredentialBinding } from "./oauth-store.js";
import type { PaseoAgentModelProvider, PaseoAgentModelReference } from "./pi-services.js";
import {
PASEO_AGENT_PROVIDER_CATALOG,
requirePaseoAgentCatalogEntry,
type PaseoAgentCatalogEntry,
type PaseoAgentCatalogModel,
type PaseoAgentCatalogRef,
} from "./catalog.js";
import { findEnvReferences } from "./env-references.js";
@@ -57,14 +60,33 @@ export const PaseoAgentConfigSchema = z
export type PaseoAgentConfig = z.infer<typeof PaseoAgentConfigSchema>;
export type PaseoAgentModelProviderEntry = z.infer<typeof PaseoAgentModelProviderSchema>;
type PiModelConfig = NonNullable<PaseoAgentModelProvider["config"]["models"]>[number];
export type PaseoAgentProviderModelConfig = z.infer<typeof PaseoAgentModelSchema>;
export type { PaseoAgentCatalogManifestEntry };
type PiModel = Model<Api>;
type PiModelConfig = NonNullable<PaseoAgentModelProvider["config"]["models"]>[number];
type ProviderOptions = PaseoAgentModelProviderEntry["options"];
const DEFAULT_INPUT: PiModelConfig["input"] = ["text"];
const ZERO_COST: PiModelConfig["cost"] = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const DEFAULT_CONTEXT_WINDOW = 128_000;
const DEFAULT_MAX_TOKENS = 16_384;
const DEFAULT_MODEL_FIELDS = [
"id",
"label",
"api",
"reasoning",
"contextWindow",
"maxTokens",
] as const;
type ResolvedCatalogAuth =
| { kind: "api_key"; envVar: string; keyUrl?: string; placeholder?: string; hint?: string }
| { kind: "oauth"; flow: string };
export interface ResolvedProviderSettings {
baseUrl: string;
api: string;
api: Api;
apiKey?: string;
headers?: Record<string, string>;
authHeader?: boolean;
@@ -74,37 +96,131 @@ function entries(config: PaseoAgentConfig): [string, PaseoAgentModelProviderEntr
return Object.entries(config.providers ?? {});
}
function mergeHeaders(
catalogHeaders: Record<string, string> | undefined,
optionHeaders: Record<string, string> | undefined,
): Record<string, string> | undefined {
const headers = { ...catalogHeaders, ...optionHeaders };
return Object.keys(headers).length > 0 ? headers : undefined;
function getPaseoAgentPiModels(catalogEntry: PaseoAgentCatalogRef): PiModel[] {
return getModels(catalogEntry.piProvider);
}
function requirePaseoAgentPrimaryModel(catalogEntry: PaseoAgentCatalogRef): PiModel {
const first = getPaseoAgentPiModels(catalogEntry)[0];
if (!first) {
throw new Error(`Paseo Agent provider "${catalogEntry.id}" has no Pi models.`);
}
return first;
}
function defaultPaseoAgentPiModels(catalogEntry: PaseoAgentCatalogRef): PiModel[] {
return catalogEntry.defaultModels === false ? [] : getPaseoAgentPiModels(catalogEntry);
}
export function resolvePaseoAgentCatalogAuth(
catalogEntry: PaseoAgentCatalogRef,
): ResolvedCatalogAuth {
if (catalogEntry.auth?.kind === "oauth") {
return { kind: "oauth", flow: catalogEntry.auth.flow ?? catalogEntry.piProvider };
}
if (!catalogEntry.auth && getOAuthProvider(catalogEntry.piProvider)) {
return { kind: "oauth", flow: catalogEntry.piProvider };
}
const envVar =
catalogEntry.auth?.kind === "api_key"
? catalogEntry.auth.envVar
: findEnvKeys(catalogEntry.piProvider)?.[0];
if (!envVar) {
throw new Error(`Paseo Agent provider "${catalogEntry.id}" has no auth source.`);
}
return {
kind: "api_key",
envVar,
...(catalogEntry.auth?.kind === "api_key" && catalogEntry.auth.keyUrl
? { keyUrl: catalogEntry.auth.keyUrl }
: {}),
...(catalogEntry.auth?.kind === "api_key" && catalogEntry.auth.placeholder
? { placeholder: catalogEntry.auth.placeholder }
: {}),
...(catalogEntry.auth?.kind === "api_key" && catalogEntry.auth.hint
? { hint: catalogEntry.auth.hint }
: {}),
};
}
export function resolvePaseoAgentProviderSettings(
entry: PaseoAgentModelProviderEntry,
catalogEntry: PaseoAgentCatalogEntry = requirePaseoAgentCatalogEntry(entry.type),
catalogEntry: PaseoAgentCatalogRef = requirePaseoAgentCatalogEntry(entry.type),
): ResolvedProviderSettings {
const apiKey =
catalogEntry.auth.kind === "api_key"
? (entry.options.apiKey ?? `$${catalogEntry.auth.envVar}`)
: undefined;
const headers = mergeHeaders(catalogEntry.headers, entry.options.headers);
const primaryModel = requirePaseoAgentPrimaryModel(catalogEntry);
const auth = resolvePaseoAgentCatalogAuth(catalogEntry);
const apiKey = auth.kind === "api_key" ? (entry.options.apiKey ?? `$${auth.envVar}`) : undefined;
return {
baseUrl: entry.options.baseUrl ?? catalogEntry.baseUrl,
api: entry.options.api ?? catalogEntry.api,
baseUrl: entry.options.baseUrl ?? primaryModel.baseUrl,
api: entry.options.api ?? primaryModel.api,
...(apiKey ? { apiKey } : {}),
...(headers ? { headers } : {}),
...(entry.options.headers ? { headers: entry.options.headers } : {}),
...(entry.options.authHeader ? { authHeader: entry.options.authHeader } : {}),
};
}
function toCatalogModel(model: PiModel): PaseoAgentProviderModelConfig {
return {
id: model.id,
label: model.name,
api: model.api,
reasoning: model.reasoning,
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
};
}
export function resolvePaseoAgentProviderModels(
entry: PaseoAgentModelProviderEntry,
catalogEntry: PaseoAgentCatalogEntry = requirePaseoAgentCatalogEntry(entry.type),
): PaseoAgentCatalogModel[] {
return entry.options.models ?? catalogEntry.models;
catalogEntry: PaseoAgentCatalogRef = requirePaseoAgentCatalogEntry(entry.type),
): PaseoAgentProviderModelConfig[] {
return entry.options.models ?? defaultPaseoAgentPiModels(catalogEntry).map(toCatalogModel);
}
export function isPaseoAgentDefaultModelSelection(
models: PaseoAgentProviderModelConfig[] | undefined,
catalogEntry: PaseoAgentCatalogRef,
): boolean {
if (!models) {
return false;
}
const defaults = defaultPaseoAgentPiModels(catalogEntry).map(toCatalogModel);
if (models.length !== defaults.length || defaults.length === 0) {
return false;
}
return models.every((model, index) => {
const defaultModel = defaults[index];
return DEFAULT_MODEL_FIELDS.every((field) => model[field] === defaultModel?.[field]);
});
}
export function paseoAgentCatalogManifests(): PaseoAgentCatalogManifestEntry[] {
return PASEO_AGENT_PROVIDER_CATALOG.map((catalogEntry: PaseoAgentCatalogRef) => {
const primaryModel = requirePaseoAgentPrimaryModel(catalogEntry);
const manifest: PaseoAgentCatalogManifestEntry = {
id: catalogEntry.id,
label: catalogEntry.label,
api: primaryModel.api,
baseUrl: primaryModel.baseUrl,
auth: resolvePaseoAgentCatalogAuth(catalogEntry),
models: defaultPaseoAgentPiModels(catalogEntry).map(toCatalogModel),
};
if (catalogEntry.iconName) {
manifest.iconName = catalogEntry.iconName;
}
if (catalogEntry.docsUrl) {
manifest.docsUrl = catalogEntry.docsUrl;
}
if (primaryModel.headers) {
manifest.headers = { ...primaryModel.headers };
}
return manifest;
});
}
/**
@@ -138,26 +254,65 @@ export function parsePaseoAgentModelId(modelId: string): PaseoAgentModelReferenc
return { provider: modelId.slice(0, slash), id: modelId.slice(slash + 1) };
}
function applyModelOverrides(
model: PiModel,
options: ProviderOptions,
override?: PaseoAgentProviderModelConfig,
): PiModelConfig {
return {
id: override?.id ?? model.id,
name: override?.label ?? model.name,
api: override?.api ?? options.api ?? model.api,
baseUrl: options.baseUrl ?? model.baseUrl,
reasoning: override?.reasoning ?? model.reasoning,
...(model.thinkingLevelMap ? { thinkingLevelMap: model.thinkingLevelMap } : {}),
input: model.input,
cost: model.cost,
contextWindow: override?.contextWindow ?? model.contextWindow,
maxTokens: override?.maxTokens ?? model.maxTokens,
...(model.headers ? { headers: { ...model.headers } } : {}),
...(model.compat ? { compat: model.compat } : {}),
};
}
function customModelFromOptions(
model: PaseoAgentProviderModelConfig,
options: ProviderOptions,
fallback: PiModel,
): PiModelConfig {
return {
id: model.id,
name: model.label ?? model.id,
api: model.api ?? options.api ?? fallback.api,
baseUrl: options.baseUrl ?? fallback.baseUrl,
reasoning: model.reasoning ?? false,
input: fallback.input ?? DEFAULT_INPUT,
cost: ZERO_COST,
contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: model.maxTokens ?? DEFAULT_MAX_TOKENS,
...(fallback.headers ? { headers: { ...fallback.headers } } : {}),
};
}
function toPiModels(
entry: PaseoAgentModelProviderEntry,
settings: ResolvedProviderSettings,
catalogEntry: PaseoAgentCatalogRef,
): PiModelConfig[] {
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
return resolvePaseoAgentProviderModels(entry, catalogEntry).map((model) => {
const api = model.api ?? settings.api;
const piModel: PiModelConfig = {
id: model.id,
name: model.label ?? model.id,
reasoning: model.reasoning ?? false,
input: ["text"] as ("text" | "image")[],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
maxTokens: model.maxTokens ?? DEFAULT_MAX_TOKENS,
};
if (api) {
piModel.api = api;
}
return piModel;
const piModels = getPaseoAgentPiModels(catalogEntry);
const piModelsById = new Map(piModels.map((model) => [model.id, model]));
const selectedModels = entry.options.models;
if (!selectedModels) {
return defaultPaseoAgentPiModels(catalogEntry).map((model) =>
applyModelOverrides(model, entry.options),
);
}
const fallback = requirePaseoAgentPrimaryModel(catalogEntry);
return selectedModels.map((model) => {
const piModel = piModelsById.get(model.id);
return piModel
? applyModelOverrides(piModel, entry.options, model)
: customModelFromOptions(model, entry.options, fallback);
});
}
@@ -169,22 +324,24 @@ export async function paseoAgentModelProviders(
for (const [name, entry] of entries(config)) {
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
const auth = resolvePaseoAgentCatalogAuth(catalogEntry);
const settings = resolvePaseoAgentProviderSettings(entry, catalogEntry);
const models = toPiModels(entry, settings);
const models = toPiModels(entry, catalogEntry);
const providerConfig = {
baseUrl: settings.baseUrl,
api: settings.api,
...(settings.headers ? { headers: settings.headers } : {}),
models,
};
if (catalogEntry.auth.kind === "oauth") {
if (auth.kind === "oauth") {
const refreshToken = entry.options.refreshToken
? await resolveRefreshTokenExpression(entry.options.refreshToken, env)
: undefined;
providers.push({
name,
config: {
baseUrl: settings.baseUrl,
api: settings.api,
...(settings.headers ? { headers: settings.headers } : {}),
models,
},
oauth: { flow: catalogEntry.auth.flow, ...(refreshToken ? { refreshToken } : {}) },
config: providerConfig,
oauth: { flow: auth.flow, ...(refreshToken ? { refreshToken } : {}) },
});
continue;
}
@@ -192,12 +349,9 @@ export async function paseoAgentModelProviders(
providers.push({
name,
config: {
baseUrl: settings.baseUrl,
...providerConfig,
...(settings.apiKey ? { apiKey: settings.apiKey } : {}),
api: settings.api,
...(settings.headers ? { headers: settings.headers } : {}),
...(settings.authHeader ? { authHeader: settings.authHeader } : {}),
models,
},
});
}
@@ -231,10 +385,13 @@ export function paseoAgentHasUsableModel(
): boolean {
return entries(config).some(([name, entry]) => {
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
if (resolvePaseoAgentProviderModels(entry, catalogEntry).length === 0) {
const models = resolvePaseoAgentProviderModels(entry, catalogEntry);
if (models.length === 0) {
return false;
}
if (catalogEntry.auth.kind === "oauth") {
const auth = resolvePaseoAgentCatalogAuth(catalogEntry);
if (auth.kind === "oauth") {
if (
entry.options.refreshToken &&
isRefreshTokenExpressionConfigured(entry.options.refreshToken, env)
@@ -242,7 +399,7 @@ export function paseoAgentHasUsableModel(
return true;
}
return isOAuthAuthed(name, {
flow: catalogEntry.auth.flow,
flow: auth.flow,
baseUrl: resolvePaseoAgentProviderSettings(entry, catalogEntry).baseUrl,
});
}
@@ -275,8 +432,8 @@ export function resolvePaseoAgentModel(
function paseoAgentModelInventory(config: PaseoAgentConfig): PaseoAgentModelProvider[] {
return entries(config).map(([name, entry]) => {
const settings = resolvePaseoAgentProviderSettings(entry);
return { name, config: { models: toPiModels(entry, settings) } };
const catalogEntry = requirePaseoAgentCatalogEntry(entry.type);
return { name, config: { models: toPiModels(entry, catalogEntry) } };
});
}