/** * Canonical catalog of built-in web-search providers. * * This is the single source of truth that drives: * * 1. The config-schema enum (`VALID_WEB_SEARCH_PROVIDERS` in * `assistant/src/config/schemas/services.ts`). * 2. The search-provider env-var map (`SEARCH_PROVIDER_ENV_VAR_NAMES` * in `provider-env-vars.ts`). * 3. The API-key provider set (`SEARCH_API_KEY_PROVIDERS` in * `provider-secret-catalog.ts`). * 4. (Indirect, via `meta/web-search-provider-catalog.json` generated by * `assistant/scripts/sync-web-search-catalog.ts`) the CLI mirror in * `cli/src/shared/provider-env-vars.ts` and the web vendored catalog * in `vellum-assistant-platform`. * * Mirrors the pattern from `model-catalog.ts` (Provider Expansion Phase 3 * for LLM providers). When introducing a new built-in web-search provider, * the daemon-side changes collapse to: * * 1. Add a new executor in `tools/network/web-search.ts`. * 2. Register an adapter in the `WEB_SEARCH_ADAPTERS` table. * 3. Add an entry to this catalog. * 4. Run `bun run sync:web-search-catalog` to regenerate * `meta/web-search-provider-catalog.json`. * * The split between adapter (which carries the executor) and catalog (which * carries metadata) keeps the catalog purely declarative — adding an entry * never forces a code rebuild downstream and the JSON projection stays * trivial. */ export type SearchProviderKind = "managed" | "byok"; export interface SearchProviderCatalogEntry { /** Stable provider identifier. Matches config + secret-catalog values. */ readonly id: string; /** Short display name used by picker UIs. */ readonly displayName: string; /** * Optional long display name for prose contexts (marketing docs, * privacy notices). Defaults to {@link displayName} when omitted. */ readonly displayNameLong?: string; /** * Authentication style for the search provider choice. * * `managed` means the choice does not require a user-supplied search API key. * `vellum` searches through the Vellum platform search proxy, billed to * Vellum credits; the platform connection is the credential. For * `inference-provider-native`, the daemon uses the inference API's native * hosted web-search tool only when the selected inference provider/model * supports it; otherwise the app-executed `web_search` tool runs, falling * back to the platform search proxy only when no user search key is * configured. * * `byok` providers require a user-supplied API key. */ readonly kind: SearchProviderKind; /** * BYOK provider that also works without a user-supplied key (keyless by * default; a key is optional and only lifts rate limits). The daemon adapter * runs it even when no key is stored, and the missing-key fallback/error path * does not treat it as fatal. */ readonly keyless?: boolean; /** Placeholder shown in the API-key input. BYOK providers only. */ readonly apiKeyPrefix?: string; /** Environment variable name carrying the API key. BYOK providers only. */ readonly envVar?: string; /** Secret-catalog key (the bare provider name accepted by * `getProviderKeyAsync`). BYOK providers only. */ readonly secretKey?: string; /** Position in the daemon fallback chain (lower = earlier). BYOK only. */ readonly fallbackOrder?: number; /** Privacy-policy URL surfaced in marketing data-sharing docs. * BYOK providers only. */ readonly privacyPolicyUrl?: string; } export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [ // vellum leads: it is the managed option, selected like any other provider. { id: "vellum", displayName: "Vellum", displayNameLong: "Vellum Managed Search", kind: "managed", }, { id: "inference-provider-native", displayName: "Provider Native", kind: "managed", }, { id: "perplexity", displayName: "Perplexity", kind: "byok", apiKeyPrefix: "pplx-...", envVar: "PERPLEXITY_API_KEY", secretKey: "perplexity", fallbackOrder: 1, privacyPolicyUrl: "https://www.perplexity.ai/hub/legal/privacy-policy", }, { id: "brave", displayName: "Brave", displayNameLong: "Brave Search", kind: "byok", apiKeyPrefix: "BSA...", envVar: "BRAVE_API_KEY", secretKey: "brave", fallbackOrder: 2, privacyPolicyUrl: "https://search.brave.com/help/privacy-policy", }, { id: "tavily", displayName: "Tavily", kind: "byok", apiKeyPrefix: "tvly-...", envVar: "TAVILY_API_KEY", secretKey: "tavily", fallbackOrder: 3, privacyPolicyUrl: "https://tavily.com/privacy", }, { id: "firecrawl", displayName: "Firecrawl", kind: "byok", apiKeyPrefix: "fc-...", envVar: "FIRECRAWL_API_KEY", secretKey: "firecrawl", fallbackOrder: 4, privacyPolicyUrl: "https://www.firecrawl.dev/privacy-policy", }, { id: "keenable", displayName: "Keenable", kind: "byok", keyless: true, apiKeyPrefix: "keen_... (optional)", envVar: "KEENABLE_API_KEY", secretKey: "keenable", fallbackOrder: 5, privacyPolicyUrl: "https://keenable.ai/privacy", }, ]; /** Provider ids accepted by the web-search config schema. */ export const SEARCH_PROVIDER_IDS: readonly string[] = SEARCH_PROVIDER_CATALOG.map((p) => p.id); /** Catalog entries that store an API key under their bare provider name. */ export const BYOK_SEARCH_PROVIDERS: readonly SearchProviderCatalogEntry[] = SEARCH_PROVIDER_CATALOG.filter((p) => p.kind === "byok"); /** BYOK provider ids, ordered by `fallbackOrder` (ascending). */ export const SEARCH_PROVIDER_FALLBACK_ORDER: readonly string[] = BYOK_SEARCH_PROVIDERS.slice() .sort((a, b) => (a.fallbackOrder ?? 0) - (b.fallbackOrder ?? 0)) .map((p) => p.id); /** Look up a single catalog entry by id. Returns `undefined` if unknown. */ export function getSearchProvider( id: string, ): SearchProviderCatalogEntry | undefined { return SEARCH_PROVIDER_CATALOG.find((p) => p.id === id); }