Migrate sonar to agent api
Skill perplexityai/api-platform-developers/skills/migrate-sonar-to-agent-api
Migrate code from Perplexity's Sonar API (chat completions) to the Agent API (/v1/agent, responses.create). Maps endpoints, request params, model slugs (sonar, sonar-pro, sonar-reasoning, sonar-deep-research), response parsing, streaming, and citations. Use when migrating from Sonar, moving chat completions to the Agent API, replacing deprecated sonar-pro or sonar-reasoning models, or fixing 400 unknown-field errors and missing citations after a migration attempt.From its SKILL.md
npx -y skills add perplexityai/api-platform-developers --skill migrate-sonar-to-agent-apiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
11.3 KB, as published. Nobody here has run it
Migrate Sonar chat completions to the Perplexity Agent API
What this skill does
Migrates a codebase from Perplexity's Sonar API (POST /chat/completions, chat.completions.create()) to the Agent API (POST /v1/agent, responses.create()).
Both APIs are live; this is a contract migration - endpoint, request body, response parsing, streaming - not a rewrite.
Start by inventorying Sonar usage:
python scripts/scan_sonar_usage.py [path-to-code]
Run it from this skill's directory (the directory containing this SKILL.md), or pass the script's full path. It prints a file-by-file migration checklist with line numbers and the reference file that covers each finding.
Canonical human guide: https://docs.perplexity.ai/docs/agent-api/migrate-from-sonar - when this skill and the live docs disagree on an API fact, the docs win; cross-check there if something looks off.
If a perplexity-docs MCP server is available in your session (the Claude Code plugin ships one; other clients can add https://docs.perplexity.ai/mcp), prefer its search tools for that cross-check.
Never invent model availability, limits, defaults, or API behavior: if a fact is in neither this skill nor the docs, verify it against the live API - or state that you could not.
Step 1: classify each call site by integration style
Classify before touching code, and KEEP the style - do not "upgrade" raw HTTP to an SDK or swap SDKs.
- (a) Raw HTTP (
requests,fetch,curltohttps://api.perplexity.ai/chat/completions): migrate toPOST https://api.perplexity.ai/v1/agentwith the same HTTP library. - (b) OpenAI SDK pointed at Perplexity:
set
base_url="https://api.perplexity.ai/v1"- the/v1is required, the SDK appends/responses- and switch toclient.responses.create(). Perplexity-only fields (preset,max_steps,response_format, Perplexity tool types) go throughextra_body={...}in Python or a type cast in TypeScript. Requires a recent OpenAI SDK (openai>=1.66for Python). - (c) Perplexity SDK (
from perplexity import Perplexity/@perplexity-ai/perplexity_ai): switchclient.chat.completions.create()toclient.responses.create(). Same client construction, samePERPLEXITY_API_KEYenv var. - (d) Framework bridge (LangChain
ChatPerplexity, LlamaIndex Perplexity LLM, OpenAI Agents SDK): read integration-styles.md - it has the concrete Agents SDK class swap and the ranked options for LangChain/LlamaIndex.
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.
Leave framework-internal LLMs pointed at other providers alone.
Do NOT migrate:
- Search API code (
client.search.create(),POST /search) - a different product, unaffected. - Code already on the Agent API (
responses.create,/v1/agent,/v1/responses). - Historical or factual references to "the Sonar API" as a legacy platform in prose.
- Third-party showcase code that merely describes what someone built.
The strict-mode warning (read this first)
THE #1 MIGRATION FAILURE: the Agent API rejects ANY unknown or leftover field - top-level or nested - with HTTP 400:
{"error":{"message":"invalid request body: json: unknown field \"X\"","type":"invalid_request","code":400,"param":"X"}}Sonar silently dropped many params; the Agent API does not. Strip every field not explicitly mapped in request-mapping.md, including everything on its REMOVE list.
Top 10 pitfalls
-
Leftover Sonar params -> 400 unknown field. Strict mode (above). Check every request body against the param table and REMOVE list in request-mapping.md.
-
Web search is NOT automatic - and offering the tool does not guarantee the model calls it. A bare model request does no search and answers ungrounded. Add
"tools": [{"type": "web_search"}](or use a preset, which bundles it). For citation-critical flows, force the search with"tool_choice": {"type": "web_search"}or use a preset. Even forced, an occasional run returns zero sources - handle emptysearch_resultsgracefully. Do not forcetool_choiceuniversally; it adds cost and latency where grounding is optional. -
Citations moved. There is no top-level
citationsorsearch_resultson Agent responses. Read thesearch_resultsOUTPUT ITEM insideoutput[]. Messageannotationsare often an empty array - do not rely on them for citations. -
200-wrapped failures. Failed and cancelled runs return HTTP 200 with
status: "failed"or"cancelled"and a populatederrorfield. Always branch onresponse.status, never only on the HTTP status code. -
Streaming consumers must handle ALL terminals. Exactly one of
response.completed,response.failed,response.incomplete,response.cancelledarrives, plus a bareerrorevent for transport failures. A consumer that only exits onresponse.completedhangs on failed runs. -
No
perplexity/sonar-proslug exists. Mapsonar-proper the two-option rule (preset"low"vsperplexity/sonar+ web_search) in models-and-presets.md. -
max_output_tokensis REQUIRED foranthropic/*models (400 otherwise); values under 16 floor to 16. On presets and reasoning models, tight caps can be consumed by reasoning tokens before any text or search appears - size generously or omit.incomplete_details.reason: "max_output_tokens"is the truncation signal (the oldfinish_reason: "length"). -
OpenAI SDK traps.
text={"format": ...}is rejected - send top-levelresponse_formatviaextra_body. Perplexity-only output items deserialize as raw dicts: useresult.get("url"), notresult.url, and avoidmodel_dump()on responses containing them. Preferresponse.output_textfor the plain-text case. -
Reserved custom-function names. Custom tool names colliding with built-in tool names (
web_search,search_web,people_search,search_people,fetch_url,finance_search,sandbox, and others) are rejected with a 400. Sonar allowed shadowing; rename during migration (e.g.web_search->my_web_search). If any custom tool name 400s, rename it - other names may also be reserved. -
Preset mechanics.
instructionsalongside a preset REPLACES the preset's system prompt (empty string clears it). Preset tools cannot be disabled (tools: []does not clear them;max_tool_calls: 0is the blunt all-tools off-switch). Request fields override preset values. Presets may resolve to third-party models.
Step-by-step procedure
- Scan.
Run
python scripts/scan_sonar_usage.py [path-to-code]from this skill's directory (the directory containing this SKILL.md), or pass the script's full path, and turn its output into a file-by-file checklist. - Classify each call site by integration style (Step 1 above).
- Rewrite the request per request-mapping.md:
endpoint to
/v1/agent;messages->input(system prompt toinstructionsor a system input item); model slug or preset per models-and-presets.md; search filters intotools[web_search].filters; strip everything on the REMOVE list. - Rewrite response handling per response-and-streaming.md:
output[]walk oroutput_text; citations from thesearch_resultsitem; usage key renames; status branching. - Rewrite streaming consumers per response-and-streaming.md:
typed SSE events instead of
delta.content; handle all terminals. - Migrate function calling:
Responses-style tool shape (top-level
name/description/parameters), reserved-name renames,function_call->function_call_outputloop. Details in integration-styles.md. - Verify per verification.md: live smoke test each distinct call shape, then grep for leftovers. The live run is MANDATORY when an API key is available - compile-clean SDK code can still crash on call (see the typed-kwarg trap in integration-styles.md); if you truly cannot run it, say so explicitly in your summary.
Minimal-diff rule: keep the author's structure, naming, and voice; do not refactor unrelated code.
When a Sonar feature has no Agent equivalent (images, related questions, academic search mode), apply the documented workaround from request-mapping.md if it is small.
For return_related_questions specifically, use the lightweight prompt-delimiter recipe in request-mapping.md - it fits free-text flows too.
If no workaround fits, remove the feature AND call the removal out in your migration notes; never drop user-facing output silently.
Verification loop
- Compile or typecheck every changed file.
- Run one real request per distinct call shape against production, with a small
max_output_tokensand a cheap model (except on presets and reasoning models - see pitfall 7). - Capture real response JSON for any printed samples; never fabricate response JSON.
- Grep for leftovers:
chat.completions,choices[0],max_tokens(as a request field), top-levelsearch_recency_filter,citations,delta.content,sonar-model slugs. Every hit must be intentional legacy prose. - Full checklist in verification.md.
Reference index
- request-mapping.md - read when rewriting a request body: full param table, the REMOVE list with workarounds, limits.
- response-and-streaming.md - read when rewriting response parsing or a streaming consumer: output[] walk, search_results shape, full SSE event set, error catalogue.
- models-and-presets.md - read when choosing a target model or preset: slug mapping, preset mechanics.
- integration-styles.md - read when the code uses the OpenAI SDK, Perplexity SDK, LangChain, LlamaIndex, or the OpenAI Agents SDK.
- verification.md - read before declaring the migration done: checklist, live smoke pattern, leftover greps.