Skip to main content

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

p2d_duck-1.3.1.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

p2d_duck-1.3.1-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

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

Hashes for p2d_duck-1.3.1.tar.gz
Algorithm Hash digest
SHA256 a41e834842a648885ae9895447d29db181b7ff87081ed308f3e92cb08442dd28
MD5 4217061a91d66cfac9e7166a2ed34a6a
BLAKE2b-256 27eea09500cfe200f2818d5c6757eca6faadb3bd515d935a50595ad204b3cf1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for p2d_duck-1.3.1.tar.gz:

Publisher: publish.yml on pooraddyy/p2d-duck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for p2d_duck-1.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 eecd006acfe394caf518a764fab2aa2699a7ed8db9be119089e53149f309e466
MD5 4e6e515b0f5371bc06d2d39ae9abb73e
BLAKE2b-256 297467d69b8c9b01714e637ed51a8539d417926656d2301abf7781da492c302a

See more details on using hashes here.

Provenance

The following attestation bundles were made for p2d_duck-1.3.1-py3-none-any.whl:

Publisher: publish.yml on pooraddyy/p2d-duck

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page