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
pip install p2d-duck
Free, no-API-key Python client for DuckDuckGo AI Chat (duck.ai).
Import name: duck_ai.
Features
--------
Single sync client built on httpx
Auto-retry with exponential backoff on challenge failures
Browser-faithful session warm-up — first call succeeds, not the third
6 chat models + image generation (see model table below)
Reasoning effort control (fast / reasoning)
Image generation (text-to-image)
Image edit (caption + source image -> edited image)
Image upload (vision / multimodal)
Web search (opt-in per call, supported models only)
Multi-turn history (off by default, opt-in)
Built-in x-vqd-hash-1 JS challenge solver via mini-racer
CLI: p2d-duck
No account · No API key · No server · No fee
Install
pip install p2d-duck
Requires Python 3.10+.
Models
Alias Model ID Reasoning Vision Web Search
----------- ----------------------------------------- --------- ------ ----------
gpt5_mini gpt-5.4-mini yes yes yes
gpt5_nano gpt-5.4-nano yes yes yes
claude claude-haiku-4-5 yes yes yes
mistral mistral-small-2603 no no no
gpt_oss tinfoil/gpt-oss-120b yes no no
image image-generation no yes no
Pass any alias, full model ID, or ModelType enum member to DuckChat(model=...).
from duck_ai import (
DuckChat,
gpt5_mini, gpt5_nano,
claude, mistral, gpt_oss, image_generation,
)
Quickstart
from duck_ai import DuckChat, gpt5_mini
with DuckChat(model=gpt5_mini) as duck:
print(duck.ask("Explain quantum tunneling in one sentence."))
Streaming
from duck_ai import DuckChat
with DuckChat() as duck:
for chunk in duck.stream("Write a 4-line haiku about ducks."):
print(chunk, end="", flush=True)
Reasoning effort
from duck_ai import DuckChat, gpt5_mini, claude
with DuckChat(model=gpt5_mini, effort="fast") as duck:
print(duck.ask("Quick: 2+2?"))
with DuckChat(model=claude, effort="reasoning") as duck:
print(duck.ask("Solve: I speak without a mouth..."))
Model fast token reasoning token Default
----------- ---------- --------------- -------
gpt5_mini minimal low low
gpt5_nano minimal low low
claude none low low
gpt_oss low low low
mistral (none) (none) (none)
Pass effort="fast" or effort="reasoning". Non-reasoning models silently ignore it.
Multi-turn conversation
History is off by default. Each ask / stream call is a fresh single-turn request
unless you opt in.
from duck_ai import DuckChat, claude
# Per-client: enable at construction
with DuckChat(model=claude, history=True) as duck:
duck.ask("My name is Alice. Remember it.")
print(duck.ask("What is my name?")) # -> Alice
duck.reset() # clear the buffered turns
# Toggle live
with DuckChat(model=claude) as duck:
duck.enable_history()
duck.ask("Pick a number between 1 and 10.")
print(duck.ask("What number did you pick?"))
duck.disable_history() # also clears the buffer
# Per-call override
with DuckChat() as duck:
duck.ask("hi", remember=True) # buffer this turn
duck.ask("ignore me", remember=False) # do not buffer
Web search
from duck_ai import DuckChat, gpt5_mini, model_supports_web_search
assert model_supports_web_search(gpt5_mini)
with DuckChat(model=gpt5_mini) as duck:
print(duck.ask(
"What did Apple announce at WWDC this year?",
web_search=True,
))
Supported on: gpt5_mini, gpt5_nano, claude. Silently ignored for other models.
Image generation
from duck_ai import DuckChat, image_generation
with DuckChat(model=image_generation) as duck:
duck.generate_image(
"a cute rubber duck wearing a wizard hat, digital art",
save_to="duck_wizard.jpg",
)
Image edit
from duck_ai import DuckChat, image_generation
with DuckChat(model=image_generation) as duck:
duck.edit_image(
"make the duck wear a tiny chef hat",
"duck_wizard.jpg",
save_to="duck_chef.jpg",
)
Image upload (multimodal vision)
from duck_ai import DuckChat, ImagePart
with DuckChat() as duck:
print(duck.ask_with_image("What is in this image?", "photo.jpg"))
print(duck.ask([
"Compare these two images:",
ImagePart.from_path("a.png"),
ImagePart.from_path("b.png"),
]))
If the selected model has no vision capability, the request is automatically
routed to a vision-capable model (gpt-5.4-mini).
CLI
p2d-duck interactive REPL
p2d-duck chat "Hello, who are you?"
p2d-duck -m claude chat "Hi Claude!"
p2d-duck -m gpt5_mini -e reasoning chat "Solve x^2 - 5x + 6 = 0"
p2d-duck chat "Describe this" --image cat.jpg
p2d-duck -m gpt5_mini chat "Top news today" --web-search
p2d-duck image "a watercolor moon over a lake" -o moon.jpg
p2d-duck edit "make the cat wear sunglasses" --image cat.jpg -o cat_cool.jpg
p2d-duck models
REPL commands: /reset, /history on|off, /quit
Reliability
1. Session warm-up — visits duck.ai homepage on construction so cookies are
present before the first chat request.
2. Challenge rotation — captures x-vqd-hash-1 from each response and reuses it
for the next call, matching the browser client behaviour.
3. Retry loop — on ChallengeError, RemoteProtocolError, RateLimitError,
dropped streams, or an empty SSE response: re-fetches the
challenge and retries with exponential backoff + jitter.
4. Terminal errors — ConversationLimitError is never retried.
5. Real RSA key — uses cryptography to generate a real RSA-OAEP-256 JWK for
durable streams; refuses to fall back to a fake key.
duck = DuckChat(max_retries=4, backoff_base=0.6)
Exceptions
Exception When
-------------------- -----------------------------------------------
DuckChatError Generic error — base class for all below.
ChallengeError Could not solve the x-vqd-hash-1 JS challenge.
RateLimitError HTTP 429 from the server.
ConversationLimitError Too many turns in one session (terminal).
APIError Any other non-200 response (.status_code, .body).
How it works
DuckDuckGo's AI Chat backend (duck.ai/duckchat/v1/*) requires a per-request
proof-of-work challenge encoded in the x-vqd-hash-1 header. The server returns
an obfuscated JavaScript snippet that must be evaluated in a browser-like environment.
1. stubs.js — minimal browser-DOM shim injected before the challenge script.
2. mini-racer — embedded V8 isolate (no external Node.js install required).
3. SHA-256 — hashes the fingerprint values produced by the challenge.
4. RSA-OAEP-256 — real public key for durable (resumable) streams.
License
MIT. See LICENSE.
Disclaimer
Unofficial reverse-engineered client. Not affiliated with or endorsed by DuckDuckGo. Use at your own risk and respect duck.ai's terms of service. The backend may change at any time.
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.0.tar.gz.
File metadata
- Download URL: p2d_duck-1.3.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e79ed4ea49b625701638c0dcf7f271be381df925ad8c3c356834c14e425cba8
|
|
| MD5 |
e35b39bdc417918a6650b62a37e27845
|
|
| BLAKE2b-256 |
5f94b398cd3c6dca057ec34d12a11bc8d48629ea4b3b00ad921ca6aaa5068e79
|
Provenance
The following attestation bundles were made for p2d_duck-1.3.0.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.0.tar.gz -
Subject digest:
5e79ed4ea49b625701638c0dcf7f271be381df925ad8c3c356834c14e425cba8 - Sigstore transparency entry: 1935800946
- Sigstore integration time:
-
Permalink:
pooraddyy/p2d-duck@eba1f94fb8653ca6541c6cf0310528dbe59b3fce -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/pooraddyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eba1f94fb8653ca6541c6cf0310528dbe59b3fce -
Trigger Event:
push
-
Statement type:
File details
Details for the file p2d_duck-1.3.0-py3-none-any.whl.
File metadata
- Download URL: p2d_duck-1.3.0-py3-none-any.whl
- Upload date:
- Size: 21.1 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 |
91d5d988c6b321916b60bffd220544ad5b5be1ba58a8cf4a0e056e719f3cb821
|
|
| MD5 |
f8d7cfe434e192d3c84871526f0b65e6
|
|
| BLAKE2b-256 |
4cda1f4a2f4de93cb58ec3eefff40d0f212ab3c0ef88be566543e3ddadfa3f93
|
Provenance
The following attestation bundles were made for p2d_duck-1.3.0-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.0-py3-none-any.whl -
Subject digest:
91d5d988c6b321916b60bffd220544ad5b5be1ba58a8cf4a0e056e719f3cb821 - Sigstore transparency entry: 1935800962
- Sigstore integration time:
-
Permalink:
pooraddyy/p2d-duck@eba1f94fb8653ca6541c6cf0310528dbe59b3fce -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/pooraddyy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eba1f94fb8653ca6541c6cf0310528dbe59b3fce -
Trigger Event:
push
-
Statement type: