Metadata-Version: 2.4
Name: p2d-duck
Version: 1.3.1
Summary: 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-URL: Homepage, https://github.com/pooraddyy/p2d-duck
Project-URL: Repository, https://github.com/pooraddyy/p2d-duck
Project-URL: Issues, https://github.com/pooraddyy/p2d-duck/issues
Author: duck-ai contributors
License: MIT
License-File: LICENSE
Keywords: ai,chatbot,chatgpt,claude,duck.ai,duckchat,duckduckgo,image-generation,llm,mistral,multimodal
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=41
Requires-Dist: html5lib>=1.1
Requires-Dist: httpx>=0.27
Requires-Dist: mini-racer>=0.12
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# p2d-duck

Unofficial Python client for [duck.ai](https://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.

```bash
pip install p2d-duck
```

Dependencies installed automatically: `httpx`, `py-mini-racer`, `cryptography`, `html5lib`.

---

## Quick start

```python
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

```python
from duck_ai import DuckChat, claude

with DuckChat(model=claude) as chat:
    print(chat.ask("Explain quantum entanglement in one paragraph."))
```

### Multi-turn conversation

```python
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

```python
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`):

```python
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:

```python
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:

```python
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):

```python
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`

```python
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

```python
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:

```python
DuckChat(model="claude-haiku-4-5")
```

List all known model IDs at runtime:

```python
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 |

```python
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 |

```bash
# 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](https://github.com/sqreen/PyMiniRacer),
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](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.
