diff --git a/.changeset/add-perplexity-plugin.md b/.changeset/add-perplexity-plugin.md new file mode 100644 index 000000000..0ced67c0d --- /dev/null +++ b/.changeset/add-perplexity-plugin.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-perplexity': patch +--- + +Add a Perplexity LLM plugin backed by the OpenAI-compatible chat completions transport. diff --git a/REUSE.toml b/REUSE.toml index b2cb5c4a4..d2a802cee 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -54,3 +54,9 @@ SPDX-License-Identifier = "Apache-2.0" path = ["**/README.md"] SPDX-FileCopyrightText = "2026 LiveKit, Inc." SPDX-License-Identifier = "Apache-2.0" + +# API Extractor reports +[[annotations]] +path = ["**/etc/*.api.md"] +SPDX-FileCopyrightText = "2026 LiveKit, Inc." +SPDX-License-Identifier = "Apache-2.0" diff --git a/plugins/perplexity/README.md b/plugins/perplexity/README.md new file mode 100644 index 000000000..29a8b2849 --- /dev/null +++ b/plugins/perplexity/README.md @@ -0,0 +1,30 @@ +# Perplexity plugin for LiveKit Node Agents + +Support for [Perplexity](https://www.perplexity.ai/) LLMs via the OpenAI-compatible +chat completions endpoint at `https://api.perplexity.ai`. + +## Installation + +```bash +pnpm add @livekit/agents-plugin-perplexity +``` + +## Pre-requisites + +You'll need an API key from Perplexity. It can be passed directly or set as the +`PERPLEXITY_API_KEY` environment variable. + +## Usage + +```ts +import * as perplexity from '@livekit/agents-plugin-perplexity'; + +const llm = new perplexity.LLM({ + model: 'sonar-pro', + // apiKey picked up from PERPLEXITY_API_KEY if omitted +}); +``` + +The plugin reuses the OpenAI plugin's chat completions transport with +`baseURL: 'https://api.perplexity.ai'` and forwards an `X-Pplx-Integration` +attribution header on every outgoing request. diff --git a/plugins/perplexity/api-extractor.json b/plugins/perplexity/api-extractor.json new file mode 100644 index 000000000..32c90f0fa --- /dev/null +++ b/plugins/perplexity/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor-shared.json", + "mainEntryPointFilePath": "./dist/index.d.ts" +} diff --git a/plugins/perplexity/etc/agents-plugin-perplexity.api.md b/plugins/perplexity/etc/agents-plugin-perplexity.api.md new file mode 100644 index 000000000..4a92357d6 --- /dev/null +++ b/plugins/perplexity/etc/agents-plugin-perplexity.api.md @@ -0,0 +1,52 @@ +## API Report File for "@livekit/agents-plugin-perplexity" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { llm } from '@livekit/agents'; +import { LLM as LLM_2 } from '@livekit/agents-plugin-openai'; +import OpenAI from 'openai'; + +// @public (undocumented) +export class LLM extends LLM_2 { + constructor(opts?: Partial); + // (undocumented) + chat(opts: Parameters[0]): ReturnType; + // (undocumented) + label(): string; + // (undocumented) + get provider(): string; +} + +// @public (undocumented) +export interface LLMOptions { + // (undocumented) + apiKey?: string; + // (undocumented) + baseURL?: string; + // (undocumented) + client?: OpenAI; + // (undocumented) + model: string | PerplexityChatModels; + // (undocumented) + parallelToolCalls?: boolean; + // (undocumented) + temperature?: number; + // (undocumented) + toolChoice?: llm.ToolChoice; + // (undocumented) + topP?: number; + // (undocumented) + user?: string; +} + +// @public (undocumented) +export const PERPLEXITY_BASE_URL = "https://api.perplexity.ai"; + +// @public (undocumented) +export type PerplexityChatModels = 'sonar-pro'; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/plugins/perplexity/package.json b/plugins/perplexity/package.json new file mode 100644 index 000000000..118996578 --- /dev/null +++ b/plugins/perplexity/package.json @@ -0,0 +1,52 @@ +{ + "name": "@livekit/agents-plugin-perplexity", + "version": "1.4.1", + "description": "Perplexity plugin for LiveKit Node Agents", + "main": "dist/index.js", + "require": "dist/index.cjs", + "types": "dist/index.d.ts", + "exports": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "author": "LiveKit", + "type": "module", + "repository": "git@github.com:livekit/agents-js.git", + "license": "Apache-2.0", + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "tsup --onSuccess \"pnpm build:types\"", + "build:types": "tsc --declaration --emitDeclarationOnly && node ../../scripts/copyDeclarationOutput.js", + "clean": "rm -rf dist", + "clean:build": "pnpm clean && pnpm build", + "lint": "eslint -f unix \"src/**/*.{ts,js}\"", + "api:check": "api-extractor run --typescript-compiler-folder ../../node_modules/typescript", + "api:update": "api-extractor run --local --typescript-compiler-folder ../../node_modules/typescript --verbose" + }, + "devDependencies": { + "@livekit/agents": "workspace:*", + "@livekit/agents-plugin-openai": "workspace:*", + "@livekit/rtc-node": "catalog:", + "@microsoft/api-extractor": "^7.35.0", + "tsup": "^8.3.5", + "typescript": "^5.0.0" + }, + "dependencies": { + "openai": "^6.8.1" + }, + "peerDependencies": { + "@livekit/agents": "workspace:*", + "@livekit/agents-plugin-openai": "workspace:*", + "@livekit/rtc-node": "catalog:" + } +} diff --git a/plugins/perplexity/src/index.ts b/plugins/perplexity/src/index.ts new file mode 100644 index 000000000..61bd04526 --- /dev/null +++ b/plugins/perplexity/src/index.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { Plugin } from '@livekit/agents'; + +export { LLM, PERPLEXITY_BASE_URL } from './llm.js'; +export type { LLMOptions } from './llm.js'; +export type { PerplexityChatModels } from './models.js'; + +class PerplexityPlugin extends Plugin { + constructor() { + super({ + title: 'perplexity', + version: __PACKAGE_VERSION__, + package: __PACKAGE_NAME__, + }); + } +} + +Plugin.registerPlugin(new PerplexityPlugin()); diff --git a/plugins/perplexity/src/llm.test.ts b/plugins/perplexity/src/llm.test.ts new file mode 100644 index 000000000..032cd8601 --- /dev/null +++ b/plugins/perplexity/src/llm.test.ts @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import { llm } from '@livekit/agents'; +import OpenAI from 'openai'; +import { afterEach, describe, expect, it } from 'vitest'; +import { LLM, PERPLEXITY_BASE_URL } from './llm.js'; + +describe('Perplexity LLM', () => { + const originalApiKey = process.env.PERPLEXITY_API_KEY; + + afterEach(() => { + if (originalApiKey === undefined) { + delete process.env.PERPLEXITY_API_KEY; + } else { + process.env.PERPLEXITY_API_KEY = originalApiKey; + } + }); + + it('uses the default model and base URL', () => { + process.env.PERPLEXITY_API_KEY = 'test-key'; + const model = new LLM(); + + expect(model.model).toBe('sonar-pro'); + expect(PERPLEXITY_BASE_URL).toBe('https://api.perplexity.ai'); + }); + + it('attaches the attribution header on chat requests', async () => { + let capturedHeaders: Record = {}; + const client = new OpenAI({ apiKey: 'test-key', baseURL: PERPLEXITY_BASE_URL }); + client.chat.completions.create = (async (_body: unknown, options?: unknown) => { + capturedHeaders = + (options as { headers?: Record } | undefined)?.headers ?? {}; + + return { + async *[Symbol.asyncIterator]() { + // no-op + }, + }; + }) as unknown as typeof client.chat.completions.create; + + const stream = new LLM({ client }).chat({ chatCtx: new llm.ChatContext() }); + for await (const _chunk of stream) { + void _chunk; + } + + expect(capturedHeaders['X-Pplx-Integration']).toBe(`livekit-agents/${__PACKAGE_VERSION__}`); + }); + + it('throws when the API key is missing', () => { + delete process.env.PERPLEXITY_API_KEY; + + expect(() => new LLM()).toThrow('PERPLEXITY_API_KEY'); + }); + + it('sets the provider name', () => { + process.env.PERPLEXITY_API_KEY = 'test-key'; + + expect(new LLM().provider).toBe('Perplexity'); + }); +}); diff --git a/plugins/perplexity/src/llm.ts b/plugins/perplexity/src/llm.ts new file mode 100644 index 000000000..ec590e9c7 --- /dev/null +++ b/plugins/perplexity/src/llm.ts @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +import type { llm } from '@livekit/agents'; +import { LLM as OpenAILLM } from '@livekit/agents-plugin-openai'; +import OpenAI from 'openai'; +import type { PerplexityChatModels } from './models.js'; + +/** @public */ +export const PERPLEXITY_BASE_URL = 'https://api.perplexity.ai'; + +/** @public */ +export interface LLMOptions { + model: string | PerplexityChatModels; + apiKey?: string; + baseURL?: string; + client?: OpenAI; + user?: string; + temperature?: number; + topP?: number; + toolChoice?: llm.ToolChoice; + parallelToolCalls?: boolean; +} + +const defaultLLMOptions: LLMOptions = { + model: 'sonar-pro', + baseURL: PERPLEXITY_BASE_URL, +}; + +/** @public */ +export class LLM extends OpenAILLM { + #topP?: number; + #extraHeaders = { + 'X-Pplx-Integration': `livekit-agents/${__PACKAGE_VERSION__}`, + }; + + constructor(opts: Partial = {}) { + const merged = { ...defaultLLMOptions, ...opts }; + const apiKey = merged.apiKey ?? process.env.PERPLEXITY_API_KEY; + + if (!apiKey && !merged.client) { + throw new Error( + 'Perplexity API key is required, either as an argument or as $PERPLEXITY_API_KEY', + ); + } + + super({ + ...merged, + apiKey, + client: + merged.client ?? + new OpenAI({ + apiKey, + baseURL: merged.baseURL, + }), + strictToolSchema: false, + }); + + this.#topP = merged.topP; + } + + override label(): string { + return 'perplexity.LLM'; + } + + override get provider(): string { + return 'Perplexity'; + } + + override chat(opts: Parameters[0]): ReturnType { + const extraKwargs = { ...opts.extraKwargs }; + if (this.#topP !== undefined) { + extraKwargs.top_p = this.#topP; + } + + extraKwargs.extra_headers = { + ...((extraKwargs.extra_headers as Record | undefined) ?? {}), + ...this.#extraHeaders, + }; + + return super.chat({ ...opts, extraKwargs }); + } +} diff --git a/plugins/perplexity/src/models.ts b/plugins/perplexity/src/models.ts new file mode 100644 index 000000000..213029757 --- /dev/null +++ b/plugins/perplexity/src/models.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2026 LiveKit, Inc. +// +// SPDX-License-Identifier: Apache-2.0 + +/** @public */ +export type PerplexityChatModels = 'sonar-pro'; diff --git a/plugins/perplexity/tsconfig.json b/plugins/perplexity/tsconfig.json new file mode 100644 index 000000000..14ecaae62 --- /dev/null +++ b/plugins/perplexity/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "rootDir": "./src", + "declarationDir": "./dist", + "outDir": "./dist" + }, + "typedocOptions": { + "name": "plugins/agents-plugin-perplexity", + "entryPointStrategy": "resolve", + "entryPoints": ["src/index.ts"] + } +} diff --git a/plugins/perplexity/tsup.config.ts b/plugins/perplexity/tsup.config.ts new file mode 100644 index 000000000..b491713a4 --- /dev/null +++ b/plugins/perplexity/tsup.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'tsup'; +import defaults from '../../tsup.config'; + +export default defineConfig({ + ...defaults, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6e63cef8..304fe2bc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1020,6 +1020,31 @@ importers: specifier: ^5.0.0 version: 5.9.3 + plugins/perplexity: + dependencies: + openai: + specifier: ^6.8.1 + version: 6.8.1(ws@8.20.0)(zod@4.3.6) + devDependencies: + '@livekit/agents': + specifier: workspace:* + version: link:../../agents + '@livekit/agents-plugin-openai': + specifier: workspace:* + version: link:../openai + '@livekit/rtc-node': + specifier: 'catalog:' + version: 0.13.27 + '@microsoft/api-extractor': + specifier: ^7.35.0 + version: 7.43.7(@types/node@25.6.0) + tsup: + specifier: ^8.3.5 + version: 8.4.0(@microsoft/api-extractor@7.43.7(@types/node@25.6.0))(postcss@8.5.9)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.0.0 + version: 5.9.3 + plugins/phonic: dependencies: phonic: