feat: add Perplexity as a first-class API provider

Introduce a new Perplexity provider that targets Perplexity's
OpenAI-compatible chat-completions endpoint at https://api.perplexity.ai.
Models exposed: sonar, sonar-pro, sonar-reasoning, sonar-reasoning-pro
(all 128k context).

API key resolution order:
  1. perplexityApiKey from settings
  2. PERPLEXITY_API_KEY env var
  3. PPLX_API_KEY env var (fallback)

Wired into the provider registry (packages/types), api factory,
profile validation, webview settings UI, model picker, validation,
and English locale strings.

Tests cover construction, base URL/auth, env-var fallbacks, model
selection, streaming, and error propagation.
This commit is contained in:
james-pplx 2026-04-29 22:28:49 +00:00 committed by PSI Bot
parent b867ec9145
commit 16e37607d0
19 changed files with 464 additions and 8 deletions

View file

@ -275,6 +275,7 @@ export const SECRET_STATE_KEYS = [
"sambaNovaApiKey",
"zaiApiKey",
"fireworksApiKey",
"perplexityApiKey",
"vercelAiGatewayApiKey",
"basetenApiKey",
] as const

View file

@ -13,6 +13,7 @@ import {
moonshotModels,
openAiCodexModels,
openAiNativeModels,
perplexityModels,
qwenCodeModels,
sambaNovaModels,
vertexModels,
@ -114,6 +115,7 @@ export const providerNames = [
"minimax",
"openai-codex",
"openai-native",
"perplexity",
"qwen-code",
"sambanova",
"vertex",
@ -368,6 +370,10 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({
fireworksApiKey: z.string().optional(),
})
const perplexitySchema = apiModelIdProviderModelSchema.extend({
perplexityApiKey: z.string().optional(),
})
const qwenCodeSchema = apiModelIdProviderModelSchema.extend({
qwenCodeOauthPath: z.string().optional(),
})
@ -412,6 +418,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })),
zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
perplexitySchema.merge(z.object({ apiProvider: z.literal("perplexity") })),
qwenCodeSchema.merge(z.object({ apiProvider: z.literal("qwen-code") })),
vercelAiGatewaySchema.merge(z.object({ apiProvider: z.literal("vercel-ai-gateway") })),
defaultSchema,
@ -445,6 +452,7 @@ export const providerSettingsSchema = z.object({
...sambaNovaSchema.shape,
...zaiSchema.shape,
...fireworksSchema.shape,
...perplexitySchema.shape,
...qwenCodeSchema.shape,
...vercelAiGatewaySchema.shape,
...codebaseIndexProviderSchema.shape,
@ -520,6 +528,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
sambanova: "apiModelId",
zai: "apiModelId",
fireworks: "apiModelId",
perplexity: "apiModelId",
"vercel-ai-gateway": "vercelAiGatewayModelId",
}
@ -575,6 +584,11 @@ export const MODELS_BY_PROVIDER: Record<
label: "Fireworks",
models: Object.keys(fireworksModels),
},
perplexity: {
id: "perplexity",
label: "Perplexity",
models: Object.keys(perplexityModels),
},
gemini: {
id: "gemini",
label: "Google Gemini",

View file

@ -13,6 +13,7 @@ export * from "./openai.js"
export * from "./openai-codex.js"
export * from "./openai-codex-rate-limits.js"
export * from "./openrouter.js"
export * from "./perplexity.js"
export * from "./poe.js"
export * from "./qwen-code.js"
export * from "./requesty.js"
@ -36,6 +37,7 @@ import { mistralDefaultModelId } from "./mistral.js"
import { moonshotDefaultModelId } from "./moonshot.js"
import { openAiCodexDefaultModelId } from "./openai-codex.js"
import { openRouterDefaultModelId } from "./openrouter.js"
import { perplexityDefaultModelId } from "./perplexity.js"
import { poeDefaultModelId } from "./poe.js"
import { qwenCodeDefaultModelId } from "./qwen-code.js"
import { requestyDefaultModelId } from "./requesty.js"
@ -103,6 +105,8 @@ export function getProviderDefaultModelId(
return sambaNovaDefaultModelId
case "fireworks":
return fireworksDefaultModelId
case "perplexity":
return perplexityDefaultModelId
case "qwen-code":
return qwenCodeDefaultModelId
case "poe":

View file

@ -0,0 +1,50 @@
import type { ModelInfo } from "../model.js"
// Perplexity
// https://docs.perplexity.ai/docs/getting-started
// https://docs.perplexity.ai/guides/pricing
export type PerplexityModelId = keyof typeof perplexityModels
export const perplexityDefaultModelId: PerplexityModelId = "sonar-pro"
export const perplexityModels = {
sonar: {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.0,
outputPrice: 1.0,
description:
"Lightweight, cost-effective model with built-in web search grounding. Best for quick lookups and short answers.",
},
"sonar-pro": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 3.0,
outputPrice: 15.0,
description:
"Perplexity's flagship model with built-in web search grounding. Best for complex queries that benefit from up-to-date information.",
},
"sonar-reasoning": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.0,
outputPrice: 5.0,
description: "Reasoning model with chain-of-thought and built-in web search grounding.",
},
"sonar-reasoning-pro": {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 8.0,
description:
"Reasoning model with extended chain-of-thought reasoning and built-in web search grounding for complex multi-step problems.",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -30,6 +30,7 @@ import {
SambaNovaHandler,
ZAiHandler,
FireworksHandler,
PerplexityHandler,
VercelAiGatewayHandler,
MiniMaxHandler,
BasetenHandler,
@ -169,6 +170,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new ZAiHandler(options)
case "fireworks":
return new FireworksHandler(options)
case "perplexity":
return new PerplexityHandler(options)
case "vercel-ai-gateway":
return new VercelAiGatewayHandler(options)
case "minimax":

View file

@ -326,6 +326,42 @@ describe("BaseOpenAiCompatibleProvider", () => {
// Should yield reasoning with spaces (only pure whitespace is filtered)
expect(chunks).toEqual([{ type: "reasoning", text: " content with spaces " }])
})
it("should yield reasoning_content before content when both are present in a delta", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [
{
delta: {
reasoning_content: "Thinking first",
content: "Final answer",
},
},
],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks).toEqual([
{ type: "reasoning", text: "Thinking first" },
{ type: "text", text: "Final answer" },
])
})
})
describe("Basic functionality", () => {

View file

@ -0,0 +1,226 @@
// npx vitest run api/providers/__tests__/perplexity.spec.ts
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { type PerplexityModelId, perplexityDefaultModelId, perplexityModels } from "@roo-code/types"
import { PerplexityHandler, resolvePerplexityApiKey } from "../perplexity"
const mockCreate = vi.fn()
vi.mock("openai", () => ({
default: vi.fn(() => ({
chat: {
completions: {
create: mockCreate,
},
},
})),
}))
describe("PerplexityHandler", () => {
let handler: PerplexityHandler
const originalEnv = { ...process.env }
beforeEach(() => {
vi.clearAllMocks()
mockCreate.mockImplementation(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test response" }, index: 0 }],
usage: null,
}
yield {
choices: [{ delta: {}, index: 0 }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}
},
}))
handler = new PerplexityHandler({ perplexityApiKey: "test-key" })
})
afterEach(() => {
vi.restoreAllMocks()
process.env = { ...originalEnv }
})
it("should use the correct Perplexity base URL", () => {
new PerplexityHandler({ perplexityApiKey: "test-perplexity-api-key" })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.perplexity.ai" }))
})
it("should use the provided API key from settings", () => {
const perplexityApiKey = "test-perplexity-api-key"
new PerplexityHandler({ perplexityApiKey })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: perplexityApiKey }))
})
it("should fall back to PERPLEXITY_API_KEY env var when no settings key is provided", () => {
delete process.env.PPLX_API_KEY
process.env.PERPLEXITY_API_KEY = "env-perplexity-key"
new PerplexityHandler({})
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "env-perplexity-key" }))
})
it("should fall back to PPLX_API_KEY env var as a secondary fallback", () => {
delete process.env.PERPLEXITY_API_KEY
process.env.PPLX_API_KEY = "pplx-fallback-key"
new PerplexityHandler({})
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "pplx-fallback-key" }))
})
it("should fall back to PPLX_API_KEY when PERPLEXITY_API_KEY is empty", () => {
process.env.PERPLEXITY_API_KEY = ""
process.env.PPLX_API_KEY = "pplx-fallback-key"
new PerplexityHandler({})
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "pplx-fallback-key" }))
})
it("should prefer explicit settings API key over env vars", () => {
process.env.PERPLEXITY_API_KEY = "env-key"
process.env.PPLX_API_KEY = "pplx-key"
new PerplexityHandler({ perplexityApiKey: "explicit-key" })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "explicit-key" }))
})
it("should throw when no API key is configured (settings or env vars)", () => {
delete process.env.PERPLEXITY_API_KEY
delete process.env.PPLX_API_KEY
expect(() => new PerplexityHandler({})).toThrow("API key is required")
})
it("resolvePerplexityApiKey should return undefined when nothing is set", () => {
delete process.env.PERPLEXITY_API_KEY
delete process.env.PPLX_API_KEY
expect(resolvePerplexityApiKey()).toBeUndefined()
expect(resolvePerplexityApiKey("")).toBeUndefined()
})
it("should return default sonar-pro model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(perplexityDefaultModelId)
expect(model.id).toBe("sonar-pro")
expect(model.info).toEqual(expect.objectContaining(perplexityModels[perplexityDefaultModelId]))
})
it("should return sonar-reasoning-pro model when configured", () => {
const testModelId: PerplexityModelId = "sonar-reasoning-pro"
const handlerWithModel = new PerplexityHandler({
apiModelId: testModelId,
perplexityApiKey: "test-key",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(
expect.objectContaining({
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 2.0,
outputPrice: 8.0,
}),
)
})
it("should fall back to default model when an unknown model id is provided", () => {
const handlerWithModel = new PerplexityHandler({
apiModelId: "not-a-real-model",
perplexityApiKey: "test-key",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(perplexityDefaultModelId)
})
it("should expose all four Sonar models with 128k context", () => {
const expectedIds: PerplexityModelId[] = ["sonar", "sonar-pro", "sonar-reasoning", "sonar-reasoning-pro"]
for (const id of expectedIds) {
expect(perplexityModels[id]).toBeDefined()
expect(perplexityModels[id].contextWindow).toBe(128_000)
}
})
it("createMessage should yield text content from stream", async () => {
const testContent = "Streamed content from Perplexity"
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: { content: testContent } }] },
})
.mockResolvedValueOnce({ done: true }),
}),
}))
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
})
it("createMessage should pass the configured model id to the upstream client", async () => {
const modelId: PerplexityModelId = "sonar-reasoning"
const handlerWithModel = new PerplexityHandler({
apiModelId: modelId,
perplexityApiKey: "test-key",
})
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "hi" }]
const generator = handlerWithModel.createMessage("system", messages)
await generator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
stream: true,
stream_options: { include_usage: true },
messages: expect.arrayContaining([{ role: "system", content: "system" }]),
}),
undefined,
)
})
it.each(["sonar-reasoning", "sonar-reasoning-pro"] as const)(
"createMessage should omit temperature for %s",
async (modelId) => {
const handlerWithModel = new PerplexityHandler({
apiModelId: modelId,
perplexityApiKey: "test-key",
})
mockCreate.mockImplementationOnce(() => ({
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}))
const generator = handlerWithModel.createMessage("system", [{ role: "user", content: "hi" }])
await generator.next()
const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]
expect(lastCall[0]).not.toHaveProperty("temperature")
},
)
it("createMessage should propagate upstream errors", async () => {
mockCreate.mockImplementationOnce(() => {
throw new Error("upstream 401")
})
const generator = handler.createMessage("system", [{ role: "user", content: "hi" }])
await expect(generator.next()).rejects.toThrow(/upstream 401/)
})
})

View file

@ -84,12 +84,12 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
format: "openai",
}) ?? undefined
const temperature = this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
const temperature = this.getTemperature(model, info)
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model,
max_tokens,
temperature,
...(temperature !== undefined ? { temperature } : {}),
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
@ -141,12 +141,6 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
const delta = chunk.choices?.[0]?.delta
const finishReason = chunk.choices?.[0]?.finish_reason
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
yield processedChunk
}
}
if (delta) {
for (const key of ["reasoning_content", "reasoning"] as const) {
if (key in delta) {
@ -159,6 +153,12 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
}
}
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
yield processedChunk
}
}
// Emit raw tool call chunks - NativeToolCallParser handles state management
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
@ -219,6 +219,10 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
}
}
protected getTemperature(_model: ModelName, info: ModelInfo): number | undefined {
return this.options.modelTemperature ?? info.defaultTemperature ?? this.defaultTemperature
}
async completePrompt(prompt: string): Promise<string> {
const { id: modelId, info: modelInfo } = this.getModel()

View file

@ -24,6 +24,7 @@ export { VsCodeLmHandler } from "./vscode-lm"
export { XAIHandler } from "./xai"
export { ZAiHandler } from "./zai"
export { FireworksHandler } from "./fireworks"
export { PerplexityHandler } from "./perplexity"
export { VercelAiGatewayHandler } from "./vercel-ai-gateway"
export { MiniMaxHandler } from "./minimax"
export { BasetenHandler } from "./baseten"

View file

@ -0,0 +1,38 @@
import { type PerplexityModelId, perplexityDefaultModelId, perplexityModels } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
/**
* Resolves the Perplexity API key in priority order:
* 1. explicit settings value
* 2. PERPLEXITY_API_KEY env var
* 3. PPLX_API_KEY env var (fallback)
*/
export function resolvePerplexityApiKey(explicit?: string): string | undefined {
if (explicit && explicit.length > 0) {
return explicit
}
return process.env.PERPLEXITY_API_KEY || process.env.PPLX_API_KEY || undefined
}
const REASONING_MODELS = new Set<PerplexityModelId>(["sonar-reasoning", "sonar-reasoning-pro"])
export class PerplexityHandler extends BaseOpenAiCompatibleProvider<PerplexityModelId> {
constructor(options: ApiHandlerOptions) {
super({
...options,
providerName: "Perplexity",
baseURL: "https://api.perplexity.ai",
apiKey: resolvePerplexityApiKey(options.perplexityApiKey),
defaultProviderModelId: perplexityDefaultModelId,
providerModels: perplexityModels,
defaultTemperature: 0,
})
}
protected override getTemperature(model: PerplexityModelId, info = this.providerModels[model]): number | undefined {
return REASONING_MODELS.has(model) ? undefined : super.getTemperature(model, info)
}
}

View file

@ -63,6 +63,7 @@ export class ProfileValidator {
case "xai":
case "sambanova":
case "fireworks":
case "perplexity":
return profile.apiModelId
case "litellm":
return profile.litellmModelId

View file

@ -90,6 +90,7 @@ import {
XAI,
ZAi,
Fireworks,
Perplexity,
VercelAiGateway,
MiniMax,
} from "./providers"
@ -692,6 +693,13 @@ const ApiOptions = ({
/>
)}
{selectedProvider === "perplexity" && (
<Perplexity
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{selectedProvider === "poe" && (
<Poe
apiConfiguration={apiConfiguration}

View file

@ -17,6 +17,7 @@ import {
fireworksModels,
minimaxModels,
basetenModels,
perplexityModels,
} from "@roo-code/types"
export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
@ -36,6 +37,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
fireworks: fireworksModels,
minimax: minimaxModels,
baseten: basetenModels,
perplexity: perplexityModels,
}
export const PROVIDERS = [
@ -60,6 +62,7 @@ export const PROVIDERS = [
{ value: "sambanova", label: "SambaNova", proxy: false },
{ value: "zai", label: "Z.ai", proxy: false },
{ value: "fireworks", label: "Fireworks AI", proxy: false },
{ value: "perplexity", label: "Perplexity", proxy: false },
{ value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false },
{ value: "minimax", label: "MiniMax", proxy: false },
{ value: "baseten", label: "Baseten", proxy: false },

View file

@ -0,0 +1,50 @@
import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
type PerplexityProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
}
export const Perplexity = ({ apiConfiguration, setApiConfigurationField }: PerplexityProps) => {
const { t } = useAppTranslation()
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.perplexityApiKey || ""}
type="password"
onInput={handleInputChange("perplexityApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.perplexityApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.perplexityApiKey && (
<VSCodeButtonLink href="https://www.perplexity.ai/account/api/keys" appearance="secondary">
{t("settings:providers.getPerplexityApiKey")}
</VSCodeButtonLink>
)}
</>
)
}

View file

@ -21,6 +21,7 @@ export { XAI } from "./XAI"
export { ZAi } from "./ZAi"
export { LiteLLM } from "./LiteLLM"
export { Fireworks } from "./Fireworks"
export { Perplexity } from "./Perplexity"
export { VercelAiGateway } from "./VercelAiGateway"
export { MiniMax } from "./MiniMax"
export { Baseten } from "./Baseten"

View file

@ -16,6 +16,7 @@ import {
fireworksDefaultModelId,
minimaxDefaultModelId,
basetenDefaultModelId,
perplexityDefaultModelId,
} from "@roo-code/types"
import { MODELS_BY_PROVIDER } from "../constants"
@ -39,6 +40,7 @@ export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServi
sambanova: { serviceName: "SambaNova", serviceUrl: "https://sambanova.ai" },
zai: { serviceName: "Z.ai", serviceUrl: "https://z.ai" },
fireworks: { serviceName: "Fireworks AI", serviceUrl: "https://fireworks.ai" },
perplexity: { serviceName: "Perplexity", serviceUrl: "https://www.perplexity.ai/account/api/keys" },
minimax: { serviceName: "MiniMax", serviceUrl: "https://minimax.chat" },
baseten: { serviceName: "Baseten", serviceUrl: "https://baseten.co" },
ollama: { serviceName: "Ollama", serviceUrl: "https://ollama.ai" },
@ -65,6 +67,7 @@ export const PROVIDER_DEFAULT_MODEL_IDS: Partial<Record<ProviderName, string>> =
fireworks: fireworksDefaultModelId,
minimax: minimaxDefaultModelId,
baseten: basetenDefaultModelId,
perplexity: perplexityDefaultModelId,
}
export const getProviderServiceConfig = (provider: ProviderName): ProviderServiceConfig => {

View file

@ -22,6 +22,7 @@ import {
internationalZAiModels,
mainlandZAiModels,
fireworksModels,
perplexityModels,
basetenModels,
qwenCodeModels,
litellmDefaultModelInfo,
@ -312,6 +313,11 @@ function getSelectedModel({
const info = fireworksModels[id as keyof typeof fireworksModels]
return { id, info }
}
case "perplexity": {
const id = apiConfiguration.apiModelId ?? defaultModelId
const info = perplexityModels[id as keyof typeof perplexityModels]
return { id, info }
}
case "poe": {
const id = apiConfiguration.apiModelId ?? defaultModelId
const info = routerModels.poe?.[id]

View file

@ -439,6 +439,8 @@
"poeBaseUrl": "Poe Base URL",
"fireworksApiKey": "Fireworks API Key",
"getFireworksApiKey": "Get Fireworks API Key",
"perplexityApiKey": "Perplexity API Key",
"getPerplexityApiKey": "Get Perplexity API Key",
"deepSeekApiKey": "DeepSeek API Key",
"getDeepSeekApiKey": "Get DeepSeek API Key",
"moonshotApiKey": "Moonshot API Key",

View file

@ -113,6 +113,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
return i18next.t("settings:validation.apiKey")
}
break
case "perplexity":
if (!apiConfiguration.perplexityApiKey) {
return i18next.t("settings:validation.apiKey")
}
break
case "qwen-code":
if (!apiConfiguration.qwenCodeOauthPath) {
return i18next.t("settings:validation.qwenCodeOauthPath")