mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
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:
@@ -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) } };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user