SuperAPI AI Directory

migrate-sonar-to-agent-api

Coding & development

View repository

Updated 10-16 03:17

About this skill

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, curl to https://api.perplexity.ai/chat/completions): migrate to POST https://api.perplexity.ai/v1/agent with the same HTTP library.
  • (b) OpenAI SDK pointed at Perplexity: set base_url="https://api.perplexity.ai/v1" - the /v1 is required, the SDK appends /responses - and switch to client.responses.create(). Perplexity-only fields (preset, max_steps, response_format, Perplexity tool types) go through extra_body={...} in Python or a type cast in TypeScript. Requires a recent OpenAI SDK (openai>=1.66 for Python).
  • (c) Perplexity SDK (from perplexity import Perplexity / @perplexity-ai/perplexity_ai): switch client.chat.completions.create() to client.responses.create(). Same client construction, same PERPLEXITY_API_KEY env 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

  1. Leftover Sonar params -> 400 unknown field. Strict mode (above). Check every request body against the param table and REMOVE list in request-mapping.md.

  2. 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 empty search_results gracefully. Do not force tool_choice universally; it adds cost and latency where grounding is optional.

  3. Citations moved. There is no top-level citations or search_results on Agent responses. Read the search_results OUTPUT ITEM inside output[]. Message annotations are often an empty array - do not rely on them for citations.

  4. 200-wrapped failures. Failed and cancelled runs return HTTP 200 with status: "failed" or "cancelled" and a populated error field. Always branch on response.status, never only on the HTTP status code.

  5. Streaming consumers must handle ALL terminals. Exactly one of response.completed, response.failed, response.incomplete, response.cancelled arrives, plus a bare error event for transport failures. A consumer that only exits on response.completed hangs on failed runs.

  6. No perplexity/sonar-pro slug exists. Map sonar-pro per the two-option rule (preset "low" vs perplexity/sonar + web_search) in models-and-presets.md.

  7. max_output_tokens is REQUIRED for anthropic/* 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 old finish_reason: "length").

  8. OpenAI SDK traps. text={"format": ...} is rejected - send top-level response_format via extra_body. Perplexity-only output items deserialize as raw dicts: use result.get("url"), not result.url, and avoid model_dump() on responses containing them. Prefer response.output_text for the plain-text case.

  9. 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.

  10. Preset mechanics. instructions alongside 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: 0 is the blunt all-tools off-switch). Request fields override preset values. Presets may resolve to third-party models.

Step-by-step procedure

  1. 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.
  2. Classify each call site by integration style (Step 1 above).
  3. Rewrite the request per request-mapping.md: endpoint to /v1/agent; messages -> input (system prompt to instructions or a system input item); model slug or preset per models-and-presets.md; search filters into tools[web_search].filters; strip everything on the REMOVE list.
  4. Rewrite response handling per response-and-streaming.md: output[] walk or output_text; citations from the search_results item; usage key renames; status branching.
  5. Rewrite streaming consumers per response-and-streaming.md: typed SSE events instead of delta.content; handle all terminals.
  6. Migrate function calling: Responses-style tool shape (top-level name/description/parameters), reserved-name renames, function_call -> function_call_output loop. Details in integration-styles.md.
  7. 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_tokens and 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-level search_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.