DDG2API / index.js
Rfym21's picture
Upload index.js
5cd566d verified
import { createServerAdapter } from '@whatwg-node/server'
import { AutoRouter, json, error, cors } from 'itty-router'
import { createServer } from 'http'
import dotenv from 'dotenv'
dotenv.config()
class Config {
constructor() {
this.API_PREFIX = process.env.API_PREFIX || '/'
this.API_KEY = process.env.API_KEY || ''
this.MAX_RETRY_COUNT = process.env.MAX_RETRY_COUNT || 3
this.RETRY_DELAY = process.env.RETRY_DELAY || 5000
this.FAKE_HEADERS = process.env.FAKE_HEADERS || {
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'zh-CN,zh;q=0.9',
Origin: 'https://duckduckgo.com/',
Cookie: 'l=wt-wt; ah=wt-wt; dcm=6',
Dnt: '1',
Priority: 'u=1, i',
Referer: 'https://duckduckgo.com/',
'Sec-Ch-Ua': '"Microsoft Edge";v="129", "Not(A:Brand";v="8", "Chromium";v="129"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
}
}
}
const config = new Config()
const { preflight, corsify } = cors({
origin: '*',
allowMethods: '*',
exposeHeaders: '*',
})
const withBenchmarking = (request) => {
request.start = Date.now()
}
const withAuth = (request) => {
if (config.API_KEY) {
const authHeader = request.headers.get('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return error(401, 'Unauthorized: Missing or invalid Authorization header')
}
const token = authHeader.substring(7)
if (token !== config.API_KEY) {
return error(403, 'Forbidden: Invalid API key')
}
}
}
const logger = (res, req) => {
console.log(req.method, res.status, req.url, Date.now() - req.start, 'ms')
}
const router = AutoRouter({
before: [withBenchmarking, preflight, withAuth],
missing: () => error(404, '404 not found.'),
finally: [corsify, logger],
})
router.get('/', () => json({ message: 'API 服务运行中~' }))
router.get('/ping', () => json({ message: 'pong' }))
router.get(config.API_PREFIX + '/v1/models', () =>
json({
object: 'list',
data: [
{ id: 'gpt-4o-mini', object: 'model', owned_by: 'ddg' },
{ id: 'claude-3-haiku', object: 'model', owned_by: 'ddg' },
{ id: 'llama-3.1-70b', object: 'model', owned_by: 'ddg' },
{ id: 'mixtral-8x7b', object: 'model', owned_by: 'ddg' },
],
})
)
router.post(config.API_PREFIX + '/v1/chat/completions', (req) => handleCompletion(req))
async function handleCompletion(request) {
try {
const { model: inputModel, messages, stream: returnStream } = await request.json()
const model = convertModel(inputModel)
const content = messagesPrepare(messages)
return createCompletion(model, content, returnStream)
} catch (err) {
error(500, err.message)
}
}
async function createCompletion(model, content, returnStream, retryCount = 0) {
const token = await requestToken()
try {
const response = await fetch(`https://duckduckgo.com/duckchat/v1/chat`, {
method: 'POST',
headers: {
...config.FAKE_HEADERS,
Accept: 'text/event-stream',
'Content-Type': 'application/json',
'x-vqd-4': token,
},
body: JSON.stringify({
model: model,
messages: [
{
role: 'user',
content: content,
},
],
}),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return handlerStream(model, response.body, returnStream)
} catch (err) {
console.log(err)
if (retryCount < config.MAX_RETRY_COUNT) {
console.log('Retrying... count', ++retryCount)
await new Promise((resolve) => setTimeout(resolve, config.RETRY_DELAY))
return await createCompletion(model, content, returnStream, retryCount)
}
throw err
}
}
async function handlerStream(model, rb, returnStream) {
let bwzChunk = ''
let previousText = ''
const handChunkData = (chunk) => {
chunk = chunk.trim()
if (bwzChunk != '') {
chunk = bwzChunk + chunk
bwzChunk = ''
}
if (chunk.includes('[DONE]')) {
return chunk
}
if (chunk.slice(-2) !== '"}') {
bwzChunk = chunk
}
return chunk
}
const reader = rb.getReader()
const decoder = new TextDecoder()
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read()
if (done) {
return controller.close()
}
const chunkStr = handChunkData(decoder.decode(value))
if (bwzChunk !== '') {
continue
}
chunkStr.split('\n').forEach((line) => {
if (line.length < 6) {
return
}
line = line.slice(6)
if (line !== '[DONE]') {
const originReq = JSON.parse(line)
if (originReq.action !== 'success') {
return controller.error(new Error('Error: originReq stream chunk is not success'))
}
if (originReq.message) {
previousText += originReq.message
if (returnStream) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(newChatCompletionChunkWithModel(originReq.message, originReq.model))}\n\n`)
)
}
}
} else {
if (returnStream) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(newStopChunkWithModel('stop', model))}\n\n`))
} else {
controller.enqueue(encoder.encode(JSON.stringify(newChatCompletionWithModel(previousText, model))))
}
return controller.close()
}
})
continue
}
},
})
return new Response(stream, {
headers: {
'Content-Type': returnStream ? 'text/event-stream' : 'application/json',
},
})
}
function messagesPrepare(messages) {
let content = ''
for (const message of messages) {
let role = message.role === 'system' ? 'user' : message.role
if (['user', 'assistant'].includes(role)) {
const contentStr = Array.isArray(message.content)
? message.content
.filter((item) => item.text)
.map((item) => item.text)
.join('') || ''
: message.content
content += `${role}:${contentStr};\r\n`
}
}
return content
}
async function requestToken() {
const response = await fetch(`https://duckduckgo.com/duckchat/v1/status`, {
method: 'GET',
headers: {
...config.FAKE_HEADERS,
'x-vqd-accept': '1',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const token = response.headers.get('x-vqd-4')
return token
}
function convertModel(inputModel) {
let model
switch (inputModel.toLowerCase()) {
case 'claude-3-haiku':
model = 'claude-3-haiku-20240307'
break
case 'llama-3.1-70b':
model = 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo'
break
case 'mixtral-8x7b':
model = 'mistralai/Mixtral-8x7B-Instruct-v0.1'
break
}
return model || 'gpt-4o-mini'
}
function newChatCompletionChunkWithModel(text, model) {
return {
id: 'chatcmpl-QXlha2FBbmROaXhpZUFyZUF3ZXNvbWUK',
object: 'chat.completion.chunk',
created: 0,
model,
choices: [
{
index: 0,
delta: {
content: text,
},
finish_reason: null,
},
],
}
}
function newStopChunkWithModel(reason, model) {
return {
id: 'chatcmpl-QXlha2FBbmROaXhpZUFyZUF3ZXNvbWUK',
object: 'chat.completion.chunk',
created: 0,
model,
choices: [
{
index: 0,
finish_reason: reason,
},
],
}
}
function newChatCompletionWithModel(text, model) {
return {
id: 'chatcmpl-QXlha2FBbmROaXhpZUFyZUF3ZXNvbWUK',
object: 'chat.completion',
created: 0,
model,
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
choices: [
{
message: {
content: text,
role: 'assistant',
},
index: 0,
},
],
}
}
// Serverless Service
;(async () => {
//For Cloudflare Workers
if (typeof addEventListener === 'function') return
// For Nodejs
const ittyServer = createServerAdapter(router.fetch)
console.log(`Listening on http://localhost:${process.env.PORT || 8787}`)
const httpServer = createServer(ittyServer)
httpServer.listen(8787)
})()
// export default router