Merge pull request #4 from Brioch/main
Browse filesfix(api): fix error: Missing VQD headers: vqd=false, hash=true
- README.md +5 -4
- src/duckai.ts +50 -54
README.md
CHANGED
|
@@ -72,10 +72,11 @@ DuckAI OpenAI Server bridges the gap between DuckDuckGo's free AI chat service a
|
|
| 72 |
### Supported Models
|
| 73 |
|
| 74 |
- `gpt-4o-mini` (Default)
|
| 75 |
-
- `
|
| 76 |
-
- `claude-3-haiku-
|
| 77 |
-
- `meta-llama/Llama-
|
| 78 |
-
- `mistralai/
|
|
|
|
| 79 |
|
| 80 |
### Features
|
| 81 |
|
|
|
|
| 72 |
### Supported Models
|
| 73 |
|
| 74 |
- `gpt-4o-mini` (Default)
|
| 75 |
+
- `gpt-5-mini`
|
| 76 |
+
- `claude-3-5-haiku-latest`
|
| 77 |
+
- `meta-llama/Llama-4-Scout-17B-16E-Instruct`
|
| 78 |
+
- `mistralai/Mistral-Small-24B-Instruct-2501`
|
| 79 |
+
- `openai/gpt-oss-120b`
|
| 80 |
|
| 81 |
### Features
|
| 82 |
|
src/duckai.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
| 7 |
VQDResponse,
|
| 8 |
DuckAIRequest,
|
| 9 |
} from "./types";
|
|
|
|
|
|
|
| 10 |
|
| 11 |
// Rate limiting tracking with sliding window
|
| 12 |
interface RateLimitInfo {
|
|
@@ -180,6 +182,44 @@ export class DuckAI {
|
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
private async getVQD(userAgent: string): Promise<VQDResponse> {
|
| 184 |
const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
|
| 185 |
headers: {
|
|
@@ -207,23 +247,17 @@ export class DuckAI {
|
|
| 207 |
);
|
| 208 |
}
|
| 209 |
|
| 210 |
-
const vqd = response.headers.get("x-Vqd-4");
|
| 211 |
const hashHeader = response.headers.get("x-Vqd-hash-1");
|
| 212 |
|
| 213 |
-
if (!
|
| 214 |
throw new Error(
|
| 215 |
-
`Missing VQD headers:
|
| 216 |
);
|
| 217 |
}
|
| 218 |
|
| 219 |
-
|
| 220 |
-
try {
|
| 221 |
-
hash = atob(hashHeader);
|
| 222 |
-
} catch (e) {
|
| 223 |
-
throw new Error(`Failed to decode VQD hash: ${e}`);
|
| 224 |
-
}
|
| 225 |
|
| 226 |
-
return {
|
| 227 |
}
|
| 228 |
|
| 229 |
private async hashClientHashes(clientHashes: string[]): Promise<string[]> {
|
|
@@ -247,18 +281,6 @@ export class DuckAI {
|
|
| 247 |
const userAgent = new UserAgent().toString();
|
| 248 |
const vqd = await this.getVQD(userAgent);
|
| 249 |
|
| 250 |
-
const { window } = new JSDOM(
|
| 251 |
-
`<html><body><script>window.hash = ${vqd.hash}</script></body></html>`,
|
| 252 |
-
{ runScripts: "dangerously" }
|
| 253 |
-
);
|
| 254 |
-
const hash = (window as any).hash;
|
| 255 |
-
|
| 256 |
-
if (!hash || !hash.client_hashes || !Array.isArray(hash.client_hashes)) {
|
| 257 |
-
throw new Error(`Invalid hash structure: ${JSON.stringify(hash)}`);
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
const clientHashes = await this.hashClientHashes(hash.client_hashes);
|
| 261 |
-
|
| 262 |
// Update rate limit tracking BEFORE making the request
|
| 263 |
const now = Date.now();
|
| 264 |
this.rateLimitInfo.requestTimestamps.push(now);
|
|
@@ -280,15 +302,8 @@ export class DuckAI {
|
|
| 280 |
"sec-fetch-mode": "cors",
|
| 281 |
"sec-fetch-site": "same-origin",
|
| 282 |
"x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
|
| 283 |
-
"x-vqd-4": vqd.vqd,
|
| 284 |
"User-Agent": userAgent,
|
| 285 |
-
"x-vqd-hash-1":
|
| 286 |
-
JSON.stringify({
|
| 287 |
-
server_hashes: hash.server_hashes,
|
| 288 |
-
client_hashes: clientHashes,
|
| 289 |
-
signals: hash.signal,
|
| 290 |
-
})
|
| 291 |
-
),
|
| 292 |
},
|
| 293 |
referrer: "https://duckduckgo.com/",
|
| 294 |
referrerPolicy: "origin",
|
|
@@ -357,21 +372,8 @@ export class DuckAI {
|
|
| 357 |
await this.waitIfNeeded();
|
| 358 |
|
| 359 |
const userAgent = new UserAgent().toString();
|
| 360 |
-
|
| 361 |
const vqd = await this.getVQD(userAgent);
|
| 362 |
|
| 363 |
-
const { window } = new JSDOM(
|
| 364 |
-
`<html><body><script>window.hash = ${vqd.hash}</script></body></html>`,
|
| 365 |
-
{ runScripts: "dangerously" }
|
| 366 |
-
);
|
| 367 |
-
const hash = (window as any).hash;
|
| 368 |
-
|
| 369 |
-
if (!hash || !hash.client_hashes || !Array.isArray(hash.client_hashes)) {
|
| 370 |
-
throw new Error(`Invalid hash structure: ${JSON.stringify(hash)}`);
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
const clientHashes = await this.hashClientHashes(hash.client_hashes);
|
| 374 |
-
|
| 375 |
// Update rate limit tracking BEFORE making the request
|
| 376 |
const now = Date.now();
|
| 377 |
this.rateLimitInfo.requestTimestamps.push(now);
|
|
@@ -393,15 +395,8 @@ export class DuckAI {
|
|
| 393 |
"sec-fetch-mode": "cors",
|
| 394 |
"sec-fetch-site": "same-origin",
|
| 395 |
"x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
|
| 396 |
-
"x-vqd-4": vqd.vqd,
|
| 397 |
"User-Agent": userAgent,
|
| 398 |
-
"x-vqd-hash-1":
|
| 399 |
-
JSON.stringify({
|
| 400 |
-
server_hashes: hash.server_hashes,
|
| 401 |
-
client_hashes: clientHashes,
|
| 402 |
-
signals: hash.signal,
|
| 403 |
-
})
|
| 404 |
-
),
|
| 405 |
},
|
| 406 |
referrer: "https://duckduckgo.com/",
|
| 407 |
referrerPolicy: "origin",
|
|
@@ -470,10 +465,11 @@ export class DuckAI {
|
|
| 470 |
getAvailableModels(): string[] {
|
| 471 |
return [
|
| 472 |
"gpt-4o-mini",
|
| 473 |
-
"
|
| 474 |
-
"claude-3-haiku-
|
| 475 |
-
"meta-llama/Llama-
|
| 476 |
"mistralai/Mistral-Small-24B-Instruct-2501",
|
|
|
|
| 477 |
];
|
| 478 |
}
|
| 479 |
}
|
|
|
|
| 7 |
VQDResponse,
|
| 8 |
DuckAIRequest,
|
| 9 |
} from "./types";
|
| 10 |
+
import { createHash } from "node:crypto";
|
| 11 |
+
import { Buffer } from "node:buffer";
|
| 12 |
|
| 13 |
// Rate limiting tracking with sliding window
|
| 14 |
interface RateLimitInfo {
|
|
|
|
| 182 |
}
|
| 183 |
}
|
| 184 |
|
| 185 |
+
private async getEncodedVqdHash(vqdHash: string): Promise<string> {
|
| 186 |
+
const jsScript = Buffer.from(vqdHash, 'base64').toString('utf-8');
|
| 187 |
+
|
| 188 |
+
const dom = new JSDOM(
|
| 189 |
+
`<iframe id="jsa" sandbox="allow-scripts allow-same-origin" srcdoc="<!DOCTYPE html>
|
| 190 |
+
<html>
|
| 191 |
+
<head>
|
| 192 |
+
<meta http-equiv="Content-Security-Policy"; content="default-src 'none'; script-src 'unsafe-inline'">
|
| 193 |
+
</head>
|
| 194 |
+
<body></body>
|
| 195 |
+
</html>" style="position: absolute; left: -9999px; top: -9999px;"></iframe>`,
|
| 196 |
+
{ runScripts: 'dangerously' }
|
| 197 |
+
);
|
| 198 |
+
dom.window.top.__DDG_BE_VERSION__ = 1;
|
| 199 |
+
dom.window.top.__DDG_FE_CHAT_HASH__ = 1;
|
| 200 |
+
const jsa = dom.window.top.document.querySelector('#jsa') as HTMLIFrameElement;
|
| 201 |
+
const contentDoc = jsa.contentDocument || jsa.contentWindow!.document;
|
| 202 |
+
|
| 203 |
+
const meta = contentDoc.createElement('meta');
|
| 204 |
+
meta.setAttribute('http-equiv', 'Content-Security-Policy');
|
| 205 |
+
meta.setAttribute('content', "default-src 'none'; script-src 'unsafe-inline';");
|
| 206 |
+
contentDoc.head.appendChild(meta);
|
| 207 |
+
const result = await dom.window.eval(jsScript) as {
|
| 208 |
+
client_hashes: string[];
|
| 209 |
+
[key: string]: any;
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
result.client_hashes[0] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36';
|
| 213 |
+
result.client_hashes = result.client_hashes.map((t) => {
|
| 214 |
+
const hash = createHash('sha256');
|
| 215 |
+
hash.update(t);
|
| 216 |
+
|
| 217 |
+
return hash.digest('base64');
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
return btoa(JSON.stringify(result));
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
private async getVQD(userAgent: string): Promise<VQDResponse> {
|
| 224 |
const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
|
| 225 |
headers: {
|
|
|
|
| 247 |
);
|
| 248 |
}
|
| 249 |
|
|
|
|
| 250 |
const hashHeader = response.headers.get("x-Vqd-hash-1");
|
| 251 |
|
| 252 |
+
if (!hashHeader) {
|
| 253 |
throw new Error(
|
| 254 |
+
`Missing VQD headers: hash=${!!hashHeader}`
|
| 255 |
);
|
| 256 |
}
|
| 257 |
|
| 258 |
+
const encodedHash = await this.getEncodedVqdHash(hashHeader);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
+
return { hash: encodedHash };
|
| 261 |
}
|
| 262 |
|
| 263 |
private async hashClientHashes(clientHashes: string[]): Promise<string[]> {
|
|
|
|
| 281 |
const userAgent = new UserAgent().toString();
|
| 282 |
const vqd = await this.getVQD(userAgent);
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
// Update rate limit tracking BEFORE making the request
|
| 285 |
const now = Date.now();
|
| 286 |
this.rateLimitInfo.requestTimestamps.push(now);
|
|
|
|
| 302 |
"sec-fetch-mode": "cors",
|
| 303 |
"sec-fetch-site": "same-origin",
|
| 304 |
"x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
|
|
|
|
| 305 |
"User-Agent": userAgent,
|
| 306 |
+
"x-vqd-hash-1": vqd.hash,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
},
|
| 308 |
referrer: "https://duckduckgo.com/",
|
| 309 |
referrerPolicy: "origin",
|
|
|
|
| 372 |
await this.waitIfNeeded();
|
| 373 |
|
| 374 |
const userAgent = new UserAgent().toString();
|
|
|
|
| 375 |
const vqd = await this.getVQD(userAgent);
|
| 376 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
// Update rate limit tracking BEFORE making the request
|
| 378 |
const now = Date.now();
|
| 379 |
this.rateLimitInfo.requestTimestamps.push(now);
|
|
|
|
| 395 |
"sec-fetch-mode": "cors",
|
| 396 |
"sec-fetch-site": "same-origin",
|
| 397 |
"x-fe-version": "serp_20250401_100419_ET-19d438eb199b2bf7c300",
|
|
|
|
| 398 |
"User-Agent": userAgent,
|
| 399 |
+
"x-vqd-hash-1": vqd.hash,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
},
|
| 401 |
referrer: "https://duckduckgo.com/",
|
| 402 |
referrerPolicy: "origin",
|
|
|
|
| 465 |
getAvailableModels(): string[] {
|
| 466 |
return [
|
| 467 |
"gpt-4o-mini",
|
| 468 |
+
"gpt-5-mini",
|
| 469 |
+
"claude-3-5-haiku-latest",
|
| 470 |
+
"meta-llama/Llama-4-Scout-17B-16E-Instruct",
|
| 471 |
"mistralai/Mistral-Small-24B-Instruct-2501",
|
| 472 |
+
"openai/gpt-oss-120b"
|
| 473 |
];
|
| 474 |
}
|
| 475 |
}
|