
Perplexity
- 31 installs
- 22 repo stars
- Updated August 1, 2026
- itechmeat/llm-code
Build web-grounded AI features with the Perplexity Sonar API: chat, search, streaming, structured outputs, domain filters and attachments.
About
A reference for the Perplexity API and Sonar models covering chat/search endpoints, streaming, structured JSON output, web-search filters and image/PDF attachments. Use it when adding real-time web-grounded responses or citation-backed search to an LLM application.
- Model selection guide (sonar, sonar-pro, sonar-reasoning-pro, sonar-deep-research) with pricing
- Prohibitions like Pro Search requiring stream=True and using the citations field instead of asking for URLs
Perplexity by the numbers
- 31 all-time installs (skills.sh)
- Ranked #9,164 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/itechmeat/llm-code --skill perplexityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 31 |
|---|---|
| repo stars | ★ 22 |
| Last updated | August 1, 2026 |
| Repository | itechmeat/llm-code ↗ |
What it does
Build web-grounded AI features with the Perplexity Sonar API: chat, search, streaming, structured outputs, domain filters and attachments.
Files
Perplexity API
Build AI applications with real-time web search and grounded responses.
Quick Navigation
- Models & pricing:
references/models.md - Search API patterns:
references/search-api.md - Chat completions guide:
references/chat-completions.md - Browser sessions API:
references/browser.md - Embeddings API:
references/embeddings.md - Structured outputs:
references/structured-outputs.md - Filters (domain/language/date/location):
references/filters.md - Media (images/videos/attachments):
references/media.md - Pro Search:
references/pro-search.md - Prompting best practices:
references/prompting.md
When to Use
- Need AI responses grounded in current web data
- Building search-powered applications
- Research tools requiring citations
- Real-time Q&A with source verification
- Document/image analysis with web context
Installation
Install: pip install perplexityai (Python) or npm install @perplexityai/perplexity (TypeScript/JavaScript).
Authentication
# macOS/Linux
export PERPLEXITY_API_KEY="your_api_key_here"
# Windows
setx PERPLEXITY_API_KEY "your_api_key_here"SDK auto-reads PERPLEXITY_API_KEY environment variable.
Quick Start — Chat Completion
from perplexity import Perplexity
client = Perplexity()
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "What is the latest news on AI?"}]
)
print(completion.choices[0].message.content)Note (v0.28.0): The Python client includes a custom JSON encoder to support additional types in request payloads.
Quick Start — Search API
from perplexity import Perplexity
client = Perplexity()
search = client.search.create(
query="artificial intelligence trends 2024",
max_results=5
)
for result in search.results:
print(f"{result.title}: {result.url}")Release Highlights (0.34.1 -> 0.38.0)
- Streaming:
responses.createnow yields named SSE events and discriminates theResponseStreamEventunion, which matters for typed stream consumers. - Search context:
search_context_sizewas briefly exposed onsearch.create, removed in0.35.1, then reintroduced in0.37.0for both the Search API and theweb_searchtool to control retrieved context size. - Background responses: the SDK adds background-task support and
responses.retrieve, so long-running response workflows can be polled instead of only streamed inline. - Reasoning effort:
xhighis available where the API supports reasoning-effort controls. - Sandbox tool:
0.36.0adds the Responses API sandbox built-in tool;0.38.0adds afilessubresource for retrieving sandbox-produced files. Gate both like other executable/tooling surfaces.
Model Selection Guide
| Model | Use Case | Cost |
|---|---|---|
sonar | Quick facts, simple Q&A | Lowest |
sonar-pro | Complex queries, research | Medium |
sonar-reasoning-pro | Multi-step reasoning, analysis | Medium |
sonar-deep-research | Exhaustive research, reports | Highest |
Key Patterns
Streaming Responses
stream = client.chat.completions.create(
messages=[{"role": "user", "content": "Explain quantum computing"}],
model="sonar",
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")Multi-Turn Conversation
messages = [
{"role": "system", "content": "You are a research assistant."},
{"role": "user", "content": "What causes climate change?"},
{"role": "assistant", "content": "Climate change is caused by..."},
{"role": "user", "content": "What are the solutions?"}
]
completion = client.chat.completions.create(messages=messages, model="sonar")Web Search Options
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Latest renewable energy news"}],
model="sonar",
web_search_options={
"search_recency_filter": "week",
"search_domain_filter": ["energy.gov", "iea.org"]
}
)Pro Search (Multi-Step Research)
# REQUIRES stream=True
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Research solar panel ROI"}],
search_type="pro",
stream=True
)
for chunk in completion:
print(chunk.choices[0].delta.content or "", end="")Image Attachment
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
]
}]
)File Attachment (PDF Analysis)
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this document"},
{"type": "file_url", "file_url": {"url": "https://example.com/report.pdf"}}
]
}]
)Return Images in Response
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "Mount Everest photos"}],
return_images=True,
image_format_filter=["jpg", "png"]
)Domain Filtering (Search API)
# Allowlist: include only these domains
search = client.search.create(
query="climate research",
search_domain_filter=["science.org", "nature.com"]
)
# Denylist: exclude these domains
search = client.search.create(
query="tech news",
search_domain_filter=["-reddit.com", "-pinterest.com"]
)Multi-Query Search
search = client.search.create(
query=[
"AI trends 2024",
"machine learning healthcare",
"neural networks applications"
],
max_results=5
)
for i, query_results in enumerate(search.results):
print(f"Query {i+1} results:")
for result in query_results:
print(f" {result.title}")Structured Outputs (JSON Schema)
from pydantic import BaseModel
class ContactInfo(BaseModel):
email: str
phone: str
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Find contact for Tesla IR"}],
response_format={
"type": "json_schema",
"json_schema": {"schema": ContactInfo.model_json_schema()}
}
)
contact = ContactInfo.model_validate_json(completion.choices[0].message.content)Async Operations
import asyncio
from perplexity import AsyncPerplexity
async def main():
async with AsyncPerplexity() as client:
tasks = [
client.search.create(query="AI news"),
client.search.create(query="tech trends")
]
results = await asyncio.gather(*tasks)
asyncio.run(main())Rate Limit Handling
import time
from perplexity import RateLimitError
def search_with_retry(client, query, max_retries=3):
for attempt in range(max_retries):
try:
return client.search.create(query=query)
except RateLimitError:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
else:
raiseResponse Parameters
| Parameter | Default | Description |
|---|---|---|
temperature | 0.7 | Creativity (0-2) |
max_tokens | varies | Response length limit |
top_p | 0.9 | Nucleus sampling |
presence_penalty | 0 | Reduce repetition (-2 to 2) |
frequency_penalty | 0 | Reduce word frequency (-2 to 2) |
Search API Parameters
| Parameter | Description |
|---|---|
max_results | 1-20 results per query |
max_tokens_per_page | Content extraction depth (default 2048) |
country | ISO country code for regional results |
search_domain_filter | Domain allowlist/denylist (max 20) |
search_language_filter | ISO 639-1 language codes (max 10) |
Pricing Quick Reference
Search API: $5/1K requests (no token costs)
Sonar Models (per 1M tokens):
| Model | Input | Output |
|---|---|---|
| sonar | $1 | $1 |
| sonar-pro | $3 | $15 |
| sonar-reasoning-pro | $2 | $8 |
Request fees (per 1K requests): $5-$14 depending on search context size.
Critical Prohibitions
- Do NOT request links/URLs in prompts (use
citationsfield instead — model will hallucinate URLs) - Do NOT use recursive JSON schemas (not supported)
- Do NOT use
dict[str, Any]in Pydantic models for structured outputs - Do NOT mix allowlist and denylist in
search_domain_filter - Do NOT exceed 5 queries in multi-query search
- Do NOT expect first request with new JSON schema to be fast (10-30s warmup)
- Do NOT use Pro Search without
stream=True(will fail) - Do NOT send images to
sonar-deep-research(not supported) - Do NOT include
data:prefix for file attachments base64 (only for images) - Do NOT try to control search via prompts (use API parameters instead)
Error Handling
import perplexity
try:
completion = client.chat.completions.create(...)
except perplexity.BadRequestError as e:
print(f"Invalid parameters: {e}")
except perplexity.RateLimitError:
print("Rate limited, retry later")
except perplexity.APIStatusError as e:
print(f"API error: {e.status_code}")OpenAI SDK Compatibility
Perplexity supports OpenAI Chat Completions format. Use OpenAI client by pointing to Perplexity endpoint.
Links
Browser API (Remote Sessions)
Perplexity's Python SDK exposes a minimal Browser API for creating and cleaning up remote browser sessions for CDP-based automation.
Endpoints / Methods
client.browser.sessions.create()→POST /v1/browser/sessionsclient.browser.sessions.delete(session_id)→DELETE /v1/browser/sessions/{session_id}
Basic Workflow
from perplexity import Perplexity
client = Perplexity()
session = client.browser.sessions.create()
if not session.session_id:
raise RuntimeError('Browser session did not return a session_id')
try:
# Use `session.session_id` with your CDP client to automate the remote browser.
# The Perplexity SDK manages session lifecycle; CDP automation is outside this SDK.
pass
finally:
client.browser.sessions.delete(session.session_id)Notes
- The create response includes
session_idandstatus. - Always delete sessions to avoid leaking remote resources.
Chat Completions Reference
Web-grounded AI responses with conversation context.
Basic Usage
from perplexity import Perplexity
client = Perplexity()
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "Tell me about AI developments"}]
)
print(completion.choices[0].message.content)Extended Type Encoding (v0.28.0)
The Python client ships with a custom JSON encoder for broader type support. If you pass non-primitive objects in request payloads, let the client encode them instead of manually serializing to JSON.
Message Roles
| Role | Purpose |
|---|---|
system | Set behavior and persona |
user | User input/questions |
assistant | Model responses (for context) |
Multi-Turn Conversation
messages = [
{"role": "system", "content": "You are a research assistant."},
{"role": "user", "content": "What causes climate change?"},
{"role": "assistant", "content": "Climate change is caused by..."},
{"role": "user", "content": "What are the solutions?"}
]
completion = client.chat.completions.create(
messages=messages,
model="sonar"
)Streaming Responses
stream = client.chat.completions.create(
messages=[{"role": "user", "content": "Write a summary of AI breakthroughs"}],
model="sonar",
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)Responses API streaming note (v0.34.1+)
The Python SDK now yields named SSE events for responses.create and discriminates the ResponseStreamEvent union. If your app branches on event types, prefer the named event/type fields over brittle string parsing of raw chunks.
TypeScript:
const stream = await client.chat.completions.create({
messages: [{ role: "user", content: "Explain quantum computing" }],
model: "sonar",
stream: true,
});
for await (const chunk of stream) {
if (chunk.choices[0]?.delta?.content) {
process.stdout.write(chunk.choices[0].delta.content);
}
}Web Search Options
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Latest renewable energy news"}],
model="sonar",
web_search_options={
"search_recency_filter": "week",
"search_domain_filter": ["energy.gov", "iea.org"],
"max_search_results": 10
}
)Recency Filters
hour,day,week,month,year
Return Options
completion = client.chat.completions.create(
messages=[...],
model="sonar",
return_images=True,
return_related_questions=True
)Response Parameters
| Parameter | Default | Range | Description |
|---|---|---|---|
temperature | 0.7 | 0-2 | Creativity level |
max_tokens | varies | - | Response length limit |
top_p | 0.9 | 0-1 | Nucleus sampling |
presence_penalty | 0 | -2 to 2 | Topic diversity |
frequency_penalty | 0 | -2 to 2 | Word repetition |
completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Explain ML"}],
model="sonar",
max_tokens=500,
temperature=0.7,
top_p=0.9
)Async Chat (sonar-deep-research only)
Create Async Request
async_request = client.async_.chat.completions.create(
messages=[{"role": "user", "content": "Comprehensive renewable energy analysis"}],
model="sonar-deep-research",
max_tokens=2000
)
print(f"Request ID: {async_request.request_id}")
print(f"Status: {async_request.status}")Check Status
request_id = "req_123abc456def789"
status = client.async_.chat.completions.get(request_id)
if status.status == "completed":
print(status.result.choices[0].message.content)
elif status.status == "failed":
print(f"Error: {status.error}")List Requests
requests = client.async_.chat.completions.list(
limit=10,
status="completed"
)
for req in requests.data:
print(f"ID: {req.id}, Status: {req.status}")Responses API background tasks (v0.35.0+)
The SDK adds background-task support and responses.retrieve. Use this for long-running responses when you need a request id and polling/retrieval flow instead of holding a streaming connection open.
Operational notes:
- Persist the response id immediately so retries can retrieve the same job.
- Keep timeouts and cancellation policy explicit; background execution should not become an unbounded queue.
xhighreasoning effort is available where the API/model supports reasoning controls.0.36.0adds the Responses API sandbox built-in tool. Treat sandbox use as a tool-execution surface: gate it by product policy, log invocations, and do not expose it implicitly to untrusted user prompts.0.38.0adds afilessubresource for retrieving files produced inside the sandbox. Treat retrieved files as untrusted tool output: validate type/size and avoid passing them back into the model or filesystem unchecked.
Concurrent Operations
import asyncio
from perplexity import AsyncPerplexity
async def process_questions(questions):
async with AsyncPerplexity() as client:
tasks = [
client.chat.completions.create(
messages=[{"role": "user", "content": q}],
model="sonar"
)
for q in questions
]
return await asyncio.gather(*tasks)Error Handling
import perplexity
try:
completion = client.chat.completions.create(...)
except perplexity.BadRequestError as e:
print(f"Invalid parameters: {e}")
except perplexity.RateLimitError:
print("Rate limited, retry later")
except perplexity.APIStatusError as e:
print(f"API error: {e.status_code}")Rate Limit Retry Pattern
import time
import random
def chat_with_retry(messages, max_retries=3):
for attempt in range(max_retries):
try:
return client.chat.completions.create(
messages=messages,
model="sonar"
)
except perplexity.RateLimitError:
if attempt == max_retries - 1:
raise
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)Configuration Presets
Factual Q&A
factual = {
"temperature": 0.1,
"top_p": 0.9,
"search_recency_filter": "month"
}Creative Writing
creative = {
"temperature": 0.8,
"top_p": 0.95,
"presence_penalty": 0.1,
"frequency_penalty": 0.1
}System Prompt Example
system_prompt = """You are an expert research assistant.
Always provide well-sourced information and cite your sources.
Format responses with clear headings and bullet points."""
completion = client.chat.completions.create(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": "Explain quantum computing applications"}
],
model="sonar-pro"
)Best Practices
1. Choose right model: sonar for simple, sonar-pro for complex 2. Use streaming: Better UX for long responses 3. Implement retry: Handle rate limits gracefully 4. Set temperature: Low (0.1) for facts, high (0.8) for creativity 5. Use system prompts: Consistent behavior across requests
Embeddings API
Perplexity Embeddings API generates text embeddings for semantic search, RAG, and clustering. Use standard embeddings for independent texts and contextualized embeddings for document chunks that share context.
Models
- Standard:
pplx-embed-v1-0.6b(1024 dims),pplx-embed-v1-4b(2560 dims) - Contextualized:
pplx-embed-context-v1-0.6b(1024 dims),pplx-embed-context-v1-4b(2560 dims)
All models use mean pooling and accept up to 32K tokens per input.
Similarity Rules
- Embeddings are unnormalized. Compare
base64_int8with cosine similarity. - Compare
base64_binarywith Hamming distance.
Standard Embeddings (Independent Texts)
from perplexity import Perplexity
client = Perplexity()
response = client.embeddings.create(
input=["Query text", "Document text"],
model="pplx-embed-v1-4b"
)
for item in response.data:
print(item.index, item.embedding)Contextualized Embeddings (Document Chunks)
Use contextualized embeddings when chunks belong to the same document and their meaning depends on surrounding context.
from perplexity import Perplexity
client = Perplexity()
response = client.contextualized_embeddings.create(
input=[
["Doc1 chunk 1", "Doc1 chunk 2"],
["Doc2 chunk 1", "Doc2 chunk 2"]
],
model="pplx-embed-context-v1-4b"
)
for doc in response.data:
for chunk in doc.data:
print(doc.index, chunk.index, chunk.embedding)Parameters and Limits
- Standard inputs: up to 512 texts per request, 32K tokens per text, 120K tokens total.
- Contextualized inputs: up to 512 documents, 16,000 total chunks, 32K tokens per document, 120K tokens total.
encoding_format:base64_int8(default) orbase64_binary.dimensions: Matryoshka dimension reduction (range depends on model).
When to Use Which
- Standard embeddings: queries, short sentences, independent documents.
- Contextualized embeddings: document chunks (paragraphs, PDF sections, code file segments).
References
- Embeddings Quickstart: https://docs.perplexity.ai/docs/embeddings/quickstart
- Standard Embeddings: https://docs.perplexity.ai/docs/embeddings/standard-embeddings
- Contextualized Embeddings: https://docs.perplexity.ai/docs/embeddings/contextualized-embeddings
Perplexity Search Filters
Comprehensive guide to filtering search results.
Domain Filtering
Basic Usage
# Allowlist: include ONLY these domains
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "climate research"}],
search_domain_filter=["nature.com", "science.org"]
)
# Denylist: EXCLUDE these domains (prefix with -)
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "tech news"}],
search_domain_filter=["-reddit.com", "-pinterest.com"]
)Domain Filter Rules
| Rule | Example | Description |
|---|---|---|
| Allowlist (no prefix) | ["wikipedia.org"] | Include only these domains |
| Denylist (- prefix) | ["-gettyimages.com"] | Exclude these domains |
| Root domain matching | ["example.com"] | Includes subdomains |
| TLD filtering | [".gov", ".edu"] | Filter by domain extension |
| Maximum entries | 20 | Per request limit |
| Mix modes? | FORBIDDEN | Cannot mix allowlist and denylist |
Common Patterns
# Academic sources only
search_domain_filter=["arxiv.org", "nature.com", "science.org", ".edu", ".gov"]
# Exclude stock photo sites (for image searches)
search_domain_filter=["-gettyimages.com", "-shutterstock.com", "-istockphoto.com"]
# News from government sources
search_domain_filter=[".gov"]Language Filtering
Filter results by ISO 639-1 language codes.
# Single language
response = client.search.create(
query="artificial intelligence",
search_language_filter=["en"]
)
# Multiple languages (max 10)
response = client.search.create(
query="renewable energy",
search_language_filter=["en", "de", "fr"]
)Common Language Codes
| Language | Code | Language | Code |
|---|---|---|---|
| English | en | Portuguese | pt |
| Spanish | es | Russian | ru |
| French | fr | Chinese | zh |
| German | de | Japanese | ja |
| Italian | it | Korean | ko |
| Arabic | ar | Hindi | hi |
Validation
import re
def validate_language_code(code: str) -> bool:
return bool(re.match(r'^[a-z]{2}$', code))
# Must be lowercase: "en" not "EN"
# Maximum: 10 language codes per requestDate/Time Filtering
Recency Filter (Simple)
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "AI news"}],
search_recency_filter="week" # day, week, month, year
)Date Range Filtering (Precise)
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "climate research"}],
search_after_date_filter="01/01/2024", # Format: %m/%d/%Y
search_before_date_filter="12/31/2024"
)Last Updated Filters
# Filter by when content was last updated (not published)
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "API documentation"}],
last_updated_after_filter="01/01/2024",
last_updated_before_filter="06/30/2024"
)User Location Filtering
Refine results by geographic context.
Full Location (Recommended)
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "coffee shops nearby"}],
web_search_options={
"user_location": {
"country": "US",
"region": "California",
"city": "San Francisco",
"latitude": 37.7749,
"longitude": -122.4194
}
}
)Country Only
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "political news"}],
web_search_options={
"user_location": {"country": "US"}
}
)Location Rules
latitude/longitudeREQUIREcountryparameter- ISO 3166-1 alpha-2 country codes (US, GB, DE)
cityandregionsignificantly improve accuracy- Latitude: -90 to 90; Longitude: -180 to 180
Search Context Size
Control how much web content is retrieved.
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "simple fact question"}],
web_search_options={"search_context_size": "low"} # low, medium, high
)| Size | Cost | Best For |
|---|---|---|
| low | Lowest | Simple facts, cost-critical |
| medium | Default | General queries |
| high | Highest | Deep research, citations critical |
Search Modes
Academic Mode
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "neural network research"}],
search_mode="academic" # Prioritizes scholarly sources
)SEC Filings Mode
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Apple 10-K filing"}],
search_mode="sec" # Prioritizes SEC EDGAR database
)Search Control
Search Classifier
Let AI decide when to search:
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "What is 2+2?"}],
enable_search_classifier=True # Skips search for math
)Disable Search
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Write a poem"}],
disable_search=True # Uses only training data
)Combining Filters
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "climate policy"}],
search_domain_filter=["nature.com", ".gov"],
search_language_filter=["en", "de"],
search_recency_filter="month",
web_search_options={
"search_context_size": "high",
"user_location": {"country": "US"}
}
)Filter Limits Summary
| Filter | Limit |
|---|---|
| domain_filter | 20 entries |
| language_filter | 10 languages |
| image_domain_filter | 10 entries |
| image_format_filter | 10 entries |
Perplexity Media Handling
Guide to images, videos, and file attachments.
Returning Images
Enable Image Returns
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "Mount Everest images"}],
return_images=True
)Image Domain Filtering
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "historical images"}],
return_images=True,
image_domain_filter=["wikimedia.org"], # Allow only
# OR
image_domain_filter=["-gettyimages.com", "-shutterstock.com"] # Exclude
)Image Format Filtering
completion = client.chat.completions.create(
model="sonar",
messages=[{"role": "user", "content": "funny cat gif"}],
return_images=True,
image_format_filter=["gif"] # gif, jpg, png, webp
)Image Limits
- Maximum 30 images per response
- Maximum 10 entries in
image_domain_filter - Maximum 10 entries in
image_format_filter - Formats: lowercase, no dot prefix (
gifnot.gif)
Common Stock Photo Exclusions
image_domain_filter=[
"-gettyimages.com",
"-shutterstock.com",
"-istockphoto.com",
"-pinterest.com"
]Returning Videos
Enable Video Returns
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "2024 Olympics highlights"}],
media_response={
"overrides": {
"return_videos": True
}
}
)Combined Media Response
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Mars rover discoveries"}],
media_response={
"overrides": {
"return_videos": True,
"return_images": True
}
}
)Video Response Structure
{
"videos": [
{
"url": "https://www.youtube.com/watch?v=...",
"duration": null,
"thumbnail_width": 480,
"thumbnail_height": 360,
"thumbnail_url": "..."
}
]
}Image Attachments (Input)
Analyze images by uploading them.
Via HTTPS URL
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/image.jpg"
}
}
]
}]
)Via Base64
import base64
with open("image.png", "rb") as f:
image_data = base64.b64encode(f.read()).decode()
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What text is in this screenshot?"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_data}"
}
}
]
}]
)Image Attachment Limits
- Maximum size: 50 MB per image
- Supported formats: PNG, JPEG, WEBP, GIF
- NOT supported:
sonar-deep-researchmodel
Image Pricing Formula
$$ tokens = \frac{width\ px \times height\ px}{750} $$
Examples:
- 1024×768 image = 1,048 tokens
- 512×512 image = 349 tokens
File Attachments
Analyze documents (PDF, DOC, DOCX, TXT, RTF).
Via URL
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this document"},
{
"type": "file_url",
"file_url": {
"url": "https://example.com/document.pdf"
}
}
]
}]
)Via Base64
import base64
with open("report.pdf", "rb") as f:
file_data = base64.b64encode(f.read()).decode()
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Extract key findings"},
{
"type": "file_url",
"file_url": {
"url": file_data # No prefix for base64
},
"file_name": "report.pdf"
}
]
}]
)File Attachment Limits
- Maximum size: 50 MB per file
- Maximum files: 30 per request
- Maximum processing time: 60 seconds
- Supported: PDF, DOC, DOCX, TXT, RTF
- NOT supported: Scanned images (not OCR)
Base64 Important Notes
- For files: provide only the base64 string, no
data:URI prefix - For images: use full data URI format
data:image/png;base64,...
Common Use Cases
Screenshot Analysis
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Extract all text from this screenshot"},
{"type": "image_url", "image_url": {"url": "..."}}
]
}]PDF Summarization
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Summarize key points and recommendations"},
{"type": "file_url", "file_url": {"url": "https://example.com/report.pdf"}}
]
}]Combining Document with Web Search
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Compare this paper's findings with recent studies"},
{"type": "file_url", "file_url": {"url": "https://example.com/paper.pdf"}}
]
}]
)Error Handling
| Error | Cause | Solution |
|---|---|---|
| Invalid URL | URL not accessible | Verify URL returns file directly |
| File too large | Exceeds 50MB | Compress or split document |
| Processing timeout | Document too complex | Simplify question or use smaller sections |
| Invalid base64 | Malformed base64 string | Check encoding, no prefix for files |
| Invalid format | Unsupported file type | Use PDF, DOC, DOCX, TXT, or RTF |
Perplexity Models Reference
Available Models
Sonar (sonar)
- Purpose: Lightweight, cost-effective web search
- Best for: Quick facts, news updates, simple Q&A, high-volume applications
- Pricing: $1/1M input tokens, $1/1M output tokens
Sonar Pro (sonar-pro)
- Purpose: Advanced search with deeper content understanding
- Best for: Complex queries, competitive analysis, detailed research
- Pricing: $3/1M input tokens, $15/1M output tokens
Sonar Reasoning Pro (sonar-reasoning-pro)
- Purpose: Enhanced multi-step reasoning with web search
- Best for: Complex problem-solving, research analysis, strategic planning
- Pricing: $2/1M input tokens, $8/1M output tokens
Sonar Deep Research (sonar-deep-research)
- Purpose: Exhaustive research and detailed report generation
- Best for: Academic research, market analysis, comprehensive reports
- Pricing: $2/1M input, $8/1M output, $2/1M citation tokens, $5/1K search queries, $3/1M reasoning tokens
- Note: Async-only model via
client.async_.chat.completions.create()
Model Selection Decision Tree
Quick factual query? → sonar
Complex analysis needed? → sonar-pro
Multi-step reasoning? → sonar-reasoning-pro
Comprehensive research report? → sonar-deep-researchRequest Pricing by Search Context Size
| Model | Low | Medium | High |
|---|---|---|---|
| sonar | $5/1K | $8/1K | $12/1K |
| sonar-pro | $6/1K | $10/1K | $14/1K |
| sonar-reasoning-pro | $6/1K | $10/1K | $14/1K |
- Low: (default) Fastest, cheapest
- Medium: Balanced cost/quality
- High: Maximum search depth
Pro Search (sonar-pro only)
Enables automated multi-step tool usage for complex queries.
completion = client.chat.completions.create(
messages=[...],
model="sonar-pro",
stream=True, # Required for Pro Search
web_search_options={"search_type": "pro"}
)Search Types:
fast— Standard behavior (default)pro— Multi-step tool usage ($14-$22/1K requests)auto— Automatic classification
Cost Examples
Sonar (500 input + 200 output tokens, Low context)
- Input: $0.0005
- Output: $0.0002
- Request fee: $0.005
- Total: $0.0057
Sonar Deep Research (typical query)
- Input: ~$0.00
- Output: ~$0.03-$0.06
- Citation tokens: ~$0.04-$0.12
- Reasoning tokens: ~$0.22-$1.02
- Search queries: ~$0.09-$0.15
- Total: $0.40-$1.32
Configuration Presets
Factual Q&A (Low creativity)
config = {
"temperature": 0.1,
"top_p": 0.9,
"search_recency_filter": "month"
}Creative Writing (Higher creativity)
config = {
"temperature": 0.8,
"top_p": 0.95,
"presence_penalty": 0.1,
"frequency_penalty": 0.1
}Search API Pricing
$5 per 1,000 requests — No token-based costs.
Perplexity Pro Search
Advanced multi-step reasoning with built-in tools.
Overview
Pro Search enables complex research queries with:
- Multi-step reasoning visible in responses
- Built-in tools:
web_search,fetch_url_content - Automatic tool orchestration (no configuration needed)
Basic Usage
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Research solar panel ROI for California homes"}],
search_type="pro", # "pro", "fast", or "auto"
stream=True # REQUIRED for Pro Search
)
for chunk in completion:
print(chunk.choices[0].delta.content or "", end="")Search Types
| Type | Description | Use Case |
|---|---|---|
| pro | Full multi-step reasoning | Complex research, reports |
| fast | Quick search, fewer steps | Simple queries, speed needed |
| auto | AI decides | Mixed workloads |
CRITICAL: Streaming Required
Pro Search REQUIRES stream=True. Non-streaming requests will fail.
# ✅ CORRECT
completion = client.chat.completions.create(
model="sonar-pro",
search_type="pro",
stream=True,
messages=[...]
)
# ❌ WRONG - will fail
completion = client.chat.completions.create(
model="sonar-pro",
search_type="pro",
stream=False, # Pro Search doesn't support this
messages=[...]
)Built-in Tools
Pro Search automatically uses two tools:
web_search
Conducts web searches for current information.
{
"thought": "I need current EV market data",
"type": "web_search",
"web_search": {
"search_keywords": ["EV Statistics 2024", "electric vehicle sales"],
"search_results": [
{
"title": "Trends in electric cars",
"url": "https://www.iea.org/...",
"date": "2024-03-15",
"snippet": "Electric car sales neared 14 million...",
"source": "web"
}
]
}
}fetch_url_content
Retrieves full content from specific URLs.
{
"thought": "This paper has detailed methodology I need",
"type": "fetch_url_content",
"fetch_url_content": {
"contents": [
{
"title": "Research Paper Title",
"url": "https://arxiv.org/pdf/...",
"snippet": "The dominant sequence transduction models..."
}
]
}
}Multi-Tool Workflows
Pro Search automatically chains tools:
1. User asks: "Research solar panel options for California homes" 2. web_search → finds incentives and costs 3. fetch_url_content → reads policy documents 4. web_search → verifies electricity rates
Reasoning Steps
Access tool executions in reasoning_steps:
completion = client.chat.completions.create(
model="sonar-pro",
search_type="pro",
stream=True,
messages=[{"role": "user", "content": "AI startup funding 2024"}]
)
full_response = ""
reasoning_steps = []
for chunk in completion:
if chunk.choices[0].delta.content:
full_response += chunk.choices[0].delta.content
# Collect reasoning steps from streaming response
if hasattr(chunk, 'reasoning_steps'):
reasoning_steps.extend(chunk.reasoning_steps)
print("Final answer:", full_response)
print("Research steps:", reasoning_steps)Pricing
| Search Type | Per 1K Requests |
|---|---|
| Pro Search | $14–$22 |
| Fast Search | $6–$14 |
Prices vary by search context size (Low/Medium/High).
Pro Search vs Standard Sonar Pro
| Feature | Standard Sonar Pro | Pro Search |
|---|---|---|
| Model | sonar-pro | sonar-pro |
| Streaming | Optional | Required |
| Multi-step | No | Yes |
| Tool visibility | No | Yes |
| Complex research | Limited | Excellent |
| Cost | Lower | Higher |
Best Practices
Use Pro Search When
- Complex, multi-step research needed
- Want visibility into search process
- Building research/analysis tools
- Need comprehensive answers
Use Standard Search When
- Simple factual queries
- Speed is priority
- Cost-sensitive applications
- Don't need reasoning visibility
Combine with Filters
completion = client.chat.completions.create(
model="sonar-pro",
search_type="pro",
stream=True,
messages=[{"role": "user", "content": "Latest AI research papers"}],
search_domain_filter=["arxiv.org", "nature.com"],
search_recency_filter="month"
)Error Handling
from perplexity import BadRequestError
try:
completion = client.chat.completions.create(
model="sonar-pro",
search_type="pro",
stream=False, # This will fail
messages=[...]
)
except BadRequestError as e:
if "stream" in str(e).lower():
print("Pro Search requires stream=True")sonar-deep-research
For exhaustive research, use sonar-deep-research:
# Deep research is async-only
async def deep_research():
async with AsyncPerplexity() as client:
completion = await client.chat.completions.create(
model="sonar-deep-research",
messages=[{"role": "user", "content": "Comprehensive analysis of..."}],
stream=True
)
async for chunk in completion:
print(chunk.choices[0].delta.content or "", end="")Deep Research Limitations
- Async only — use
AsyncPerplexity - No image input — doesn't support image attachments
- Longer response time — can take minutes
- Highest cost — use sparingly
Perplexity Prompting Guide
Best practices for prompting web-search models.
System vs User Prompt
System Prompt
- Controls style, tone, language of response
- NOT used by real-time search component
- Good for formatting instructions
messages=[
{
"role": "system",
"content": """You are a helpful AI assistant.
Rules:
1. Provide only the final answer without explaining steps.
2. Format as a list if multiple items."""
},
{"role": "user", "content": "Best sushi restaurants in Tokyo"}
]User Prompt
- Used to kick off real-time web search
- Should contain the actual query
- Be specific and contextual
Key Differences from Traditional LLMs
Web search models behave differently from standard LLMs:
| Aspect | Traditional LLM | Perplexity Web Search |
|---|---|---|
| Few-shot prompting | Works well | Avoid — confuses search |
| Generic questions | Often acceptable | Too broad — poor results |
| Multi-part requests | Handled reasonably | Break apart — one topic |
| URL requests | Can hallucinate anyway | Never ask — use citations |
Best Practices
Be Specific
# ✅ GOOD - specific context
"Explain recent advances in climate prediction models for urban planning"
# ❌ BAD - too generic
"Tell me about climate models"Avoid Few-Shot Prompting
# ✅ GOOD
"Summarize current research on mRNA vaccine technology"
# ❌ BAD - examples confuse search
"Here's an example summary: [example]. Now summarize mRNA vaccines."Use Search-Friendly Terms
# ✅ GOOD - terms experts would use
"Compare energy efficiency ratings of heat pumps vs traditional HVAC for residential use"
# ❌ BAD - vague
"Tell me which home heating is better"One Topic Per Query
# ✅ GOOD
"Explain quantum computing principles that might impact cryptography in the next decade"
# ❌ BAD - multiple unrelated topics
"Explain quantum computing, regenerative agriculture, and stock market predictions"Handling URLs and Sources
NEVER ask for URLs in prompts
The model cannot see actual URLs from search and will hallucinate them.
# ❌ WRONG - URLs will be hallucinated
messages=[{
"role": "user",
"content": "Find Canadian news. For each, include headline, summary, and source link."
}]
# ✅ CORRECT - get URLs from response metadata
messages=[{
"role": "user",
"content": "Find Canadian news. For each, include headline and why it matters."
}]
# Then access URLs from search_results field in response
for citation in completion.citations:
print(f"Source: {citation.url}")Use Citations Field
URLs are in search_results / citations:
completion = client.chat.completions.create(...)
# Access accurate sources
for citation in completion.citations:
print(f"{citation.title}: {citation.url}")Preventing Hallucination
Set Clear Boundaries
messages=[{
"role": "user",
"content": """Search for renewable energy developments.
If you cannot find relevant information, state that clearly
rather than providing speculative information."""
}]Request Source Transparency
messages=[{
"role": "user",
"content": """Find Tesla's latest earnings report.
Only provide information from your search results.
Clearly state if certain details are not available."""
}]Avoid Inaccessible Sources
These often lead to hallucination:
- LinkedIn posts (private/auth required)
- Paywalled content
- Private documents
- Very recent unindexed content
Use API Parameters, Not Prompts
DON'T control search via prompts
# ❌ INEFFECTIVE
messages=[{
"role": "user",
"content": "Search only Wikipedia for climate change. Only use sources from past month."
}]DO use built-in parameters
# ✅ EFFECTIVE
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Climate change policies"}],
search_domain_filter=["wikipedia.org"],
search_recency_filter="month"
)Query Type Tips
| Query Type | Best Practices |
|---|---|
| Factual Research | Use domain filters, high search context |
| Creative Content | Style guidelines in system prompt, disable_search=True |
| Technical | Include language/framework, use docs domains |
| Analysis | Request step-by-step reasoning |
Examples
Technical Documentation
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "FastAPI dependency injection patterns"}],
search_domain_filter=["fastapi.tiangolo.com", "docs.python.org"],
web_search_options={"search_context_size": "medium"}
)Current Events
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": "Latest AI policy developments in European Union this week"
}],
search_recency_filter="week"
)Creative (No Search)
completion = client.chat.completions.create(
model="sonar-pro",
messages=[
{"role": "system", "content": "You are a creative writing assistant."},
{"role": "user", "content": "Write a short sci-fi story about time travel"}
],
disable_search=True,
temperature=0.8
)Parameter Recommendations
- Do NOT tune
temperature— defaults are optimized - Do use
search_domain_filterfor trusted sources - Do use
search_context_sizeappropriately: low— simple factsmedium— general use (default)high— comprehensive research
Search API Reference
Real-time ranked web search results with advanced filtering.
Basic Search
from perplexity import Perplexity
client = Perplexity()
search = client.search.create(
query="latest AI developments 2024",
max_results=5,
max_tokens_per_page=2048
)
for result in search.results:
print(f"{result.title}: {result.url}")
print(f"Snippet: {result.snippet[:200]}...")
print(f"Date: {result.date}")Response Structure
{
"results": [
{
"title": "Article Title",
"url": "https://example.com/article",
"snippet": "Content excerpt...",
"date": "2024-01-15",
"last_updated": "2024-01-20"
}
],
"id": "request-uuid"
}Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string/list | required | Search query (up to 5 for multi-query) |
max_results | int | 10 | Results per query (1-20) |
max_tokens_per_page | int | 2048 | Content extraction per page |
max_tokens | int | 25000 | Total content budget (max 1M) |
country | string | - | ISO 3166-1 alpha-2 code |
search_type | string | - | Route specialized searches, e.g. People Search when supported |
search_domain_filter | list | - | Domain allow/denylist (max 20) |
search_language_filter | list | - | ISO 639-1 codes (max 10) |
SDK search notes (0.34.0 -> 0.37.0)
search_typewas added for People Search routing. Use it only when the API account/model surface supports that specialized search path.search_context_sizewas exposed prematurely in0.35.0, removed in0.35.1, and reintroduced in0.37.0for both the Search API and theweb_searchtool. It now controls how much search context is retrieved; pass it onclient.search.create()again, or keep using chat completionsweb_search_options.search_context_size.
Regional Search
search = client.search.create(
query="government renewable energy policies",
country="US", # ISO country code
max_results=5
)Common codes: US, GB, DE, JP, FR, CA, AU
Multi-Query Search
Execute up to 5 queries in single request:
search = client.search.create(
query=[
"artificial intelligence trends 2024",
"machine learning breakthroughs recent",
"AI applications in healthcare"
],
max_results=5
)
# Results grouped by query
for i, query_results in enumerate(search.results):
print(f"Query {i+1}:")
for result in query_results:
print(f" {result.title}")Note: Single query returns flat list; multi-query returns nested lists.
Domain Filtering
Allowlist Mode (include only)
search = client.search.create(
query="climate change research",
search_domain_filter=[
"science.org",
"nature.com",
"pnas.org"
]
)Denylist Mode (exclude)
search = client.search.create(
query="renewable energy innovations",
search_domain_filter=[
"-pinterest.com",
"-reddit.com",
"-quora.com"
]
)Rules:
- Max 20 domains per filter
- Cannot mix allowlist and denylist in same request
- Prefix with
-for denylist
Language Filtering
search = client.search.create(
query="latest AI news",
search_language_filter=["en", "fr", "de"],
max_results=10
)Max 10 language codes per request.
Content Extraction Control
# Comprehensive extraction (slower)
detailed = client.search.create(
query="AI research methodology",
max_results=5,
max_tokens_per_page=2048,
max_tokens=50000
)
# Quick extraction (faster)
brief = client.search.create(
query="AI news headlines",
max_results=10,
max_tokens_per_page=512,
max_tokens=5000
)Recommendations:
max_tokens_per_page: 256-512 for quick retrievalmax_tokens_per_page: 2048+ for deep analysis- Lower values = faster processing
Async Search
import asyncio
from perplexity import AsyncPerplexity
async def batch_search(queries, batch_size=3, delay_ms=1000):
async with AsyncPerplexity() as client:
results = []
for i in range(0, len(queries), batch_size):
batch = queries[i:i + batch_size]
tasks = [
client.search.create(query=q, max_results=5)
for q in batch
]
batch_results = await asyncio.gather(*tasks)
results.extend(batch_results)
if i + batch_size < len(queries):
await asyncio.sleep(delay_ms / 1000)
return resultsRate-Limited Concurrent Search
import asyncio
from perplexity import AsyncPerplexity
class SearchManager:
def __init__(self, max_concurrent=5):
self.semaphore = asyncio.Semaphore(max_concurrent)
async def search_single(self, client, query):
async with self.semaphore:
return await client.search.create(query=query, max_results=5)
async def search_many(self, queries):
async with AsyncPerplexity() as client:
tasks = [self.search_single(client, q) for q in queries]
return await asyncio.gather(*tasks, return_exceptions=True)Caching Pattern
import time
from typing import Dict, Tuple, Optional
class SearchCache:
def __init__(self, ttl_seconds=3600):
self.cache: Dict[str, Tuple[any, float]] = {}
self.ttl = ttl_seconds
def get(self, query: str) -> Optional[any]:
if query in self.cache:
result, timestamp = self.cache[query]
if time.time() - timestamp < self.ttl:
return result
del self.cache[query]
return None
def set(self, query: str, result: any):
self.cache[query] = (result, time.time())
# Usage
cache = SearchCache(ttl_seconds=1800)
def cached_search(client, query):
cached = cache.get(query)
if cached:
return cached
result = client.search.create(query=query)
cache.set(query, result)
return resultError Handling
from perplexity import RateLimitError, APIStatusError
def resilient_search(client, query, max_retries=3):
for attempt in range(max_retries):
try:
return client.search.create(query=query)
except RateLimitError:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
else:
raise
except APIStatusError as e:
print(f"API error: {e}")
return NoneBest Practices
1. Be specific: "AI medical diagnosis accuracy 2024" > "AI medical" 2. Use multi-query: Break research into related sub-queries 3. Request only needed results: More results = longer response 4. Cache static queries: Don't repeat unchanged searches 5. Implement backoff: Handle rate limits gracefully
Structured Outputs Reference
Enforce JSON response formats using JSON Schema.
Basic Usage
from perplexity import Perplexity
from pydantic import BaseModel
class ContactInfo(BaseModel):
email: str
phone: str
client = Perplexity()
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{"role": "user", "content": "Find Tesla IR contact info"}],
response_format={
"type": "json_schema",
"json_schema": {
"schema": ContactInfo.model_json_schema()
}
}
)
contact = ContactInfo.model_validate_json(completion.choices[0].message.content)
print(f"Email: {contact.email}")Response Format Structure
{
"response_format": {
"type": "json_schema",
"json_schema": {
"schema": {
/* your JSON schema */
}
}
}
}Financial Analysis Example
from typing import List, Optional
from pydantic import BaseModel
class FinancialMetrics(BaseModel):
company: str
quarter: str
revenue: float
net_income: float
eps: float
revenue_growth_yoy: Optional[float] = None
key_highlights: Optional[List[str]] = None
completion = client.chat.completions.create(
model="sonar-pro",
messages=[{
"role": "user",
"content": "Analyze Apple's latest quarterly earnings. Extract key metrics."
}],
response_format={
"type": "json_schema",
"json_schema": {"schema": FinancialMetrics.model_json_schema()}
}
)
metrics = FinancialMetrics.model_validate_json(
completion.choices[0].message.content
)
print(f"Revenue: ${metrics.revenue}B")Perplexity vs Other Providers
Simplified syntax — no name or strict fields required:
// Other providers
{
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "my_schema",
"strict": true,
"schema": { ... }
}
}
}
// Perplexity
{
"response_format": {
"type": "json_schema",
"json_schema": {
"schema": { ... }
}
}
}Reasoning Models
With sonar-reasoning-pro, response includes <think> section:
<think>
I need to provide information about France...
Let me format this information as required.
</think>
{"country":"France","capital":"Paris","population":67750000}Parse manually to extract JSON after </think>.
Cold Start Warning
First request with new schema takes 10-30 seconds to prepare.
- May cause timeout errors on first call
- Subsequent requests with same schema are fast
- Consider pre-warming schemas in production
Improve Compliance
Add hints in prompts:
messages = [{
"role": "user",
"content": """Find the contact information for Apple investor relations.
Return as JSON with email and phone fields."""
}]Unsupported Schemas
# ❌ UNSUPPORTED: Recursive schema
class RecursiveJson(BaseModel):
value: str
child: list["RecursiveJson"]
# ❌ UNSUPPORTED: Unconstrained dict
from typing import Any
class UnconstrainedDict(BaseModel):
data: dict[str, Any]Links in JSON Responses
Do NOT request links in JSON structured outputs.
- May result in hallucinations or broken URLs
- Use
citationsorsearch_resultsfrom API response instead
# ❌ BAD: Links in schema
class ResultWithLink(BaseModel):
title: str
url: str # May be hallucinated
# ✅ GOOD: Get links from citations
completion = client.chat.completions.create(...)
citations = completion.citations # Valid URLsBest Practices
1. Use Pydantic — Generate schemas with model_json_schema() 2. Keep schemas simple — Avoid deep nesting 3. No recursion — Flatten recursive structures 4. Type everything — Use specific types, not Any 5. Pre-warm schemas — First request is slow 6. Get links from citations — Don't request URLs in JSON 7. Add prompt hints — Improve schema compliance