Spaces:
Running
Running
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 || 7860; | |
this.API_PREFIX = process.env.API_PREFIX || '/'; | |
this.MAX_RETRY_COUNT = parseInt(process.env.MAX_RETRY_COUNT) || 3; | |
this.RETRY_DELAY = parseInt(process.env.RETRY_DELAY) || 5000; | |
this.FAKE_HEADERS = { | |
'Accept': 'application/json', | |
'Content-Type': 'application/json', | |
'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', | |
'Referer': 'https://duckduckgo.com/', | |
'Origin': 'https://duckduckgo.com', | |
'x-vqd-accept': '1', | |
...JSON.parse(process.env.FAKE_HEADERS || '{}'), | |
}; | |
} | |
} | |
const config = new Config(); | |
const { preflight, corsify } = cors({ | |
origin: '*', | |
allowMethods: '*', | |
exposeHeaders: '*', | |
}); | |
const withBenchmarking = (request) => { | |
request.start = Date.now(); | |
}; | |
const getClientIP = (req) => { | |
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || | |
req.headers['x-real-ip'] || | |
req.socket?.remoteAddress || | |
null; | |
}; | |
const logger = (res, req) => { | |
console.log(req.method, res.status, req.url, Date.now() - req.start, 'ms'); | |
}; | |
const router = AutoRouter({ | |
before: [withBenchmarking, preflight], | |
missing: () => error(404, '404 Not Found. Please check whether the calling URL is correct.'), | |
finally: [corsify, logger], | |
}); | |
router.get('/', (req) => json({ message: 'API Service Running!' })); | |
router.get('/ping', (req) => json({ message: 'pong' })); | |
router.get(config.API_PREFIX + 'v1/models', (req) => | |
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); | |
const clientIp = getClientIP(request); | |
return createCompletion(model, content, returnStream, clientIp); | |
} catch (err) { | |
console.error('Handle Completion Error:', err); | |
return error(500, err.message); | |
} | |
} | |
async function createCompletion(model, content, returnStream, clientIp, retryCount = 0) { | |
try { | |
const token = await requestToken(clientIp); | |
const response = await fetch('https://duckduckgo.com/duckchat/v1/chat', { | |
method: 'POST', | |
headers: { | |
...config.FAKE_HEADERS, | |
'x-vqd-4': token, | |
...(clientIp ? { 'x-forwarded-for': clientIp } : {}), | |
}, | |
body: JSON.stringify({ | |
model, | |
messages: [{ role: 'user', content }], | |
}), | |
}); | |
if (!response.ok) { | |
if (response.status === 418) { | |
console.warn('Rate limit hit (418), retrying...'); | |
throw new Error('Rate limit exceeded'); | |
} | |
throw new Error(`Create Completion error! status: ${response.status}, message: ${await response.text()}`); | |
} | |
return handlerStream(model, response.body, returnStream); | |
} catch (err) { | |
console.error('Create Completion Error:', err.message); | |
if (retryCount < config.MAX_RETRY_COUNT && (err.message.includes('Rate limit') || err.message.includes('418'))) { | |
console.log('Retrying... count', ++retryCount); | |
await new Promise((resolve) => setTimeout(resolve, config.RETRY_DELAY)); | |
return createCompletion(model, content, returnStream, clientIp, retryCount); | |
} | |
throw err; | |
} | |
} | |
async function handlerStream(model, body, returnStream) { | |
const reader = body.getReader(); | |
const decoder = new TextDecoder(); | |
const encoder = new TextEncoder(); | |
let previousText = ''; | |
const stream = new ReadableStream({ | |
async start(controller) { | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { | |
if (!returnStream) { | |
controller.enqueue(encoder.encode(JSON.stringify(newChatCompletionWithModel(previousText, model)))); | |
} | |
return controller.close(); | |
} | |
const chunk = decoder.decode(value).trim(); | |
const lines = chunk.split('\n').filter((line) => line.startsWith('data: ')); | |
for (const line of lines) { | |
const data = line.slice(6); | |
if (data === '[DONE]') { | |
if (returnStream) { | |
controller.enqueue(encoder.encode(`data: ${JSON.stringify(newStopChunkWithModel('stop', model))}\n\n`)); | |
} | |
return controller.close(); | |
} | |
try { | |
const parsed = JSON.parse(data); | |
if (parsed.message) { | |
previousText += parsed.message; | |
if (returnStream) { | |
controller.enqueue( | |
encoder.encode(`data: ${JSON.stringify(newChatCompletionChunkWithModel(parsed.message, model))}\n\n`) | |
); | |
} | |
} | |
} catch (err) { | |
console.error('Stream parse error:', err); | |
} | |
} | |
} | |
}, | |
}); | |
return new Response(stream, { | |
headers: { | |
'Content-Type': returnStream ? 'text/event-stream' : 'application/json', | |
'Cache-Control': 'no-cache', | |
'Connection': 'keep-alive', | |
}, | |
}); | |
} | |
function messagesPrepare(messages) { | |
return messages | |
.filter((msg) => ['user', 'assistant'].includes(msg.role)) | |
.map((msg) => msg.content) | |
.join('\n'); | |
} | |
async function requestToken(clientIp) { | |
try { | |
const response = await fetch('https://duckduckgo.com/duckchat/v1/status', { | |
method: 'GET', | |
headers: { | |
...config.FAKE_HEADERS, | |
...(clientIp ? { 'x-forwarded-for': clientIp } : {}), | |
}, | |
}); | |
if (!response.ok) { | |
throw new Error('Request Token failed!'); | |
} | |
const data = await response.json(); | |
return data?.vqd; | |
} catch (err) { | |
console.error('Request Token Error:', err); | |
throw err; | |
} | |
} | |
function convertModel(model) { | |
return model; // Adjust if needed | |
} | |
function newChatCompletionWithModel(content, model) { | |
return { | |
id: 'chatcmpl-' + Date.now(), | |
object: 'chat.completion', | |
created: Math.floor(Date.now() / 1000), | |
model, | |
choices: [ | |
{ | |
index: 0, | |
message: { role: 'assistant', content }, | |
finish_reason: 'stop', | |
}, | |
], | |
}; | |
} | |
function newChatCompletionChunkWithModel(content, model) { | |
return { | |
id: 'chatcmpl-' + Date.now(), | |
object: 'chat.completion.chunk', | |
created: Math.floor(Date.now() / 1000), | |
model, | |
choices: [ | |
{ | |
delta: { content }, | |
index: 0, | |
finish_reason: null, | |
}, | |
], | |
}; | |
} | |
function newStopChunkWithModel(reason, model) { | |
return { | |
id: 'chatcmpl-' + Date.now(), | |
object: 'chat.completion.chunk', | |
created: Math.floor(Date.now() / 1000), | |
model, | |
choices: [ | |
{ | |
delta: {}, | |
index: 0, | |
finish_reason: reason, | |
}, | |
], | |
}; | |
} | |
// Create Server | |
createServer(createServerAdapter(router.fetch)).listen(config.PORT, () => { | |
console.log('Server running at http://localhost:' + config.PORT); | |
}); | |