diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index acbca6461..27f529776 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -107,10 +107,45 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no** - Example model: `vercel-ai-gateway/anthropic/claude-opus-4.5` - CLI: `clawdbot onboard --auth-choice ai-gateway-api-key` +### OpenRouter + +- Provider: `openrouter` +- Auth: `OPENROUTER_API_KEY` +- Example model: `openrouter/anthropic/claude-sonnet-4-5` +- CLI: `clawdbot onboard --auth-choice openrouter-api-key` + +OpenRouter provides access to many third-party models through a unified API. +Model IDs use the format `openrouter//`. + +```json5 +{ + agents: { defaults: { model: { primary: "openrouter/anthropic/claude-sonnet-4-5" } } } +} +``` + +### Perplexity Agentic + +- Provider: `perplexity-agentic` +- Auth: `PERPLEXITY_API_KEY` +- Example model: `perplexity-agentic/openai/gpt-5.2` +- CLI: `clawdbot onboard --auth-choice perplexity-agentic-api-key` + +Perplexity Agentic provides access to third-party models (OpenAI, Anthropic, Google, xAI) via +Perplexity's `/v1/responses` endpoint. Model IDs use the format `perplexity-agentic//`. +Any model supported by Perplexity's Agentic API works - see [Perplexity docs](https://docs.perplexity.ai/) for available models. + +```json5 +{ + agents: { defaults: { model: { primary: "perplexity-agentic/openai/gpt-5.2" } } } +} +``` + +Note: This provider uses the same `PERPLEXITY_API_KEY` as Perplexity's other APIs (including the +`web_search` tool's Sonar integration). Clawdbot handles all tool execution - Perplexity's native +web search tools are not used. + ### Other built-in providers -- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`) -- Example model: `openrouter/anthropic/claude-sonnet-4-5` - xAI: `xai` (`XAI_API_KEY`) - Groq: `groq` (`GROQ_API_KEY`) - Cerebras: `cerebras` (`CEREBRAS_API_KEY`) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 680d0f53c..2a9d1995b 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -285,6 +285,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + "perplexity-agentic": "PERPLEXITY_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 996f09dd0..97257b1c6 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -418,9 +418,31 @@ export async function resolveImplicitProviders(params: { providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; } + // Perplexity Agentic provider - third-party models via Perplexity's /v1/responses + const perplexityAgenticKey = + resolveEnvApiKeyVarName("perplexity-agentic") ?? + resolveApiKeyFromProfiles({ provider: "perplexity-agentic", store: authStore }); + if (perplexityAgenticKey) { + providers["perplexity-agentic"] = { + ...buildPerplexityAgenticProvider(), + apiKey: perplexityAgenticKey, + }; + } + return providers; } +// Perplexity Agentic API - third-party models via Perplexity's /v1/responses endpoint. +const PERPLEXITY_AGENTIC_BASE_URL = "https://api.perplexity.ai"; + +function buildPerplexityAgenticProvider(): ProviderConfig { + return { + baseUrl: PERPLEXITY_AGENTIC_BASE_URL, + api: "openai-responses", + models: [], + }; +} + export async function resolveImplicitCopilotProvider(params: { agentDir: string; env?: NodeJS.ProcessEnv; diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index 93b7ee628..edbd18dad 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -53,6 +53,7 @@ describe("models-config", () => { const previousMoonshot = process.env.MOONSHOT_API_KEY; const previousSynthetic = process.env.SYNTHETIC_API_KEY; const previousVenice = process.env.VENICE_API_KEY; + const previousPerplexity = process.env.PERPLEXITY_API_KEY; delete process.env.COPILOT_GITHUB_TOKEN; delete process.env.GH_TOKEN; delete process.env.GITHUB_TOKEN; @@ -61,6 +62,7 @@ describe("models-config", () => { delete process.env.MOONSHOT_API_KEY; delete process.env.SYNTHETIC_API_KEY; delete process.env.VENICE_API_KEY; + delete process.env.PERPLEXITY_API_KEY; try { vi.resetModules(); @@ -93,6 +95,8 @@ describe("models-config", () => { else process.env.SYNTHETIC_API_KEY = previousSynthetic; if (previousVenice === undefined) delete process.env.VENICE_API_KEY; else process.env.VENICE_API_KEY = previousVenice; + if (previousPerplexity === undefined) delete process.env.PERPLEXITY_API_KEY; + else process.env.PERPLEXITY_API_KEY = previousPerplexity; } }); }); diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 281464b6f..7581af894 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|perplexity-agentic-api-key|skip", ) .option( "--token-provider ", @@ -75,6 +75,7 @@ export function registerOnboardCommand(program: Command) { .option("--minimax-api-key ", "MiniMax API key") .option("--synthetic-api-key ", "Synthetic API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") + .option("--perplexity-agentic-api-key ", "Perplexity API key for Agentic models") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: off|token|password") @@ -124,6 +125,7 @@ export function registerOnboardCommand(program: Command) { minimaxApiKey: opts.minimaxApiKey as string | undefined, syntheticApiKey: opts.syntheticApiKey as string | undefined, opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined, + perplexityAgenticApiKey: opts.perplexityAgenticApiKey as string | undefined, gatewayPort: typeof gatewayPort === "number" && Number.isFinite(gatewayPort) ? gatewayPort diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index f13eef365..33e335e9d 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -22,7 +22,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "perplexity-agentic"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -115,6 +116,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "perplexity-agentic", + label: "Perplexity Agentic", + hint: "Third-party models (OpenAI, Anthropic, Google, xAI)", + choices: ["perplexity-agentic-api-key"], + }, ]; function formatOAuthHint(expires?: number, opts?: { allowStale?: boolean }): string { @@ -238,6 +245,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "perplexity-agentic-api-key", + label: "Perplexity Agentic API key", + hint: "Access OpenAI, Anthropic, Google, xAI models via Perplexity", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..b83877849 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -21,6 +21,8 @@ import { applyOpencodeZenProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPerplexityAgenticConfig, + applyPerplexityAgenticProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -31,6 +33,7 @@ import { KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, + PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, @@ -39,6 +42,7 @@ import { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPerplexityAgenticApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, @@ -85,6 +89,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "venice-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; + } else if (params.opts.tokenProvider === "perplexity-agentic") { + authChoice = "perplexity-agentic-api-key"; } } @@ -579,5 +585,66 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "perplexity-agentic-api-key") { + let hasCredential = false; + if ( + !hasCredential && + params.opts?.token && + params.opts?.tokenProvider === "perplexity-agentic" + ) { + await setPerplexityAgenticApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Perplexity Agentic provides access to third-party models (OpenAI, Anthropic, Google, xAI).", + "Get your API key at: https://www.perplexity.ai/settings/api", + "Uses the same API key as Perplexity's other APIs.", + ].join("\n"), + "Perplexity Agentic", + ); + } + const envKey = resolveEnvApiKey("perplexity-agentic"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing PERPLEXITY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setPerplexityAgenticApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Perplexity API key", + validate: validateApiKeyInput, + }); + await setPerplexityAgenticApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "perplexity-agentic:default", + provider: "perplexity-agentic", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, + applyDefaultConfig: applyPerplexityAgenticConfig, + applyProviderConfig: applyPerplexityAgenticProviderConfig, + noteDefault: PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + return null; } diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 0d3a8523a..cc424d98d 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -13,6 +13,7 @@ import { import type { ClawdbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, + PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; @@ -411,6 +412,55 @@ export function applyVeniceConfig(cfg: ClawdbotConfig): ClawdbotConfig { }; } +/** + * Apply Perplexity Agentic provider configuration without changing the default model. + * Registers model alias but preserves existing model selection. + */ +export function applyPerplexityAgenticProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF] = { + ...models[PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF], + alias: models[PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF]?.alias ?? "Perplexity GPT-5.2", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + }; +} + +/** + * Apply Perplexity Agentic provider configuration AND set it as the default model. + * Use this when Perplexity Agentic is the primary provider choice during onboarding. + */ +export function applyPerplexityAgenticConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyPerplexityAgenticProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: ClawdbotConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 0c7dff409..70ea6f89e 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -164,3 +164,17 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export const PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF = "perplexity-agentic/openai/gpt-5.2"; + +export async function setPerplexityAgenticApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "perplexity-agentic:default", + credential: { + type: "api_key", + provider: "perplexity-agentic", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..90a94f94d 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -11,6 +11,8 @@ export { applyMoonshotProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, + applyPerplexityAgenticConfig, + applyPerplexityAgenticProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, applyVeniceConfig, @@ -34,6 +36,7 @@ export { } from "./onboard-auth.config-opencode.js"; export { OPENROUTER_DEFAULT_MODEL_REF, + PERPLEXITY_AGENTIC_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setKimiCodeApiKey, @@ -41,6 +44,7 @@ export { setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, + setPerplexityAgenticApiKey, setSyntheticApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 84c15afc4..310d78e3d 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -31,6 +31,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "perplexity-agentic-api-key" | "skip"; export type GatewayAuthChoice = "off" | "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -71,6 +72,7 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + perplexityAgenticApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice;