mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-22 00:31:46 +00:00
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:
parent
b867ec9145
commit
16e37607d0
19 changed files with 464 additions and 8 deletions
|
|
@ -275,6 +275,7 @@ export const SECRET_STATE_KEYS = [
|
|||
"sambaNovaApiKey",
|
||||
"zaiApiKey",
|
||||
"fireworksApiKey",
|
||||
"perplexityApiKey",
|
||||
"vercelAiGatewayApiKey",
|
||||
"basetenApiKey",
|
||||
] as const
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
50
packages/types/src/providers/perplexity.ts
Normal file
50
packages/types/src/providers/perplexity.ts
Normal 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>
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
226
src/api/providers/__tests__/perplexity.spec.ts
Normal file
226
src/api/providers/__tests__/perplexity.spec.ts
Normal 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/)
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
38
src/api/providers/perplexity.ts
Normal file
38
src/api/providers/perplexity.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ export class ProfileValidator {
|
|||
case "xai":
|
||||
case "sambanova":
|
||||
case "fireworks":
|
||||
case "perplexity":
|
||||
return profile.apiModelId
|
||||
case "litellm":
|
||||
return profile.litellmModelId
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
50
webview-ui/src/components/settings/providers/Perplexity.tsx
Normal file
50
webview-ui/src/components/settings/providers/Perplexity.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue