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.