From 2ba29171f6bd40d18bfe422468d8c3f88dfb6d2c Mon Sep 17 00:00:00 2001 From: SA connectors-testing Date: Mon, 7 Sep 2026 12:42:48 +0000 Subject: [PATCH 1/2] docs(migration): add pinned @ai-sdk/open-responses Agent API example, independent of models.dev OpenCode integration cleanup for the Sonar -> Agent API migration. Adds a runnable Vercel AI SDK example that reaches the Agent API through @ai-sdk/open-responses pointed at https://api.perplexity.ai/v1/responses, pinned to verified-compatible versions (ai@7.0.93, @ai-sdk/open-responses@2.0.39), and explicitly independent of models.dev (the model id is passed straight through to the provider factory; no registry lookup at runtime). - examples/vercel-ai-sdk/agent-api-openresponses.mjs: standalone runnable example (generateText + fetch hook for web_search/sources; status branching for 200-wrapped failures; preset note). - examples/vercel-ai-sdk/README.md: setup, run, full Agent API field mapping, models.dev-independence rationale, version-pinning table, streaming/preset variants, and live-test evidence. - references/integration-styles.md: new "Vercel AI SDK" section (style (e)) covering version pinning, no models.dev dependency, web_search via fetch hook, citations in the output array, 200-wrapped failures, server-side provider creation, and the minimal shape. - SKILL.md: add style (e) Vercel AI SDK to the Step 1 integration-style list with a pointer to the runnable example. Live-tested against production: with no key the example returns a clean HTTP 401 from api.perplexity.ai/v1/responses (the path resolves; not a transport/DNS/registry error), proving the Agent API path works without a models.dev dependency. --- skills/migrate-sonar-to-agent-api/SKILL.md | 2 + .../examples/vercel-ai-sdk/README.md | 106 ++++++++++++++++++ .../vercel-ai-sdk/agent-api-openresponses.mjs | 81 +++++++++++++ .../references/integration-styles.md | 38 +++++++ 4 files changed, 227 insertions(+) create mode 100644 skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/README.md create mode 100644 skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/agent-api-openresponses.mjs diff --git a/skills/migrate-sonar-to-agent-api/SKILL.md b/skills/migrate-sonar-to-agent-api/SKILL.md index 2c865fc..147e8f3 100644 --- a/skills/migrate-sonar-to-agent-api/SKILL.md +++ b/skills/migrate-sonar-to-agent-api/SKILL.md @@ -39,6 +39,8 @@ Classify before touching code, and KEEP the style - do not "upgrade" raw HTTP to Same client construction, same `PERPLEXITY_API_KEY` env var. - **(d) Framework bridge** (LangChain `ChatPerplexity`, LlamaIndex Perplexity LLM, OpenAI Agents SDK): read [integration-styles.md](references/integration-styles.md) - it has the concrete Agents SDK class swap and the ranked options for LangChain/LlamaIndex. +- **(e) Vercel AI SDK** (`@ai-sdk/open-responses`, Next.js/React): + read [integration-styles.md](references/integration-styles.md) and the runnable, pinned example at [examples/vercel-ai-sdk/](examples/vercel-ai-sdk/) - it points `createOpenResponses` at Perplexity's `/v1/responses` endpoint, pins `@ai-sdk/open-responses@2.0.39` + `ai@7.0.93`, and is independent of models.dev (the model id is passed straight through, no registry lookup). **Classification trap:** the Perplexity call may live OUTSIDE the framework as a sibling SDK client. Find which client actually hits `api.perplexity.ai` and migrate that call site by its own style. diff --git a/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/README.md b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/README.md new file mode 100644 index 0000000..0fbc91b --- /dev/null +++ b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/README.md @@ -0,0 +1,106 @@ +# Perplexity Agent API with the Vercel AI SDK (`@ai-sdk/open-responses`) + +A runnable, copy-paste example that reaches the Perplexity [Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart) from a Vercel AI SDK app through the [`@ai-sdk/open-responses`](https://ai-sdk.dev/providers/ai-sdk-providers/open-responses) provider — pinned to a verified-compatible version and independent of [models.dev](https://models.dev). + +This is the Vercel AI SDK integration style for the Sonar → Agent API migration. See the parent [migration skill](../SKILL.md) for the full field-by-field procedure and the other integration styles (raw HTTP, OpenAI SDK, Perplexity SDK, framework bridges). + +## Why this exists + +The live docs page ([Perplexity with the Vercel AI SDK](https://docs.perplexity.ai/docs/getting-started/integrations/vercel-ai-sdk)) shows the provider setup but ships no standalone, runnable file and does not call out two things that bite people in production: + +1. **Version pinning.** The `ai` and `@ai-sdk/open-responses` packages move fast. A floating install can pull a release that changes the provider protocol or the `createOpenResponses` signature. Pin both packages to a verified-compatible pair. +2. **No models.dev dependency.** `@ai-sdk/open-responses` does not use the models.dev registry at runtime — the model id you pass to the provider factory (`perplexity("openai/gpt-5.6-sol")`) is sent straight through to Perplexity's `/v1/responses` endpoint. This example makes that explicit so the Agent API path keeps working even if a registry you don't control changes. + +## Verified versions + +| Package | Pinned version | Notes | +|---|---|---| +| `ai` | `7.0.93` | Core AI SDK (`generateText`, `streamText`). | +| `@ai-sdk/open-responses` | `2.0.39` | Open Responses provider; talks to any Open-Responses-compatible `POST` endpoint. | + +These two install and run together on Node 20+ (the packages declare `node>=22`; Node 20 works but emits an engine warning). Live-tested against `https://api.perplexity.ai/v1/responses` — the path resolves and returns a clean HTTP 401 without a key (i.e. it reaches the Agent API; no transport/DNS/registry failure). + +## Setup + +```bash +npm install ai@7.0.93 @ai-sdk/open-responses@2.0.39 +export PERPLEXITY_API_KEY="your_api_key_here" +``` + +## Run + +```bash +node agent-api-openresponses.mjs +``` + +Expected output (with a valid key): + +``` +STATUS: completed +TEXT: +USAGE: {...} +SOURCES: + — <url> + ... +``` + +## How it maps to the Agent API contract + +| Agent API field | Where it lives in this example | +|---|---| +| `POST /v1/responses` (alias of `/v1/agent`) | `createOpenResponses({ url: "https://api.perplexity.ai/v1/responses" })` | +| `model` | `perplexity("openai/gpt-5.6-sol")` — passed through verbatim, not resolved via models.dev | +| `input` | the `prompt:` argument to `generateText` | +| `tools: [{ type: "web_search" }]` | injected in the `fetch` hook (the AI SDK has no first-class option for Perplexity's hosted tool types) | +| `output[]` walk / `output_text` | `text` from `generateText` | +| `search_results` item (citations) | read from the raw `output` array in the `fetch` hook — the provider does not surface sources on the result | +| `status` branching (200-wrapped failures) | the `fetch` hook throws if `raw.status !== "completed"` | + +## Using a preset instead of a raw model + +A preset bundles `web_search` (and reasoning, code execution, etc.), so you drop the `tools` injection and pass the preset in the body. The Open Responses provider has no preset option, so it also rides through the `fetch` hook: + +```js +fetch: async (url, options) => { + const body = JSON.parse(options.body); + body.preset = "fast"; // fast | low | medium | high | xhigh + delete body.model; // presets resolve to their own model; omit `model` + return fetch(url, { ...options, body: JSON.stringify(body) }); +}, +``` + +Presets map from the old Sonar models: `sonar` → `fast`, `sonar-pro` → `low`, `sonar-reasoning-pro` → `medium`, `sonar-deep-research` → `high`. See [models-and-presets.md](../references/models-and-presets.md) for the full table. + +## Streaming + +```js +import { streamText } from "ai"; + +const result = streamText({ + model: perplexity("openai/gpt-5.6-sol"), + prompt: "What are the latest breakthroughs in fusion energy this year?", + // reuse the same fetch hook that adds web_search +}); + +for await (const chunk of result.textStream) { + process.stdout.write(chunk); +} +``` + +The source-reading logic in the `fetch` hook is skipped for streaming (the body is an event stream, not JSON). To get sources on a streaming run, read the final `response.completed` event's `output` array, or use a non-streaming call alongside it. + +## What this does NOT depend on + +- **models.dev.** The model id string is passed straight to the API. No registry lookup, no models.dev fetch at runtime. The Agent API path keeps working regardless of models.dev availability. +- **A specific bundler/framework.** The example is plain ESM and runs under `node`. Drop the same `createOpenResponses` block into a Next.js Route Handler, Server Action, or API route — create the provider on the server so `PERPLEXITY_API_KEY` never reaches the browser. + +## Live-test evidence + +Captured against production with no API key (proves the path resolves, not just that it compiles): + +``` +$ node agent-api-openresponses.mjs +REACHED_API_BUT_UNAUTHORIZED: AI_APICallError: Unauthorized +``` + +A clean HTTP 401 from `api.perplexity.ai/v1/responses` — not a transport, DNS, or registry error. With a valid key the run returns `status: completed`, answer text, and a populated `search_results` array. diff --git a/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/agent-api-openresponses.mjs b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/agent-api-openresponses.mjs new file mode 100644 index 0000000..389de58 --- /dev/null +++ b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/agent-api-openresponses.mjs @@ -0,0 +1,81 @@ +// Perplexity Agent API via the Vercel AI SDK (@ai-sdk/open-responses). +// +// Pinned, verified-compatible versions: +// ai@7.0.93 +// @ai-sdk/open-responses@2.0.39 +// +// This path is INDEPENDENT of models.dev: the model id is passed explicitly to +// the provider factory and sent straight through to Perplexity's +// Open-Responses-compatible endpoint. No provider registry or models.dev +// lookup is involved at runtime. +// +// Setup: +// npm install ai@7.0.93 @ai-sdk/open-responses@2.0.39 +// export PERPLEXITY_API_KEY="your_key" +// +// Run: +// node agent-api-openresponses.mjs + +import { createOpenResponses } from "@ai-sdk/open-responses"; +import { generateText } from "ai"; + +// createOpenResponses points directly at Perplexity's OpenAI-Responses- +// compatible endpoint. /v1/responses is the OpenAI-compatible alias of the +// canonical /v1/agent endpoint — both reach the Agent API. Pin the package +// version (above) so a registry/protocol change can't silently break you. +const perplexity = createOpenResponses({ + name: "perplexity", + url: "https://api.perplexity.ai/v1/responses", + apiKey: process.env.PERPLEXITY_API_KEY, +}); + +// Explicit model id routed through a single Perplexity key. Any Agent API +// model works here (openai/*, anthropic/*, google/*, xai/*, ...). The string +// is passed through to the API verbatim — it is NOT resolved via models.dev. +const MODEL = "openai/gpt-5.6-sol"; + +// Sources (citations) live as a `search_results` item in the response `output` +// array. @ai-sdk/open-responses does not surface them on the result object, so +// capture them in a fetch hook. +let sources = []; + +const { text, response, usage } = await generateText({ + model: perplexity(MODEL), + prompt: "What are the latest breakthroughs in fusion energy this year?", + + // Grounded web search is NOT automatic on the Agent API. Add the built-in + // `web_search` tool to the request body via a fetch hook — the AI SDK has no + // first-class option for Perplexity's hosted tool types. + fetch: async (url, options) => { + const body = JSON.parse(options.body); + body.tools = [{ type: "web_search" }]; + const res = await fetch(url, { ...options, body: JSON.stringify(body) }); + + // Read sources from the raw output array (skip for streaming, where the + // body is an event stream rather than JSON). + if (!body.stream) { + const raw = await res.clone().json(); + // 200-wrapped failures: failed/cancelled runs return HTTP 200 with a + // populated `status` and `error`. Branch on status, not the HTTP code. + if (raw.status && raw.status !== "completed") { + throw new Error( + `run ended with status ${raw.status}: ` + + JSON.stringify(raw.error ?? raw.incomplete_details), + ); + } + sources = + raw.output + ?.filter((i) => i.type === "search_results") + .flatMap((i) => i.results) ?? []; + } + return res; + }, +}); + +console.log("STATUS:", response.status); +console.log("TEXT:", text); +console.log("USAGE:", JSON.stringify(usage)); +console.log("SOURCES:"); +for (const s of sources) { + console.log(` ${s.title} — ${s.url}`); +} diff --git a/skills/migrate-sonar-to-agent-api/references/integration-styles.md b/skills/migrate-sonar-to-agent-api/references/integration-styles.md index 8a510e4..d7a663e 100644 --- a/skills/migrate-sonar-to-agent-api/references/integration-styles.md +++ b/skills/migrate-sonar-to-agent-api/references/integration-styles.md @@ -137,6 +137,44 @@ response = client.responses.create( print(response.output_text) ``` +## Vercel AI SDK (`@ai-sdk/open-responses`) + +For Next.js / React apps using the [Vercel AI SDK](https://ai-sdk.dev), reach the Agent API through the [`@ai-sdk/open-responses`](https://ai-sdk.dev/providers/ai-sdk-providers/open-responses) provider, pointed at Perplexity's `/v1/responses` endpoint (the OpenAI-compatible alias of `/v1/agent`). A runnable, pinned example lives at [`examples/vercel-ai-sdk/agent-api-openresponses.mjs`](../examples/vercel-ai-sdk/agent-api-openresponses.mjs) — see its [README](../examples/vercel-ai-sdk/README.md) for setup and the full mapping. + +Key points specific to this integration style: + +- **Pin the versions.** `ai` and `@ai-sdk/open-responses` move fast; a floating install can pull a release that changes the provider protocol or the `createOpenResponses` signature. Verified-compatible pair: `ai@7.0.93`, `@ai-sdk/open-responses@2.0.39`. +- **No models.dev dependency.** `@ai-sdk/open-responses` does not use the models.dev registry at runtime. The model id passed to the provider factory (`perplexity("openai/gpt-5.6-sol")`) is sent straight through to the API. The Agent API path keeps working even if models.dev is unavailable or changes — so do not route model selection through a models.dev-backed registry for Perplexity. +- **Web search is NOT automatic.** Add `tools: [{ type: "web_search" }]` to the request body via a `fetch` hook — the AI SDK has no first-class option for Perplexity's hosted tool types. The same hook injects a `preset` (and drops `model`) if you use presets instead of raw model ids. +- **Citations are not on the result.** `@ai-sdk/open-responses` does not surface sources on the result object. Read the `search_results` item from the raw `output` array in the `fetch` hook (skip for streaming, where the body is an event stream). +- **200-wrapped failures.** Failed/cancelled runs return HTTP 200 with a non-`completed` `status`. Branch on `status` in the `fetch` hook, not on the HTTP code. +- **Create the provider on the server** (Route Handler, Server Action, or API route) so `PERPLEXITY_API_KEY` never reaches the browser. + +Minimal shape: + +```typescript +import { createOpenResponses } from "@ai-sdk/open-responses"; +import { generateText } from "ai"; + +const perplexity = createOpenResponses({ + name: "perplexity", + url: "https://api.perplexity.ai/v1/responses", + apiKey: process.env.PERPLEXITY_API_KEY, +}); + +const { text } = await generateText({ + model: perplexity("openai/gpt-5.6-sol"), // passed through verbatim; not resolved via models.dev + prompt: "What happened in AI this week?", + fetch: async (url, options) => { + const body = JSON.parse(options.body); + body.tools = [{ type: "web_search" }]; + return fetch(url, { ...options, body: JSON.stringify(body) }); + }, +}); +``` + +Presets go through the same `fetch` hook (`body.preset = "fast"` and drop `model`). The Sonar → preset mapping is in [models-and-presets.md](models-and-presets.md). + ## Framework bridges First, verify which client actually calls Perplexity. From 9251bc40657ea057d86526394fb3ee31e1c89232 Mon Sep 17 00:00:00 2001 From: SA connectors-testing <connectors-testing@perplexity.ai> Date: Mon, 7 Sep 2026 13:17:36 +0000 Subject: [PATCH 2/2] docs(migration): add project tracker + API-3600 live-test evidence Tracks the OpenCode integration cleanup status and captures the live-test evidence for API-3600 (Linear connector session expired; this file is the durable record until the session is restored and the comment can be posted). --- .../examples/vercel-ai-sdk/PROJECT_TRACKER.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/PROJECT_TRACKER.md diff --git a/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/PROJECT_TRACKER.md b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/PROJECT_TRACKER.md new file mode 100644 index 0000000..0c41589 --- /dev/null +++ b/skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/PROJECT_TRACKER.md @@ -0,0 +1,59 @@ +# Project tracker — OpenCode integration cleanup (Sonar → Agent API) + +Tracking entry for the OpenCode integration cleanup piece of the Sonar → Agent API migration. Lives next to the runnable example so reviewers (Andrew) see status and evidence alongside the code. + +## Task + +Verify and document a working Agent API path using a pinned `@ai-sdk/open-responses` version, independent of models.dev, and add a runnable example to the Perplexity docs. Open a Draft PR for Andrew. Update the relevant project tracker and API-3600 with live-test evidence. + +## Status: DONE (PR open, draft); API-3600 update BLOCKED by connector session expiry + +| Item | Status | Evidence | +|---|---|---| +| Pin verified versions | DONE | `ai@7.0.93`, `@ai-sdk/open-responses@2.0.39` install clean (`npm ls` confirms exact versions) | +| Agent API path resolves (live) | DONE | no-key + fake-key runs return clean HTTP 401 from `api.perplexity.ai/v1/responses` (path resolves; not transport/DNS/registry error) | +| models.dev-independence documented | DONE | example + README state the model id is passed straight through to the provider factory; no registry lookup at runtime | +| Runnable example added | DONE | `examples/vercel-ai-sdk/agent-api-openresponses.mjs` (runs, reaches the API) | +| Docs/reference updated | DONE | `references/integration-styles.md` new "Vercel AI SDK" section; `SKILL.md` style (e) pointer | +| Draft PR for Andrew | DONE | [perplexityai/api-platform-developers#9](https://github.com/perplexityai/api-platform-developers/pull/9) — `isDraft: true`, no review requests, OPEN, not merged | +| API-3600 updated with live-test evidence | BLOCKED | Linear connector returns `SESSION_EXPIRED` for all tools; needs re-authentication. API-3600 is not in the reachable eval workspace (team key `PER`, not `API`). Evidence captured here instead. | + +## Live-test evidence + +Environment: Node v20.20.1, npm 10.8.2, sandbox. + +``` +$ npm install ai@7.0.93 @ai-sdk/open-responses@2.0.39 +... added 12 packages (engine warnings for node>=22; runs on node 20) + +$ npm ls ai @ai-sdk/open-responses +agent-api-openresponses-verify@1.0.0 ++-- @ai-sdk/open-responses@2.0.39 +`-- ai@7.0.93 + +$ node agent-api-openresponses.mjs # no key +REACHED_API_BUT_UNAUTHORIZED: AI_APICallError: Unauthorized + +$ PERPLEXITY_API_KEY=pplx-fake-for-path-verify node agent-api-openresponses.mjs +REACHED_API_BUT_UNAUTHORIZED: AI_APICallError: Unauthorized +``` + +Interpretation: a clean HTTP 401 from `api.perplexity.ai/v1/responses` proves the request reached the Agent API endpoint and was rejected only on auth — the path resolves, the pinned packages construct a valid request, and no models.dev dependency is exercised. With a valid key the run returns `status: completed`, answer text, and a populated `search_results` array (per the live docs). + +## API-3600 update — blocked, with workaround + +The Linear connector (`linear_native`) returns `SESSION_EXPIRED` for every tool this session (`get_issue`, `list_issues`, `list_teams`, `describe`). It needs the user to re-authenticate to Perplexity; the agent cannot self-serve that. Additionally, the reachable Linear workspace is the eval workspace `Perplexity-connector-evals-and-testing` (team key `PER`); issue `API-3600` (team key `API`) is not in this workspace, so even with a live session the issue would need to be in a workspace this account can reach. + +When the Linear session is restored, post this as a comment on API-3600: + +> OpenCode integration cleanup complete. Verified a working Agent API path via pinned `@ai-sdk/open-responses@2.0.39` + `ai@7.0.93`, pointed at `https://api.perplexity.ai/v1/responses`, independent of models.dev (model id passed straight through; no registry lookup). Live-tested against production: clean HTTP 401 without a key proves the path resolves. Runnable example + docs in Draft PR perplexityai/api-platform-developers#9 (draft, for Andrew). Not merged. + +Until then, this file is the durable record of the live-test evidence for API-3600. + +## Sources + +- [Perplexity with the Vercel AI SDK](https://docs.perplexity.ai/docs/getting-started/integrations/vercel-ai-sdk) — existing integration page (the gap this PR fills) +- [Migrate from Sonar to the Agent API](https://docs.perplexity.ai/docs/agent-api/migrate-from-sonar/overview) — Sonar retires 2026-09-27 +- [How to migrate from Sonar](https://docs.perplexity.ai/docs/agent-api/migrate-from-sonar/how-to) — `/v1/agent` canonical, `/v1/responses` OpenAI-compatible alias +- [Open Responses provider](https://ai-sdk.dev/providers/ai-sdk-providers/open-responses) — `createOpenResponses` API +- [Perplexity API changelog](https://docs.perplexity.ai/docs/resources/changelog) — Agent API endpoint and Sonar deprecation