From perplexity-pack
Sets up local dev environment for Perplexity Sonar API with TypeScript OpenAI client wrapper, Vitest testing, mocking fixtures, and hot reload for fast iteration.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin perplexity-packThis skill is limited to using the following tools:
Set up a fast, cost-effective local development workflow for Perplexity Sonar API. Key challenge: every real API call performs a web search and costs money, so mocking and caching are essential for development.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Set up a fast, cost-effective local development workflow for Perplexity Sonar API. Key challenge: every real API call performs a web search and costs money, so mocking and caching are essential for development.
perplexity-install-auth setupvitest for testingmy-perplexity-project/
├── src/
│ ├── perplexity/
│ │ ├── client.ts # OpenAI client wrapper for Perplexity
│ │ ├── search.ts # Search functions with citation handling
│ │ └── types.ts # Response type extensions
│ └── index.ts
├── tests/
│ ├── fixtures/ # Saved API responses for mocking
│ │ └── sonar-response.json
│ ├── perplexity.test.ts
│ └── setup.ts
├── .env.local # API key (git-ignored)
├── .env.example # Template
└── package.json
// src/perplexity/client.ts
import OpenAI from "openai";
export interface PerplexityResponse extends OpenAI.ChatCompletion {
citations?: string[];
search_results?: Array<{
title: string;
url: string;
snippet: string;
}>;
related_questions?: string[];
}
export type PerplexityModel = "sonar" | "sonar-pro" | "sonar-reasoning-pro" | "sonar-deep-research";
export function createClient(apiKey?: string): OpenAI {
return new OpenAI({
apiKey: apiKey || process.env.PERPLEXITY_API_KEY,
baseURL: "https://api.perplexity.ai",
});
}
export async function search(
client: OpenAI,
query: string,
opts: {
model?: PerplexityModel;
systemPrompt?: string;
maxTokens?: number;
searchRecencyFilter?: "hour" | "day" | "week" | "month";
searchDomainFilter?: string[];
} = {}
): Promise<PerplexityResponse> {
const response = await client.chat.completions.create({
model: opts.model || "sonar",
messages: [
...(opts.systemPrompt
? [{ role: "system" as const, content: opts.systemPrompt }]
: []),
{ role: "user" as const, content: query },
],
max_tokens: opts.maxTokens,
...(opts.searchRecencyFilter && { search_recency_filter: opts.searchRecencyFilter }),
...(opts.searchDomainFilter && { search_domain_filter: opts.searchDomainFilter }),
} as any);
return response as unknown as PerplexityResponse;
}
// scripts/capture-fixture.ts
import { createClient, search } from "../src/perplexity/client";
import { writeFileSync } from "fs";
async function captureFixture() {
const client = createClient();
const response = await search(client, "What is TypeScript 5.5?");
writeFileSync(
"tests/fixtures/sonar-response.json",
JSON.stringify(response, null, 2)
);
console.log("Fixture saved with", (response.citations || []).length, "citations");
}
captureFixture();
// tests/setup.ts
import { vi } from "vitest";
import fixture from "./fixtures/sonar-response.json";
export function mockPerplexityClient() {
return {
chat: {
completions: {
create: vi.fn().mockResolvedValue(fixture),
},
},
};
}
// tests/perplexity.test.ts
import { describe, it, expect } from "vitest";
import { mockPerplexityClient } from "./setup";
import { search } from "../src/perplexity/client";
describe("Perplexity Search", () => {
it("returns answer with citations", async () => {
const client = mockPerplexityClient() as any;
const result = await search(client, "test query");
expect(result.choices[0].message.content).toBeDefined();
expect(result.citations).toBeDefined();
expect(result.citations!.length).toBeGreaterThan(0);
});
it.skipIf(!process.env.PERPLEXITY_API_KEY)(
"live API returns citations",
async () => {
const { createClient, search } = await import("../src/perplexity/client");
const client = createClient();
const result = await search(client, "What is Node.js?", {
model: "sonar",
maxTokens: 100,
});
expect(result.citations!.length).toBeGreaterThan(0);
}
);
});
{
"scripts": {
"dev": "tsx watch src/index.ts",
"test": "vitest",
"test:watch": "vitest --watch",
"test:live": "PERPLEXITY_API_KEY=$PERPLEXITY_API_KEY vitest --run",
"capture-fixtures": "tsx scripts/capture-fixture.ts"
}
}
| Error | Cause | Solution |
|---|---|---|
| Fixture missing | Never captured | Run npm run capture-fixtures once |
| Tests hit real API | Missing mock | Ensure mock client is injected |
| Stale fixtures | API response format changed | Re-capture fixtures |
| High dev costs | Making live calls in loop | Use fixtures; reserve live calls for CI |
See perplexity-sdk-patterns for production-ready code patterns.