Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-perplexity-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-perplexity': patch
---

Add a Perplexity LLM plugin backed by the OpenAI-compatible chat completions transport.
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
30 changes: 30 additions & 0 deletions plugins/perplexity/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions plugins/perplexity/api-extractor.json
Original file line number Diff line number Diff line change
@@ -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"
}
52 changes: 52 additions & 0 deletions plugins/perplexity/etc/agents-plugin-perplexity.api.md
Original file line number Diff line number Diff line change
@@ -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<LLMOptions>);
// (undocumented)
chat(opts: Parameters<LLM_2['chat']>[0]): ReturnType<LLM_2['chat']>;
// (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)

```
52 changes: 52 additions & 0 deletions plugins/perplexity/package.json
Original file line number Diff line number Diff line change
@@ -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:"
}
}
20 changes: 20 additions & 0 deletions plugins/perplexity/src/index.ts
Original file line number Diff line number Diff line change
@@ -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());
61 changes: 61 additions & 0 deletions plugins/perplexity/src/llm.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {};
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<string, string> } | 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');
});
});
83 changes: 83 additions & 0 deletions plugins/perplexity/src/llm.ts
Original file line number Diff line number Diff line change
@@ -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<LLMOptions> = {}) {
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<OpenAILLM['chat']>[0]): ReturnType<OpenAILLM['chat']> {
const extraKwargs = { ...opts.extraKwargs };
if (this.#topP !== undefined) {
extraKwargs.top_p = this.#topP;
}

extraKwargs.extra_headers = {
...((extraKwargs.extra_headers as Record<string, string> | undefined) ?? {}),
...this.#extraHeaders,
};

return super.chat({ ...opts, extraKwargs });
}
}
6 changes: 6 additions & 0 deletions plugins/perplexity/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0

/** @public */
export type PerplexityChatModels = 'sonar-pro';
14 changes: 14 additions & 0 deletions plugins/perplexity/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
6 changes: 6 additions & 0 deletions plugins/perplexity/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'tsup';
import defaults from '../../tsup.config';

export default defineConfig({
...defaults,
});
25 changes: 25 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading