p2d-duck
Unofficial Python client for duck.ai — DuckDuckGo's free, no-account AI chat interface. Supports every active model, streaming, web search, image generation, retry logic, and multi-turn conversation.
pip install p2d-duck
Models
| Constant | Model ID | Effort modes | Web search | Image gen |
|---|---|---|---|---|
gpt5_mini |
gpt-5.4-mini |
fast · reasoning | yes | — |
gpt5_nano |
gpt-5.4-nano |
fast | yes | — |
claude |
claude-haiku-4-5 |
fast · reasoning | yes | — |
mistral |
mistral-small-2603 |
fast | yes | — |
gpt_oss |
tinfoil/gpt-oss-120b |
fast · reasoning | yes | — |
image_generation |
image-generation |
— | — | yes |
Installation
Python 3.10 or later is required.
pip install p2d-duck
Dependencies installed automatically: httpx, py-mini-racer, cryptography, html5lib.
Quick start
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini) as chat:
response = chat.ask("What is the speed of light?")
print(response)
Usage
Single question
from duck_ai import DuckChat, claude
with DuckChat(model=claude) as chat:
print(chat.ask("Explain quantum entanglement in one paragraph."))
Multi-turn conversation
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini) as chat:
chat.ask("My name is Ada.")
print(chat.ask("What is my name?"))
Streaming
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini) as chat:
for token in chat.stream("Write a haiku about rain."):
print(token, end="", flush=True)
print()
Reasoning mode
Activates extended chain-of-thought on supported models (gpt5_mini, claude, gpt_oss):
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini, effort="reasoning") as chat:
print(chat.ask("Prove that the square root of 2 is irrational."))
Fast mode
Lower-latency responses with reduced reasoning, available on all models:
from duck_ai import DuckChat, gpt5_nano
with DuckChat(model=gpt5_nano, effort="fast") as chat:
print(chat.ask("Capital of France?"))
Web search
Injects live DuckDuckGo search results into the context before responding:
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini) as chat:
print(chat.ask("What happened in the news today?", web_search=True))
Image generation
Returns raw image bytes (PNG/JPEG depending on the service response):
from duck_ai import DuckChat, image_generation
with DuckChat(model=image_generation) as chat:
data = chat.generate_image("a red fox in autumn leaves, oil painting style")
with open("fox.png", "wb") as f:
f.write(data)
API reference
DuckChat
DuckChat(
model: str | Model = gpt5_mini,
effort: str | None = None, # None | "fast" | "reasoning"
max_retries: int = 3,
timeout: float = 60.0,
)
| Method | Returns | Description |
|---|---|---|
ask(prompt, *, web_search=False) |
str |
Blocking single-turn response |
stream(prompt, *, web_search=False) |
Iterator[str] |
Token-by-token generator |
generate_image(prompt) |
bytes |
Raw image bytes |
reset() |
None |
Clear conversation history |
Use as a context manager (with DuckChat(...) as chat:) or call .close() when done.
Model constants
from duck_ai import (
gpt5_mini, # gpt-5.4-mini (default)
gpt5_nano, # gpt-5.4-nano
claude, # claude-haiku-4-5
mistral, # mistral-small-2603
gpt_oss, # tinfoil/gpt-oss-120b
image_generation, # image-generation
)
You can also pass the raw model ID string directly:
DuckChat(model="claude-haiku-4-5")
List all known model IDs at runtime:
from duck_ai.models import list_models
print(list_models())
Retry behaviour
The client automatically retries on transient failures (challenge errors, 5xx server errors).
RateLimitError and ConversationLimitError are terminal — they are not retried.
attempt 1 --[ChallengeError]--> re-solve challenge --> attempt 2 --> ...
max_retries
Disable retries by setting max_retries=1.
Error reference
| Exception | Condition |
|---|---|
DuckChatError |
Base class for all library exceptions |
ChallengeError |
JS challenge solve failed or was rejected by the server |
RateLimitError |
HTTP 429 — too many requests from this IP |
ConversationLimitError |
Session exceeded duck.ai's per-conversation message cap |
APIError |
Any other non-retryable HTTP error from the server |
from duck_ai.exceptions import RateLimitError, ChallengeError
try:
response = chat.ask("Hello")
except RateLimitError:
print("Rate limited — wait before retrying or use a different IP.")
except ChallengeError:
print("Challenge failed — update py-mini-racer or open an issue.")
Command-line interface
python -m duck_ai [--model MODEL] [--effort EFFORT] [--no-stream] [--web]
| Flag | Description |
|---|---|
--model |
Model name or ID string (default: gpt-5.4-mini) |
--effort |
fast or reasoning (optional) |
--no-stream |
Print the complete response instead of streaming tokens |
--web |
Enable web search on each message |
# Interactive reasoning session with Claude
python -m duck_ai --model claude-haiku-4-5 --effort reasoning
# Web-aware session with the default model
python -m duck_ai --web
Architecture
duck.ai requires solving a JavaScript proof-of-work challenge before each session.
p2d-duck handles this automatically using py-mini-racer,
a V8-based JS runtime, together with a DOM stub layer (stubs.js) that emulates the
browser APIs the challenge script depends on.
ask("...")
│
▼
┌─────────────────────────────────────────────────────────┐
│ DuckChat │
│ │
│ 1. GET /duckchat/v1/status │──► duck.ai
│ ◄── x-vqd-hash-1 (base64 JS challenge) │
│ │
│ 2. challenge.solve_challenge(js, user_agent) │
│ ├─ decode base64 payload │
│ ├─ inject DOM stubs (stubs.js) │
│ ├─ evaluate in V8 (py-mini-racer) │
│ └─ SHA-256 hash result → token │
│ │
│ 3. POST /duckchat/v1/chat │──► duck.ai
│ x-vqd-hash-1: <solved token> │
│ ◄── SSE stream of tokens │
│ │
│ 4. assemble and return │
└─────────────────────────────────────────────────────────┘
│
▼
response string
The challenge payload is a self-contained obfuscated JS function that fingerprints DOM
geometry (getBoundingClientRect, offsetWidth, offsetHeight, getComputedStyle) and
navigator properties (webdriver, userAgent). stubs.js supplies realistic values for
each property so the challenge computes the same hash a real browser would.
Rate limits
duck.ai enforces per-IP rate limits.
If you receive a RateLimitError, wait a few minutes before retrying.
Datacenter and cloud IPs are throttled more aggressively than residential IPs.
Requirements
| Package | Minimum | Purpose |
|---|---|---|
httpx |
0.27 | HTTP/2 client with streaming support |
py-mini-racer |
0.12 | Embedded V8 engine for JS challenge solving |
cryptography |
42 | Token signing and encryption |
html5lib |
1.1 | HTML normalisation used in challenge pre-processing |
License
MIT — see LICENSE.
Disclaimer
This project is not affiliated with, endorsed by, or supported by DuckDuckGo. It interfaces with duck.ai's public web UI. DuckDuckGo's terms of service apply. The upstream API may change without notice; open an issue if something breaks.
Release files for p2d-duck 1.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| p2d_duck-1.3.1.tar.gz | 20.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| p2d_duck-1.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.5 kB
Release files / p2d_duck-1.3.1.tar.gz
| Download URL | p2d_duck-1.3.1.tar.gz |
|---|---|
| Size | 20.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a41e834842a648885ae9895447d29db181b7ff87081ed308f3e92cb08442dd28
|
|
BLAKE2b-256 checksum How to use checksums |
27eea09500cfe200f2818d5c6757eca6faadb3bd515d935a50595ad204b3cf1b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency logRelease files / p2d_duck-1.3.1-py3-none-any.whl
| Download URL | p2d_duck-1.3.1-py3-none-any.whl |
|---|---|
| Size | 21.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eecd006acfe394caf518a764fab2aa2699a7ed8db9be119089e53149f309e466
|
|
BLAKE2b-256 checksum How to use checksums |
297467d69b8c9b01714e637ed51a8539d417926656d2301abf7781da492c302a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 24, 2026.
Transparency log