Skip to content
Draft
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
2 changes: 2 additions & 0 deletions skills/migrate-sonar-to-agent-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions skills/migrate-sonar-to-agent-api/examples/vercel-ai-sdk/README.md
Original file line number Diff line number Diff line change
@@ -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: <answer text grounded in current web search results>
USAGE: {...}
SOURCES:
<title> — <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.
Original file line number Diff line number Diff line change
@@ -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}`);
}
38 changes: 38 additions & 0 deletions skills/migrate-sonar-to-agent-api/references/integration-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down