An OpenAI-compatible API for Duck.ai β driven by a real browser.
duckai-api opens Duck.ai in a real Chromium session via Playwright, types your prompt into the
composer, streams the answer back token by token, and re-exposes it as a standard
/v1/chat/completions endpoint. Any OpenAI SDK, Chatbox, Cherry Studio,
Open WebUI or Cursor client can talk to it with nothing but a base URL.
What it is
Duck.ai publishes no public API. This project drives the ordinary web interface in a real browser and repackages the stream as an OpenAI-compatible surface.
Drop-in compatibility
Speaks the standard /v1/chat/completions and /v1/models contract β streaming and non-streaming.
Real token streaming
Intercepts Duck.ai's SSE feed and forwards deltas as OpenAI chunks with zero buffering.
Multiple models
GPT-6 Luna, Claude Haiku 4.5, GPT-5.4 mini, Mistral Small 4, GPT-OSS 120B and Gemma 4 31B β with friendly aliases.
Bundled web GUI
A dark ChatGPT-style interface at /, with an optional API-key field and live model picker.
Memory recycling
The browser page is recycled automatically after N requests or a time interval to keep long runs healthy.
Hardened by default
Optional API key, constant-time comparison, compliant CORS, generation-scoped stream routing and disconnect handling.
Running it locally
Python 3.11 or newer (the bridge uses asyncio.timeout).
-
Install dependencies
Create a virtual environment, then install the Python packages and the Chromium build Playwright needs.
terminal# 1. install packages pip install -r requirements.txt # 2. download the Chromium binary playwright install chromium -
Start the server
Defaults to
http://localhost:3006. The web GUI is at/, the schema at/docs.terminalpython server.py -
Point any OpenAI client at it
Use
http://localhost:3006/v1as the base URL. IfAPI_KEYis unset, any key value is accepted.pythonfrom openai import OpenAI client = OpenAI(base_url="http://localhost:3006/v1", api_key="duckai") stream = client.chat.completions.create( model="gpt-6-luna", messages=[{"role": "user", "content": "Explain quantum computing in one sentence."}], stream=True, ) for chunk in stream: print(chunk.choices[0].delta.content or "", end="", flush=True)
API endpoints
All endpoints live under the same host and port as the GUI.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /v1/chat/completions | Chat completion (streaming & non-streaming) | if key set |
| GET | /v1/models | List available models | if key set |
| GET | /health | Health & bridge status | open |
| GET | / | Web chat GUI | open |
| GET | /docs | OpenAPI schema (Swagger UI) | open |
Streaming a completion
curl -N -X POST http://localhost:3006/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{
"model": "gpt-6-luna",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'
Tip: pass "stream": false to receive a single JSON
chat.completion object instead of SSE chunks. Model names are validated β an
unknown model returns HTTP 400.
Available models
Aliases on the right are accepted too and resolved to the canonical model.
| Model ID | Description | Aliases |
|---|---|---|
gpt-6-luna | Flagship β best for everyday use | gpt-6, luna, default |
claude-haiku-4.5 | Fast, concise reasoning | claude-haiku, claude-3-5-haiku, claude |
gpt-5.4-mini | Lightweight and fast | gpt-4o-mini, gpt-4o, gpt-3.5-turbo |
mistral-small-4 | Efficient multilingual | mistral |
gpt-oss-120b | Open-weights large model | oss-120b |
gemma-4-31b | Google open model (beta) | gemma |
Environment variables
Everything is optional β sensible defaults ship in the Docker image.
| Variable | Default | Description |
|---|---|---|
PORT | 3006 | HTTP port |
HOST | 0.0.0.0 | Bind host |
HEADLESS | true | Set false to show the browser window |
API_KEY | unset | If set, every /v1/* request must send Authorization: Bearer <key> |
RECYCLE_AFTER_REQUESTS | 100 | Recycle the browser page after this many requests |
RECYCLE_INTERVAL_HOURS | 24 | Recycle the browser page after this many hours |
Docker
# build & run with compose
docker compose up --build
# β¦or plain docker
docker build -t duckai-api .
docker run -p 3006:3006 -e API_KEY="your-secret" duckai-api
The image is based on python:3.11-slim and ships Chromium plus its
system libraries, so no host browser is required.
Hardening notes
This project was audited adversarially; the findings below have been addressed in the current revision.
Generation-scoped routing
Every stream frame is tagged with a generation id, so a superseded request can never leak tokens into the next one.
Sanitized rendering
Model output is rendered through DOMPurify after markdown parsing, closing the stored-XSS path in the GUI.
Constant-time auth
API keys are compared with hmac.compare_digest over the Authorization header only.
Bounded lock acquisition
A cancel-safe 30-second lock acquisition prevents a stalled request from blocking the bridge forever.
Disconnect handling
Client disconnects stop the browser generation instead of letting it run on in the background.
Error propagation
Upstream Duck.ai error frames surface as stream errors rather than empty "successful" responses.
Still your responsibility: when API_KEY
is unset the API is open, and /health, /docs and the GUI
remain unauthenticated. Bind to a trusted network, or set a key and put it behind a reverse proxy.
β οΈ Disclaimer & legal notice
This project is unofficial and is not affiliated with, endorsed by, or sponsored by Duck.ai or DuckDuckGo. It is an independent tool built for educational, research and interoperability purposes.
Duck.ai publishes no public API. This software works by automating the ordinary web interface in a real browser β it types into the composer and reads the page. Automating a consumer service is exactly the kind of activity that terms of service commonly prohibit.
- You are responsible for compliance. Whether you may use this is between you and Duck.ai, and the law where you live. If using it would violate Duck.ai's Terms of Service, do not use it.
- No warranty. The software is provided "as is", without warranty of any kind. Models, DOM selectors and upstream behaviour change without notice and may break at any time.
- Use at your own risk. Traffic originates from your machine or server IP; misusing the relay can result in rate limits, account or IP bans, and other consequences.
- No secrets shipped. Never commit real API keys. Treat any key you configure as a credential and keep the service off the public internet unless you understand the exposure.