Spaces:
Sleeping
Sleeping
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.PORT = process.env.PORT || 8787 | |
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: 'dcm=3', | |
Dnt: '1', | |
Priority: 'u=1, i', | |
Referer: 'https://duckduckgo.com/', | |
'Sec-Ch-Ua': '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', | |
'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/130.0.0.0 Safari/537.36 Edg/130.0.0.0', | |
} | |
} | |
} | |
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. Please check whether the calling URL is correct.'), | |
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' }, | |
{ id: 'o3-mini', 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) { | |
return 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(`Create Completion 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() { | |
try { | |
const response = await fetch(`https://duckduckgo.com/duckchat/v1/status`, { | |
method: 'GET', | |
headers: { | |
...config.FAKE_HEADERS, | |
'x-vqd-accept': '1', | |
}, | |
}) | |
const token = response.headers.get('x-vqd-4') | |
return token | |
} catch (error) { | |
console.log("Request token error: ", err) | |
} | |
} | |
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 | |
case 'o3-mini': | |
model = 'o3-mini' | |
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 Node.js | |
const ittyServer = createServerAdapter(router.fetch) | |
console.log(`Listening on http://0.0.0.0:7860`) | |
const httpServer = createServer(ittyServer) | |
httpServer.listen(7860, '0.0.0.0') // Force binding to 0.0.0.0 on port 7860 | |
})() | |
// export default router |