Skip to content
Open
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: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ After your recipe is in place, add an entry to two places so it shows up everywh

Use lowercase, hyphenated tags drawn from this controlled vocabulary so filters stay clean:

- **API surface**: `search`, `extract`, `task`, `deep-research`, `ingest`, `mcp`, `webhooks`, `sse`, `oauth`
- **API surface**: `responses`, `search`, `extract`, `task`, `deep-research`, `ingest`, `mcp`, `webhooks`, `sse`, `oauth`
- **Stack**: `cloudflare`, `vercel`, `nextjs`, `supabase`, `vertex-ai`, `python`, `typescript`, `temporal`
- **Pattern**: `agent`, `enrichment`, `monitoring`, `realtime`, `batch`, `fact-checking`, `template`, `entity-resolution`

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

---

The Parallel Cookbook is a curated set of recipes that show how to build with Parallel's web research stack — **Search**, **Extract**, **Task**, **Ingest**, and **MCP**. Each recipe is a working example with a clear way to run or deploy it and prose that explains the design decisions.
The Parallel Cookbook is a curated set of recipes that show how to build with Parallel's web research stack — **Responses**, **Search**, **Extract**, **Task**, **Ingest**, and **MCP**. Each recipe is a working example with a clear way to run or deploy it and prose that explains the design decisions.

> **New here?** Start with the [Vercel Template](typescript-recipes/parallel-vercel-template) (TypeScript) or the [Deep Research notebook](python-recipes/Deep_Research_Recipe.ipynb) (Python) for an end-to-end tour of the platform.

Expand Down Expand Up @@ -71,14 +71,15 @@ print(run.output)

## Recipes by Category

Every recipe in this repo is listed below, grouped by what you're trying to build. Looking for a specific API surface? Search for the badge — `Search`, `Extract`, `Task`, `SSE`, `Webhooks`, `MCP`, `OAuth`, `Ingest`.
Every recipe in this repo is listed below, grouped by what you're trying to build. Looking for a specific API surface? Search for the badge — `Responses`, `Search`, `Extract`, `Task`, `SSE`, `Webhooks`, `MCP`, `OAuth`, `Ingest`.

### Templates & Starters

Multi-API starter projects — fork these first.
Quickstarts and starter projects — copy these first.

| Recipe | Description | APIs | Stack | Demo |
| --- | --- | --- | --- | --- |
| [**Responses API Quickstart**](python-recipes/parallel-responses-quickstart) | Ask a current multi-source question with the OpenAI SDK and print a cited, web-grounded answer from Parallel. | `Responses` | Python · OpenAI SDK | – |
| [**Vercel Template**](typescript-recipes/parallel-vercel-template) | Next.js demo of Search + Extract + Tasks with SSE in a single app. Includes Vercel marketplace integration for one-click API key. | `Search` `Extract` `Task` `SSE` | Next.js · Vercel | [Live](https://parallel-vercel-template-cookbook.vercel.app/) · [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fparallel-web%2Fparallel-cookbook%2Ftree%2Fmain%2Ftypescript-recipes%2Fparallel-vercel-template&project-name=parallel-vercel-template&repository-name=parallel-vercel-template&integration-ids=oac_qjiYAM8BTtX0UDS6HEPY97nU) |

### Agents & Search
Expand Down
1 change: 1 addition & 0 deletions python-recipes/parallel-responses-quickstart/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PARALLEL_API_KEY=your-api-key-here
21 changes: 21 additions & 0 deletions python-recipes/parallel-responses-quickstart/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Parallel Web Systems Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
145 changes: 145 additions & 0 deletions python-recipes/parallel-responses-quickstart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Parallel Responses API Quickstart

Ask a current, multi-source question with the OpenAI SDK and print a cited,
web-grounded answer from Parallel.

## What it shows

- Use the standard OpenAI Python SDK with Parallel's Responses API.
- Choose the `parallel` model and an explicit reasoning effort.
- Read the answer from `response.output_text`.
- Extract and deduplicate standard `url_citation` annotations.

The first run requires only a Parallel API key. You do not need an OpenAI key.

## Architecture

```text
Your question
OpenAI SDK ── base_url=https://api.parallel.ai/v1
Parallel Responses API ── live web research at medium effort
Answer text + URL citation annotations
```

## Quick Start

Prerequisites: Python 3.10+ and [`uv`](https://docs.astral.sh/uv/).

```bash
cd python-recipes/parallel-responses-quickstart
uv sync
export PARALLEL_API_KEY="your-api-key-here"
uv run python quickstart.py
```

The default question compares NVIDIA and AMD's most recently reported
quarterly data-center revenue. The command prints the grounded answer followed
by a numbered source list:

```text
<grounded comparison of the latest reported quarters>

Sources:
1. <source title> — https://...
2. <source title> — https://...
```

Pass a different question as the optional positional argument:

```bash
uv run python quickstart.py \
"What changed in the latest stable Python release? Cite the release notes."
```

## The API call

The request itself uses the stock OpenAI SDK:

```python
import os

from openai import OpenAI

client = OpenAI(
api_key=os.environ["PARALLEL_API_KEY"],
base_url="https://api.parallel.ai/v1",
)

response = client.responses.create(
model="parallel",
input=prompt,
reasoning={"effort": "medium"},
)

print(response.output_text)
```

Parallel exposes one model, `parallel`. Set `reasoning.effort` to `low`,
`medium`, or `high` to choose the research depth. This quickstart uses
`medium` explicitly.

## Reading citations

Citations are standard Responses API `url_citation` annotations attached to
output-text content parts. `parse_response()` walks every output item and
deduplicates citations by URL before rendering the source list.

The script makes exactly one billed Responses call. It reports a clear contract
error if that call returns no answer or no URL citations; it does not hide the
failure behind additional research requests.

## Migrating an OpenAI Responses call

If your application already uses `client.responses.create(...)`, the core
migration is a small configuration diff:

```diff
- client = OpenAI(api_key=OPENAI_API_KEY)
+ client = OpenAI(
+ api_key=PARALLEL_API_KEY,
+ base_url="https://api.parallel.ai/v1",
+ )

response = client.responses.create(
- model="gpt-5.4-mini",
+ model="parallel",
input=prompt,
reasoning={"effort": "medium"},
- tools=[{"type": "web_search"}],
)
```

Parallel performs web research automatically, so callers do not configure a
separate `web_search` tool.

## Verification

Run the deterministic tests, which use local fixtures and make no network
calls:

```bash
uv run pytest -m "not live"
```

The billed live smoke test is doubly opt-in: it requires both a key and an
explicit flag, and it makes exactly one Responses call.

```bash
PARALLEL_API_KEY="your-api-key-here" \
RUN_LIVE_TESTS=1 \
uv run pytest -m live -v
```

The live test verifies that the response mentions both companies and includes
at least one URL citation. It intentionally does not freeze financial figures
that will become stale.

## License

MIT
25 changes: 25 additions & 0 deletions python-recipes/parallel-responses-quickstart/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[project]
name = "parallel-responses-quickstart"
version = "0.1.0"
description = "Minimal cited research with Parallel's OpenAI-compatible Responses API"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
dependencies = [
"openai>=2.0.0,<3",
]

[dependency-groups]
dev = [
"pytest>=8.0.0,<10",
]

[tool.uv]
package = false

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
markers = [
"live: opt-in test that calls the billed Parallel API",
]
104 changes: 104 additions & 0 deletions python-recipes/parallel-responses-quickstart/quickstart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Minimal cited research with Parallel's OpenAI-compatible Responses API."""

from __future__ import annotations

import argparse
import os
import sys

from openai import OpenAI
from openai.types.responses import Response

PARALLEL_BASE_URL = "https://api.parallel.ai/v1"
DEFAULT_PROMPT = (
"Compare NVIDIA and AMD's most recently reported quarterly data-center revenue. "
"Include each reporting period, explain the main difference, and cite your sources."
)

Citation = tuple[str, str]


def create_response(client: OpenAI, prompt: str) -> Response:
"""Run one medium-effort, web-grounded Responses request."""
if not prompt.strip():
raise ValueError("The research prompt cannot be empty.")

return client.responses.create(
model="parallel",
input=prompt,
reasoning={"effort": "medium"},
)


def parse_response(response: Response) -> tuple[str, list[Citation]]:
"""Return the answer and unique URL citations from a completed response."""
answer = response.output_text.strip()
if not answer:
raise ValueError("Parallel returned an empty answer.")

citations: list[Citation] = []
seen_urls: set[str] = set()
for output in response.output:
if output.type != "message":
continue
for part in output.content:
if part.type != "output_text":
continue
for annotation in part.annotations:
if annotation.type != "url_citation":
continue
url = annotation.url.strip()
if not url or url in seen_urls:
continue
citations.append((annotation.title.strip() or "Source", url))
seen_urls.add(url)

if not citations:
raise ValueError("Parallel returned an answer without URL citations.")
return answer, citations


def render_result(answer: str, citations: list[Citation]) -> str:
"""Render a terminal-friendly answer followed by its sources."""
source_lines = [
f"{index}. {title} — {url}"
for index, (title, url) in enumerate(citations, start=1)
]
return "\n".join([answer, "", "Sources:", *source_lines])


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Ask Parallel a current question and print its cited answer."
)
parser.add_argument(
"prompt",
nargs="?",
default=DEFAULT_PROMPT,
help="Research question. Defaults to a current NVIDIA/AMD comparison.",
)
args = parser.parse_args(argv)

api_key = os.environ.get("PARALLEL_API_KEY")
if not api_key:
print(
"Error: PARALLEL_API_KEY is not set. "
"Export a key from https://platform.parallel.ai.",
file=sys.stderr,
)
return 1

client = OpenAI(api_key=api_key, base_url=PARALLEL_BASE_URL)
try:
response = create_response(client, args.prompt)
answer, citations = parse_response(response)
except ValueError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1

print(render_result(answer, citations))
return 0


if __name__ == "__main__":
raise SystemExit(main())
32 changes: 32 additions & 0 deletions python-recipes/parallel-responses-quickstart/tests/test_live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

import pytest

from openai import OpenAI

from quickstart import (
DEFAULT_PROMPT,
PARALLEL_BASE_URL,
create_response,
parse_response,
)


@pytest.mark.live
def test_responses_api_returns_a_cited_answer() -> None:
if os.environ.get("RUN_LIVE_TESTS") != "1":
pytest.skip("set RUN_LIVE_TESTS=1 to opt into the billed live test")
if not os.environ.get("PARALLEL_API_KEY"):
pytest.skip("PARALLEL_API_KEY is required for the billed live test")

client = OpenAI(
api_key=os.environ["PARALLEL_API_KEY"],
base_url=PARALLEL_BASE_URL,
)
response = create_response(client, DEFAULT_PROMPT)
answer, citations = parse_response(response)

assert "NVIDIA" in answer.upper()
assert "AMD" in answer.upper()
assert citations
assert all(url.startswith(("http://", "https://")) for _, url in citations)
Loading
Loading