Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions agentkthx/cli/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"cf": "Cloudflare",
"siliconflow": "SiliconFlow",
"sf": "SiliconFlow",
"duckduckgo": "DuckDuckGo",
"ddg": "DuckDuckGo",
}


Expand Down Expand Up @@ -244,6 +246,24 @@
"RESERVED for a future free tier — unused today (no free models; "
"default: Qwen/Qwen3-8B)",
),
# ── DuckDuckGo AI Chat ──
(
"DUCKDUCKGO_BASE_URL",
"DuckDuckGo AI Chat URL (default: https://duckduckgo.com — the "
"protocol lives under /duckchat/v1)",
),
(
"DUCKDUCKGO_USER_AGENT",
"Browser User-Agent override — DDG 403s non-browser UAs; empty = "
"built-in Chrome 120 UA (only set if DDG rejects the built-in one)",
),
(
"DUCKDUCKGO_DEFAULT_MODEL",
"Default model (default: gpt-4o-mini; catalog: gpt-4o-mini, "
"claude-3-haiku-20240307, meta-llama/Llama-3.3-70B-Instruct-Turbo, "
"mistralai/Mistral-Small-24B-Instruct-2501, o3-mini — no API key "
"needed, the backend is keyless and anonymous)",
),
# ── ACP / TurboQuant ──
("ACP_BASE_URL", "ACP server URL"),
("ACP_USER", "ACP username (default: admin)"),
Expand Down Expand Up @@ -286,6 +306,9 @@ def cmd_config(args: argparse.Namespace) -> int:
CLOUDFLARE_FREE_ONLY,
DEBUG,
DEFAULT_MODEL,
DUCKDUCKGO_BASE_URL,
DUCKDUCKGO_DEFAULT_MODEL,
DUCKDUCKGO_USER_AGENT,
GEMINI_API_KEY,
GEMINI_BASE_URL,
GEMINI_DEFAULT_MODEL,
Expand Down Expand Up @@ -374,6 +397,7 @@ def cmd_config(args: argparse.Namespace) -> int:
("NVIDIA_BASE_URL", NVIDIA_BASE_URL),
("CLOUDFLARE_BASE_URL", CLOUDFLARE_BASE_URL or "(built from CLOUDFLARE_ACCOUNT_ID)"),
("SILICONFLOW_BASE_URL", SILICONFLOW_BASE_URL),
("DUCKDUCKGO_BASE_URL", DUCKDUCKGO_BASE_URL),
("ACP_BASE_URL", ACP_BASE_URL),
]
for name, val in urls:
Expand All @@ -400,6 +424,7 @@ def cmd_config(args: argparse.Namespace) -> int:
("MISTRAL_BASE_URL", MISTRAL_BASE_URL),
("ORCAROUTER_BASE_URL", ORCAROUTER_BASE_URL),
("POLLINATIONS_BASE_URL", POLLINATIONS_BASE_URL),
("DUCKDUCKGO_BASE_URL", DUCKDUCKGO_BASE_URL),
("ACP_BASE_URL", ACP_BASE_URL),
],
"OpenRouter": [
Expand Down Expand Up @@ -480,6 +505,14 @@ def cmd_config(args: argparse.Namespace) -> int:
("SILICONFLOW_FREE_ONLY", str(SILICONFLOW_FREE_ONLY)),
("SILICONFLOW_FREE_FALLBACK_MODEL", SILICONFLOW_FREE_FALLBACK_MODEL),
],
"DuckDuckGo": [
("DUCKDUCKGO_BASE_URL", DUCKDUCKGO_BASE_URL),
("DUCKDUCKGO_DEFAULT_MODEL", DUCKDUCKGO_DEFAULT_MODEL),
(
"DUCKDUCKGO_USER_AGENT",
DUCKDUCKGO_USER_AGENT or "(built-in browser UA)",
),
],
"ACP": [
("ACP_USER", ACP_USER),
("ACP_PASS", _mask_key(ACP_PASS)),
Expand Down Expand Up @@ -544,6 +577,7 @@ def cmd_config(args: argparse.Namespace) -> int:
"Cloudflare": CLOUDFLARE_BASE_URL
or f"https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID or '<ID>'}/ai/v1",
"SiliconFlow": SILICONFLOW_BASE_URL,
"DuckDuckGo": DUCKDUCKGO_BASE_URL,
"ACP": ACP_BASE_URL,
},
acp_user=ACP_USER,
Expand Down
28 changes: 28 additions & 0 deletions agentkthx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,34 @@
SILICONFLOW_FREE_FALLBACK_MODEL = os.environ.get("SILICONFLOW_FREE_FALLBACK_MODEL", "Qwen/Qwen3-8B")


# ═══════════════════════════════════════════════════════════════════════════════
# DuckDuckGo plugin (agentkthx/plugins/duckduckgo/)
# DuckDuckGo AI Chat (duck.ai) — the keyless, anonymous, zero-cost LLM
# surface via the NON-OpenAI /duckchat/v1 protocol. NO API KEY EXISTS —
# "authentication" is the x-vqd-hash-1 CHALLENGE PROOF (the retired
# x-vqd-4 opaque token is gone): GET /status issues an obfuscated JS
# challenge, the bundled Node solver executes it against a clean-browser
# environment, and the solved result rides the X-Vqd-Hash-1 request
# header. Each /chat response carries the NEXT challenge (per-turn
# rotation). Held in-memory per backend instance, never persisted. See
# docs/api/DUCKDUCKGO_API_TECHNICAL_REFERENCE.md.
# ═══════════════════════════════════════════════════════════════════════════════
DUCKDUCKGO_BASE_URL = os.environ.get("DUCKDUCKGO_BASE_URL", "https://duck.ai")
# Browser User-Agent — DDG's anti-bot layer returns 403 for
# non-browser UAs. Empty string falls back to the backend's built-in
# Chrome 136 UA (the default). The challenge solver is fed the SAME
# UA (a mismatch invalidates the proof). Only set this if DDG starts
# rejecting the built-in one (UA rotation is an anti-fingerprinting
# last resort, not a routine knob).
DUCKDUCKGO_USER_AGENT = os.environ.get("DUCKDUCKGO_USER_AGENT", "")
# Default model — gpt-6-luna is DuckDuckGo's own default pick. The
# catalog: gpt-6-luna, gpt-5.6-luna, gpt-5.4-nano, gpt-5.4-mini,
# claude-haiku-4-5, mistral-small-2603, tinfoil/gpt-oss-120b,
# tinfoil/gemma4-31b (legacy aliases gpt-4o-mini, claude-3-haiku,
# llama, mixtral, o3-mini resolve forward to their successors).
DUCKDUCKGO_DEFAULT_MODEL = os.environ.get("DUCKDUCKGO_DEFAULT_MODEL", "gpt-6-luna")


# ═══════════════════════════════════════════════════════════════════════════════
# BACKEND SELECTION
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down
14 changes: 14 additions & 0 deletions agentkthx/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ class BackendType(Enum):
# docs/api/SILICONFLOW_API_TECHNICAL_REFERENCE.md.
SILICONFLOW = "siliconflow"

# DuckDuckGo AI Chat — the keyless, anonymous, zero-cost LLM
# surface at duckduckgo.com/duckchat/v1 (fronting duck.ai). NOT
# OpenAI-compatible: custom x-vqd-4 header-token handshake
# (bootstrap via GET /status, rotate on every /chat response),
# own SSE shape, no /models endpoint, no sampling params, tools
# stripped (ReAct only), ~20-turn conversation limit. Catalog:
# GPT-4o mini, o3-mini, Claude Haiku, Llama 3.3 70B, Mistral
# Small 3 24B — the upstream frontier set, IP-stripped by DDG's
# privacy layer. The ONLY cloud backend that subclasses
# BaseBackend directly instead of CloudBackend.
# See agentkthx/plugins/duckduckgo/ and
# docs/api/DUCKDUCKGO_API_TECHNICAL_REFERENCE.md.
DUCKDUCKGO = "duckduckgo"


class ApiMode(Enum):
"""API mode for backend communication.
Expand Down
74 changes: 74 additions & 0 deletions agentkthx/data/model_seed.json
Original file line number Diff line number Diff line change
Expand Up @@ -1923,5 +1923,79 @@
"default_temperature": 0.7,
"default_max_tokens": 8192
}
},
"duckduckgo": {
"gpt-6-luna": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 16384,
"family": "gpt",
"free_tier": true,
"upstream": "OpenAI GPT-6 Luna (via DDG proxy)",
"note": "DuckDuckGo's own default model ('Best for everyday use'); verified against the live dropdown + benoitpetit/duckduckgo-chat-cli models.go, 2026-10-09"
},
"gpt-5.6-luna": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 16384,
"family": "gpt",
"free_tier": true,
"upstream": "OpenAI GPT-5.6 Luna (via DDG proxy)",
"note": "Previous Luna model; legacy alias gpt-4o-mini resolves here (duckduckgo-chat-cli compat map)"
},
"gpt-5.4-nano": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 16384,
"family": "gpt",
"free_tier": true,
"upstream": "OpenAI GPT-5.4 Nano (via DDG proxy)",
"note": "Fast, lightweight tier; alias 'nano' resolves here"
},
"gpt-5.4-mini": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 16384,
"family": "gpt",
"free_tier": true,
"upstream": "OpenAI GPT-5.4 Mini (via DDG proxy)",
"note": "'Solid but hits limits sooner'; legacy aliases o3-mini / o4mini resolve here"
},
"claude-haiku-4-5": {
"context_length": 200000,
"default_temperature": 0.7,
"default_max_tokens": 8192,
"family": "claude",
"free_tier": true,
"upstream": "Anthropic Claude Haiku 4.5",
"note": "Alias 'claude' resolves here; legacy claude-3-haiku / claude-3-haiku-20240307 IDs rotated out (now 404 ERR_MODEL_UNAVAILABLE)"
},
"mistral-small-2603": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 8192,
"family": "mistral",
"free_tier": true,
"upstream": "Mistral Small 4",
"note": "Display name 'Mistral Small 4'; wire ID is mistral-small-2603 (NOT mistral-small-4); alias 'mixtral' resolves here"
},
"tinfoil/gpt-oss-120b": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 16384,
"family": "gpt-oss",
"free_tier": true,
"upstream": "OpenAI gpt-oss-120b on Tinfoil (zero provider visibility)",
"note": "Open-weight reasoning model served with reasoningEffort 'low'; alias 'llama' resolves here (the open-weights slot)"
},
"tinfoil/gemma4-31b": {
"context_length": 128000,
"default_temperature": 0.7,
"default_max_tokens": 8192,
"family": "gemma",
"free_tier": true,
"upstream": "Google Gemma 4 31B on Tinfoil (zero provider visibility)",
"note": "BETA in the dropdown; served with reasoningEffort 'low'; alias 'gemma' resolves here"
}
}
}
38 changes: 38 additions & 0 deletions agentkthx/plugins/duckduckgo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
AgentKthx Plugin — DuckDuckGo AI Chat Backend

DuckDuckGo AI Chat (duck.ai) backend — the keyless, anonymous,
zero-cost LLM surface: no API key, no signup, no quota. Current
lineup (GPT-6 Luna, GPT-5.6 Luna, GPT-5.4 Nano/Mini, Claude
Haiku 4.5, Mistral Small 4, and the Tinfoil-hosted gpt-oss-120b /
Gemma 4 31B) proxied through DuckDuckGo's privacy layer via the
NON-OpenAI /duckchat/v1 protocol — now CHALLENGE-BASED (the
x-vqd-hash-1 JS proof replaced the retired x-vqd-4 token; solved
by the bundled Node helper).

The only AgentKthx cloud backend that subclasses BaseBackend
directly instead of CloudBackend — see duckduckgo.py for the
protocol rationale.

See docs/api/DUCKDUCKGO_API_TECHNICAL_REFERENCE.md for full details.

Written by VTSTech — https://www.vts-tech.org
"""

from __future__ import annotations


def register(manager) -> None:
"""Register the DuckDuckGo backend with the plugin manager."""
from .duckduckgo import DuckDuckGoBackend

manager.register_backend("duckduckgo", DuckDuckGoBackend)
# Alias for convenience — `--backend duckduckgo` and `--backend ddg`
# both work. Mirrors the SiliconFlow `sf` alias pattern.
manager.register_backend("ddg", DuckDuckGoBackend)


def unregister(manager) -> None:
"""Unregister the DuckDuckGo backend from the plugin manager."""
manager.unregister_backend("duckduckgo")
manager.unregister_backend("ddg")
40 changes: 40 additions & 0 deletions agentkthx/plugins/duckduckgo/ddg_durable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env node
// ═══════════════════════════════════════════════════════════════════════
// ddg_durable.js — durableStream RSA keypair generator for duck.ai
//
// The duck.ai POST /duckchat/v1/chat body now carries a `durableStream`
// object: {messageId, conversationId, publicKey}. The publicKey is a
// client-generated RSA-2048 JWK (the web client persists its pair in
// localStorage under `duckaiStreamsTestCredentials`; the server uses it
// to encrypt stream-resume payloads). Python stdlib has no RSA, so we
// mint the keypair here — Node crypto is built in.
//
// Prints one JSON object on stdout:
// {"publicJwk": {alg,e,ext,key_ops,kty,n,use},
// "privateJwk": {…full RSA private JWK…}}
//
// Usage: node ddg_durable.js
// ═══════════════════════════════════════════════════════════════════════
'use strict';

const { generateKeyPairSync } = require('crypto');

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { format: 'jwk' },
privateKeyEncoding: { format: 'jwk' },
});

// Normalize to the exact shape the duck.ai frontend sends (captured live):
// alg RSA-OAEP-256, key_ops ["encrypt"], use "enc", ext true.
const publicJwk = {
alg: 'RSA-OAEP-256',
e: publicKey.e,
ext: true,
key_ops: ['encrypt'],
kty: publicKey.kty,
n: publicKey.n,
use: 'enc',
};

process.stdout.write(JSON.stringify({ publicJwk, privateJwk: privateKey }));
Loading
Loading