diff --git a/CHANGELOG.md b/CHANGELOG.md index 2404502..2dfb6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.1] - 2026-05-11 + +### Changed + +- Model selection is now controlled by `/perplexity-config` or `PI_PERPLEXITY_MODEL` instead of an LLM-facing tool parameter. +- Updated the configured model list with current Perplexity internal model slugs. + +### Added + +- Opt-in live E2E test for validating Perplexity model selection. + ## [0.2.0] - 2026-03-21 ### Added diff --git a/README.md b/README.md index da340e2..6838967 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,10 @@ Once installed, the agent automatically calls `perplexity_search` whenever it ne | `query` | string | ✅ | The search query | | `recency` | string | — | Filter by age: `hour` · `day` · `week` · `month` · `year` | | `limit` | number | — | Max sources to include (1–50) | -| `model` | string | — | Model preference, e.g. `pplx_pro_upgraded`, `pplx_pro`, `experimental`, `gpt54`, `gpt54_thinking`, `claude46sonnet`, `claude46sonnetthinking`, `gemini31pro_high`, `nv_nemotron_3_super`, `pplx_reasoning`, `pplx_alpha` | | `incognito` | boolean | — | Whether to hide the search from Perplexity history; defaults to `true` | +Model selection is configured globally with `/perplexity-config` or `PI_PERPLEXITY_MODEL`; it is not exposed as a tool parameter, so agent-generated tool calls cannot accidentally override your configured model. + ### Output format The tool returns structured text the agent can reason over: @@ -98,6 +99,13 @@ bun test # Run tests bunx tsc --noEmit # Type check ``` +Optional live model-selection E2E test (requires cached auth from `/perplexity-login`): + +```bash +PI_PERPLEXITY_E2E=1 bun test test/e2e-models.test.ts +PI_PERPLEXITY_E2E=1 PI_PERPLEXITY_E2E_MODELS=pplx_pro_upgraded,gpt54 bun test test/e2e-models.test.ts +``` + ## License MIT — see [LICENSE](LICENSE) for details. diff --git a/package.json b/package.json index de4e5b3..5a97152 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pi-perplexity", - "version": "0.2.0", + "version": "0.2.1", "description": "Perplexity web search for pi — uses your Pro/Max subscription, no API credits needed", "keywords": [ "pi-package", diff --git a/src/commands/config.ts b/src/commands/config.ts index 7f5f47e..95bbf67 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -6,20 +6,7 @@ import { saveConfig as defaultSaveConfig, type PerplexityConfig, } from "../config.js"; - -const KNOWN_MODELS: { value: string; label: string }[] = [ - { value: "pplx_pro_upgraded", label: "Best (auto)" }, - { value: "pplx_pro", label: "Default Pro" }, - { value: "experimental", label: "Sonar" }, - { value: "gpt54", label: "GPT-5.4" }, - { value: "gpt54_thinking", label: "GPT-5.4 Thinking" }, - { value: "claude46sonnet", label: "Claude 4.6 Sonnet" }, - { value: "claude46sonnetthinking", label: "Claude 4.6 Sonnet Thinking" }, - { value: "gemini31pro_high", label: "Gemini 3.1 Pro" }, - { value: "nv_nemotron_3_super", label: "Nemotron 3 Super" }, - { value: "pplx_reasoning", label: "Default Reasoning" }, - { value: "pplx_alpha", label: "Deep Research" }, -]; +import { KNOWN_MODELS } from "../search/models.js"; function formatCurrentConfig(config: { model?: string; incognito?: boolean }): string { const model = config.model ?? "pplx_pro_upgraded (default)"; diff --git a/src/index.ts b/src/index.ts index b53d916..1d2cced 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,6 @@ export default function (pi: ExtensionAPI) { limit: Type.Optional( Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }), ), - model: Type.Optional(Type.String({ description: "Model preference" })), incognito: Type.Optional(Type.Boolean({ description: "Hide search from Perplexity history" })), }), renderCall: renderPerplexityCall, @@ -76,7 +75,6 @@ export default function (pi: ExtensionAPI) { const config = await loadConfig(); const { model, incognito } = resolveSearchDefaults( { - ...(params.model !== undefined ? { model: params.model } : {}), ...(params.incognito !== undefined ? { incognito: params.incognito } : {}), }, config, diff --git a/src/search/models.ts b/src/search/models.ts new file mode 100644 index 0000000..07e97f6 --- /dev/null +++ b/src/search/models.ts @@ -0,0 +1,36 @@ +export interface ModelOption { + value: string; + label: string; +} + +export const KNOWN_MODELS: ModelOption[] = [ + { value: "pplx_pro_upgraded", label: "Pro (auto)" }, + { value: "pplx_pro", label: "Best (auto)" }, + { value: "experimental", label: "Sonar" }, + { value: "pplx_reasoning", label: "Reasoning" }, + { value: "pplx_alpha", label: "Deep Research" }, + { value: "gpt54", label: "GPT-5.4" }, + { value: "gpt54_thinking", label: "GPT-5.4 Thinking" }, + { value: "gpt52", label: "GPT-5.2" }, + { value: "gpt52_thinking", label: "GPT-5.2 Thinking" }, + { value: "gpt52_pro", label: "GPT-5.2 Pro" }, + { value: "gpt51", label: "GPT-5.1" }, + { value: "gpt51_thinking", label: "GPT-5.1 Thinking" }, + { value: "claude46sonnet", label: "Claude 4.6 Sonnet" }, + { value: "claude46sonnetthinking", label: "Claude 4.6 Sonnet Thinking" }, + { value: "claude47opus", label: "Claude 4.7 Opus" }, + { value: "claude47opusthinking", label: "Claude 4.7 Opus Thinking" }, + { value: "claude45sonnet", label: "Claude 4.5 Sonnet" }, + { value: "claude45sonnetthinking", label: "Claude 4.5 Sonnet Thinking" }, + { value: "gemini31pro_low", label: "Gemini 3.1 Pro" }, + { value: "gemini31pro_high", label: "Gemini 3.1 Pro Thinking" }, + { value: "gemini30pro", label: "Gemini 3 Pro" }, + { value: "gemini30flash", label: "Gemini 3 Flash" }, + { value: "gemini30flash_high", label: "Gemini 3 Flash Thinking" }, + { value: "grok41nonreasoning", label: "Grok 4.1" }, + { value: "grok41reasoning", label: "Grok 4.1 Reasoning" }, + { value: "grok4nonthinking", label: "Grok 4" }, + { value: "grok4", label: "Grok 4 Thinking" }, + { value: "nv_nemotron_3_super", label: "Nemotron 3 Super" }, + { value: "kimik25thinking", label: "Kimi K2.5 Thinking" }, +]; diff --git a/test/e2e-models.test.ts b/test/e2e-models.test.ts new file mode 100644 index 0000000..1d9fd14 --- /dev/null +++ b/test/e2e-models.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from "bun:test"; + +import { loadToken } from "../src/auth/storage.js"; +import { searchPerplexity } from "../src/search/client.js"; + +const DEFAULT_MODELS = [ + "pplx_pro_upgraded", + "experimental", + "pplx_reasoning", + "gpt54", + "claude46sonnetthinking", +]; + +const runE2E = process.env.PI_PERPLEXITY_E2E === "1"; +const maybeTest = runE2E ? test : test.skip; + +function configuredModels(): string[] { + const raw = process.env.PI_PERPLEXITY_E2E_MODELS; + if (!raw) return DEFAULT_MODELS; + return raw + .split(",") + .map((model) => model.trim()) + .filter(Boolean); +} + +function configuredDelayMs(): number { + const raw = Number(process.env.PI_PERPLEXITY_E2E_DELAY_MS ?? "1500"); + return Number.isFinite(raw) && raw >= 0 ? raw : 1500; +} + +async function delay(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("Perplexity model selection e2e", () => { + maybeTest( + "sends requested model slugs and receives matching display_model values", + async () => { + const token = await loadToken(); + if (!token) { + throw new Error("No cached Perplexity token. Run /perplexity-login before PI_PERPLEXITY_E2E=1 bun test."); + } + + const models = configuredModels(); + const delayMs = configuredDelayMs(); + + for (const model of models) { + const result = await searchPerplexity( + { + query: "Say exactly OK", + model, + incognito: true, + limit: 1, + }, + token.access, + ); + + expect(result.answer.trim().startsWith("OK")).toBe(true); + expect(result.displayModel).toBe(model); + + if (delayMs > 0 && model !== models.at(-1)) { + await delay(delayMs); + } + } + }, + { timeout: 180_000 }, + ); +}); diff --git a/test/index-execute.test.ts b/test/index-execute.test.ts index ff21eac..1be497a 100644 --- a/test/index-execute.test.ts +++ b/test/index-execute.test.ts @@ -28,19 +28,28 @@ describe("perplexity_search execute", () => { const { default: registerExtension } = await import(`../src/index.ts?test=${crypto.randomUUID()}`); let execute: ((toolCallId: string, params: any, signal?: AbortSignal, onUpdate?: any, ctx?: any) => Promise) | undefined; + let parameters: unknown; registerExtension({ registerCommand() { return undefined; }, - registerTool(tool: { execute: typeof execute }) { + registerTool(tool: { execute: typeof execute; parameters: unknown }) { execute = tool.execute; + parameters = tool.parameters; }, } as any); expect(execute).toBeDefined(); + expect(JSON.stringify(parameters)).not.toContain("model"); - const result = await execute!("tool-1", { query: "how many planets" }, undefined, undefined, { ui: {} }); + const result = await execute!( + "tool-1", + { query: "how many planets", model: "pplx_pro" }, + undefined, + undefined, + { ui: {} }, + ); expect(loadConfig).toHaveBeenCalledTimes(1); expect(resolveSearchDefaults).toHaveBeenCalledWith({}, { model: "gpt54", incognito: false });