rynn-k / gists
duckduckgo-ai.js javascript
const axios = require('axios');
const { JSDOM } = require('jsdom');
const { VM } = require('vm2');
const UserAgent = require('user-agents');
const { webcrypto } = require('crypto');

const subtle = webcrypto.subtle;

class DuckAI {
    static MODELS = ['gpt-5-mini', 'gpt-4o-mini', 'openai/gpt-oss-120b', 'meta-llama/Llama-4-Scout-17B-16E-Instruct', 'claude-haiku-4-5', 'mistralai/Mistral-Small-24B-Instruct-2501'];
    
    static _ua = null;
    static _rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
    static _sleep = ms => new Promise(r => setTimeout(r, ms));
    
    constructor({ maxRetries = 5, useTools = false } = {}) {
        this._maxRetries = maxRetries;
        this._useTools = useTools;
        this._vqd = null;
        this._vqdAt = 0;
        
        if (!DuckAI._ua) {
            let s = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36', p = 'Win32', vd = 'Google Inc.';
            try { const u = new UserAgent({ deviceCategory: 'desktop', vendor: vd, platform: p }); if (u.toString().includes('Chrome')) { s = u.toString(); p = u.data?.platform || p; vd = u.data?.vendor || vd; } } catch {}
            const v = (s.match(/Chrome\/(\d+)/) || [])[1] || '135';
            DuckAI._ua = {
                str: s,
                platform: p,
                vendor: vd,
                secChUa: `"Google Chrome";v="${v}", "Not-A.Brand";v="8", "Chromium";v="${v}"`,
            };
        }
    }
    
    _headers(extra = {}) {
        const { str, platform, secChUa } = DuckAI._ua;
        return {
            'accept-language': 'en-US,en;q=0.9',
            'accept-encoding': 'gzip, deflate, br',
            'referer': 'https://duck.ai/',
            'origin': 'https://duck.ai',
            'cache-control': 'no-store',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin',
            'sec-ch-ua': secChUa,
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': `"${platform}"`,
            'user-agent': str,
            ...extra,
        };
    }
    
    async _getVqd(force = false) {
        if (!force && this._vqd && Date.now() - this._vqdAt < 200_000) return this._vqd;
        
        const res = await axios.get('https://duck.ai/duckchat/v1/status', {
            headers: this._headers({ 'x-vqd-accept': '1' }),
        });
        
        const raw = res.headers['x-vqd-hash-1'];
        if (!raw) throw new Error('VQD header missing');
        this._vqd = await DuckAI._solveChallenge(raw);
        this._vqdAt = Date.now();
        
        return this._vqd;
    }
    
    static async _solveChallenge(raw) {
        const STACK = ['Error', '    at l (https://duck.ai/dist/duckai-dist/entry.duckai.508538477be99c7fc13b8.js:2:1180630)', '    at async https://duck.ai/dist/duckai-dist/entry.duckai.508538477be99c7fc13b8.js:2:1063659'].join('\n');
        
        let src;
        try { src = Buffer.from(decodeURIComponent(raw), 'base64').toString('utf-8'); } catch { return raw; }
        
        let obj = null;
        try {
            obj = await new Promise((resolve, reject) => {
                const t = setTimeout(() => reject(new Error('timeout')), 10_000);
                let dom;
                
                try {
                    dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
                        url: 'https://duck.ai',
                        runScripts: 'dangerously',
                        pretendToBeVisual: true,
                        resources: 'usable',
                    });
                } catch (e) {
                    clearTimeout(t);
                    return reject(e);
                }
                
                const win = dom.window;
                for (const [k, v] of [['webdriver', false], ['plugins', { length: 3 }], ['languages', ['en-US', 'en']], ['hardwareConcurrency', 8], ['deviceMemory', 8]])
                    try {
                        Object.defineProperty(win.navigator, k, { get: () => v });
                    } catch {}
                
                win.__resolve = r => { clearTimeout(t); dom.window.close(); resolve(r); };
                win.__reject = e => { clearTimeout(t); dom.window.close(); reject(new Error(e)); };
                win.chrome = { runtime: { id: undefined } };
                
                try {
                    const el = win.document.createElement('script');
                    el.textContent = `(async()=>{try{window.__resolve(await(${src}))}catch(e){window.__reject(String(e?.message??e))}})()`;
                    win.document.head.appendChild(el);
                } catch (e) {
                    clearTimeout(t);
                    dom.window.close();
                    reject(e);
                }
            });
            
            if (!Array.isArray(obj?.server_hashes) || !obj.server_hashes.length) obj = null;
        } catch {}
        
        if (!obj) {
            try {
                const r = DuckAI._rand, ua = DuckAI._ua;
                const sb = {
                    navigator: {
                        userAgent: ua.str,
                        webdriver: false,
                        languages: ['en-US', 'en'],
                        platform: ua.platform,
                        hardwareConcurrency: 8,
                        deviceMemory: 8,
                        maxTouchPoints: 0,
                        vendor: ua.vendor,
                        plugins: { length: 3 },
                        cookieEnabled: true,
                    },
                    document: {
                        createElement: () => ({
                            innerHTML: '',
                            children: { length: 0 },
                            appendChild: () => {},
                            removeChild: () => {},
                            addEventListener: () => {},
                            removeEventListener: () => {},
                            querySelectorAll: () => [],
                        }),
                        body: { appendChild: () => {}, removeChild: () => {}, children: { length: r(5, 12) }, onerror: null },
                        querySelectorAll: () => Object.assign([], { length: r(20, 60) }),
                    },
                    performance: { now: () => Date.now() },
                    location: { href: 'https://duck.ai', origin: 'https://duck.ai', hostname: 'duck.ai' },
                    chrome: { runtime: { id: undefined } },
                    crypto: {
                        subtle,
                        getRandomValues: a => { for (let i = 0; i < a.length; i++) a[i] = r(0, 255); return a; },
                        randomUUID: () => webcrypto.randomUUID(),
                    },
                    setTimeout: (fn, ms) => { setTimeout(fn, Math.min(ms || 0, 50)); return 1; },
                    clearTimeout: () => {},
                    atob: s => Buffer.from(s, 'base64').toString('binary'),
                    btoa: s => Buffer.from(s, 'binary').toString('base64'),
                    decodeURIComponent, encodeURIComponent,
                    Array, Object, String, Number, Boolean, Math, JSON, Symbol, Proxy, Promise, Map, Set, WeakMap,
                    Uint8Array, Int32Array, ArrayBuffer, DataView, parseInt, parseFloat, isNaN, isFinite,
                    Error: class extends globalThis.Error {
                        constructor(m) { super(m); this.stack = STACK; }
                        static captureStackTrace() {}
                    },
                };
                
                sb.window = sb; sb.self = sb; sb.top = sb; sb.globalThis = sb;
                const res = await new VM({ timeout: 8000, sandbox: sb, eval: false, wasm: false }).run(`(async()=>await(${src}))()`);
                
                if (Array.isArray(res?.server_hashes) && res.server_hashes.length) obj = res;
            } catch {}
        }
        
        if (!obj) {
            const hashes = [], re = /['"]([A-Za-z0-9+/]{40,64}={0,2})['"]/g; let m;
            while ((m = re.exec(src))) hashes.push(m[1]);
            
            obj = {
                server_hashes: hashes.slice(0, 3),
                client_hashes: [],
                signals: {},
                meta: {
                    v: '4',
                    challenge_id: src.match(/challenge_id['"]\s*[,)]\s*['"]\s*([^'"]{10,})['"]/)?.[1] || '',
                    timestamp: src.match(/'(\d{13})'/)?.[1] || String(Date.now()),
                },
            };
        }
        
        const clientHashes = await Promise.all((obj.client_hashes || []).map(async h => {
            const hash = await subtle.digest('SHA-256', new TextEncoder().encode(h));
            return Buffer.from(hash).toString('base64');
        }));
        
        return Buffer.from(JSON.stringify({
            server_hashes: obj.server_hashes || [],
            client_hashes: clientHashes,
            signals: obj.signals || {},
            meta: {
                ...(obj.meta || {}),
                v: '4',
                origin: 'https://duck.ai',
                stack: STACK,
                duration: String(DuckAI._rand(820, 1380)),
            },
        })).toString('base64');
    }
    
    async chat({ messages, model = 'gpt-4o-mini' } = {}) {
        let attempt = 0, lastErr = null;
        while (attempt <= this._maxRetries) {
            try {
                const vqd = await this._getVqd(attempt > 0);
                await DuckAI._sleep(DuckAI._rand(200, 700));
                
                const r = DuckAI._rand;
                const signals = Buffer.from(JSON.stringify({
                    start: Date.now() - r(1800, 4000),
                    events: [
                        { name: 'onboarding_impression', delta: r(300, 500) },
                        { name: 'onboarding_finish', delta: r(9000, 12000) },
                        { name: 'startNewChat_free', delta: r(9500, 13000) },
                        { name: 'initSwitchModel', delta: r(11000, 15000) },
                    ],
                    end: r(20000, 28000),
                })).toString('base64');
                
                const { data } = await axios.post('https://duck.ai/duckchat/v1/chat', {
                    model,
                    messages,
                    metadata: {
                        toolChoice: {
                            NewsSearch: this._useTools,
                            VideosSearch: false,
                            LocalSearch: false,
                            WeatherForecast: false,
                        },
                    },
                    canUseTools: this._useTools,
                    canUseApproxLocation: null,
                }, {
                    headers: this._headers({
                        'accept': 'text/event-stream',
                        'content-type': 'application/json',
                        'x-fe-version': 'serp_20260311_192230_ET-a6d03d3dee4ffac4d95a950f0bb5590dcf8b187a',
                        'x-fe-signals': signals,
                        'x-vqd-hash-1': vqd,
                    }),
                    responseType: 'text'
                });
                
                const parsed = data.split('\n').filter(l => l.startsWith('data: {')).map(l => JSON.parse(l.slice(6)));
                const content = parsed.filter(p => p.action === 'success' && p.role === 'assistant').map(p => p.message).join('');
                const detectedModel = parsed.findLast(p => p.model)?.model ?? model;
                
                return {
                    model: detectedModel,
                    content: content
                };
            } catch (err) {
                lastErr = err;
                
                if (err.name === 'AbortError' || err.name === 'CanceledError') throw err;
                if (attempt >= this._maxRetries) break;
                if (err.response?.status === 418) {
                    await DuckAI._sleep(DuckAI._rand(4000, 8000));
                    await this._getVqd(true);
                }
                
                console.error(`[DuckAI] Attempt ${attempt + 1} failed:`, err.message);
                await DuckAI._sleep(Math.min(2000 * Math.pow(2, attempt) + DuckAI._rand(200, 800), 30_000));
                attempt++;
            }
        }
        
        throw lastErr;
    }
}

// Usage:
const duck = new DuckAI();
duck.chat({
    messages: [{
        role: 'user',
        content: 'hi'
    }],
    model: 'claude-haiku-4-5'
}).then(console.log);
12705 bytes ยท Updated Mar 22, 2026