Free Python client for DuckDuckGo AI Chat (duck.ai). Sync, streaming, image generation, image edit, multimodal vision, web search. Auto-retry on challenge failures. No API key required.
Project description
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file p2d_duck-1.3.1.tar.gz.
File metadata
- Download URL: p2d_duck-1.3.1.tar.gz
- Upload date:
- Size: 20.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a41e834842a648885ae9895447d29db181b7ff87081ed308f3e92cb08442dd28
|
|
| MD5 |
4217061a91d66cfac9e7166a2ed34a6a
|
|
| BLAKE2b-256 |
27eea09500cfe200f2818d5c6757eca6faadb3bd515d935a50595ad204b3cf1b
|
Provenance
The following attestation bundles were made for p2d_duck-1.3.1.tar.gz:
Publisher:
publish.yml on pooraddyy/p2d-duck
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
p2d_duck-1.3.1.tar.gz -
Subject digest:
a41e834842a648885ae9895447d29db181b7ff87081ed308f3e92cb08442dd28 - Sigstore transparency entry: 1935902047
- Sigstore integration time:
-
Permalink:
pooraddyy/p2d-duck@337bf84aa9b0e00ed47599f04e05b201e9632c1f -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/pooraddyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@337bf84aa9b0e00ed47599f04e05b201e9632c1f -
Trigger Event:
push
-
Statement type:
File details
Details for the file p2d_duck-1.3.1-py3-none-any.whl.
File metadata
- Download URL: p2d_duck-1.3.1-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eecd006acfe394caf518a764fab2aa2699a7ed8db9be119089e53149f309e466
|
|
| MD5 |
4e6e515b0f5371bc06d2d39ae9abb73e
|
|
| BLAKE2b-256 |
297467d69b8c9b01714e637ed51a8539d417926656d2301abf7781da492c302a
|
Provenance
The following attestation bundles were made for p2d_duck-1.3.1-py3-none-any.whl:
Publisher:
publish.yml on pooraddyy/p2d-duck
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
p2d_duck-1.3.1-py3-none-any.whl -
Subject digest:
eecd006acfe394caf518a764fab2aa2699a7ed8db9be119089e53149f309e466 - Sigstore transparency entry: 1935902086
- Sigstore integration time:
-
Permalink:
pooraddyy/p2d-duck@337bf84aa9b0e00ed47599f04e05b201e9632c1f -
Branch / Tag:
refs/tags/v1.3.1 - Owner: https://github.com/pooraddyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@337bf84aa9b0e00ed47599f04e05b201e9632c1f -
Trigger Event:
push
-
Statement type: