|
|
|
|
|
|
|
|
import { serve } from "https://deno.land/std@0.203.0/http/server.ts"; |
|
|
|
|
|
|
|
|
|
|
|
function getKeysFromEnv(envVarName: string): Set<string> { |
|
|
const keysString = Deno.env.get(envVarName); |
|
|
if (!keysString) { |
|
|
console.warn(`Environment variable ${envVarName} is not set.`); |
|
|
return new Set(); |
|
|
} |
|
|
|
|
|
return new Set(keysString.split(',').map(k => k.trim()).filter(Boolean)); |
|
|
} |
|
|
|
|
|
|
|
|
const CLIENT_API_KEYS = getKeysFromEnv("CLIENT_KEYS"); |
|
|
|
|
|
|
|
|
const codegeeXTokensRaw = Array.from(getKeysFromEnv("CODEGEEX_KEYS")); |
|
|
|
|
|
const CODEGEEX_TOKENS: { |
|
|
token: string; |
|
|
isValid: boolean; |
|
|
lastUsed: number; |
|
|
errorCount: number; |
|
|
}[] = codegeeXTokensRaw.map(token => ({ |
|
|
token: token, |
|
|
isValid: true, |
|
|
lastUsed: 0, |
|
|
errorCount: 0 |
|
|
})); |
|
|
|
|
|
|
|
|
const MAX_ERROR_COUNT = 3; |
|
|
const ERROR_COOLDOWN = 300 * 1000; |
|
|
|
|
|
|
|
|
function now(): number { |
|
|
return Date.now(); |
|
|
} |
|
|
|
|
|
function rotateToken(): typeof CODEGEEX_TOKENS[0] | null { |
|
|
if (CODEGEEX_TOKENS.length === 0) { |
|
|
console.error("CODEGEEX_TOKENS array is empty. Check your CODEGEEX_KEYS secret."); |
|
|
return null; |
|
|
} |
|
|
|
|
|
const available = CODEGEEX_TOKENS.filter(t => { |
|
|
if (!t.isValid) return false; |
|
|
if (t.errorCount >= MAX_ERROR_COUNT && now() - t.lastUsed < ERROR_COOLDOWN) return false; |
|
|
return true; |
|
|
}); |
|
|
if (available.length === 0) return null; |
|
|
|
|
|
|
|
|
for (const t of available) { |
|
|
if (t.errorCount >= MAX_ERROR_COUNT && now() - t.lastUsed >= ERROR_COOLDOWN) { |
|
|
t.errorCount = 0; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
available.sort((a, b) => a.lastUsed - b.lastUsed || a.errorCount - b.errorCount); |
|
|
const tok = available[0]; |
|
|
tok.lastUsed = now(); |
|
|
return tok; |
|
|
} |
|
|
|
|
|
|
|
|
function convertToCodeGeeXPayload(params: { model: string; messages: any[] }) { |
|
|
|
|
|
|
|
|
const lastMessage = params.messages.slice(-1)[0]; |
|
|
const history = params.messages.slice(0, -1) |
|
|
.filter(msg => msg.role === 'user' || msg.role === 'assistant') |
|
|
.map(msg => ({ |
|
|
role: msg.role, |
|
|
content: msg.content |
|
|
})); |
|
|
|
|
|
return { |
|
|
user_role: 0, |
|
|
ide: "HuggingFace", |
|
|
prompt: lastMessage?.content || "", |
|
|
history: history, |
|
|
model: params.model, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
async function proxyChat(req: Request, params: { stream: boolean; model: string; messages: any[] }) { |
|
|
const tokenObj = rotateToken(); |
|
|
if (!tokenObj) { |
|
|
return new Response(JSON.stringify({ error: { message: "No valid CodeGeeX tokens available", type: "server_error" } }), { status: 503, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
|
|
|
const payload = convertToCodeGeeXPayload(params); |
|
|
|
|
|
try { |
|
|
const response = await fetch("https://codegeex.cn/prod/code/chatCodeSseV3/chat", { |
|
|
method: "POST", |
|
|
headers: { |
|
|
"Content-Type": "application/json", |
|
|
"Accept": "text/event-stream", |
|
|
"code-token": tokenObj.token, |
|
|
}, |
|
|
body: JSON.stringify(payload), |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
console.error(`Upstream error from CodeGeeX: ${response.status}`); |
|
|
if (response.status === 401 || response.status === 403) { |
|
|
tokenObj.isValid = false; |
|
|
console.warn(`Token ${tokenObj.token.substring(0, 15)}... marked as invalid due to 401/403 error.`); |
|
|
} else { |
|
|
tokenObj.errorCount++; |
|
|
console.warn(`Token ${tokenObj.token.substring(0, 15)}... error count increased to ${tokenObj.errorCount}.`); |
|
|
} |
|
|
const errorBody = await response.text(); |
|
|
return new Response(JSON.stringify({ error: { message: `Upstream error ${response.status}: ${errorBody}`, type: "upstream_error" } }), { status: 502, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
|
|
|
|
|
|
if (params.stream) { |
|
|
const { readable, writable } = new TransformStream(); |
|
|
const writer = writable.getWriter(); |
|
|
const encoder = new TextEncoder(); |
|
|
|
|
|
|
|
|
(async () => { |
|
|
const reader = response.body?.getReader(); |
|
|
if (!reader) { |
|
|
await writer.close(); |
|
|
return; |
|
|
} |
|
|
const decoder = new TextDecoder(); |
|
|
const completionId = `chatcmpl-${crypto.randomUUID()}`; |
|
|
const creationTime = Math.floor(now() / 1000); |
|
|
|
|
|
try { |
|
|
while(true) { |
|
|
const { done, value } = await reader.read(); |
|
|
if (done) break; |
|
|
|
|
|
const chunkText = decoder.decode(value); |
|
|
|
|
|
const openAIChunk = { |
|
|
id: completionId, |
|
|
object: "chat.completion.chunk", |
|
|
created: creationTime, |
|
|
model: params.model, |
|
|
choices: [{ delta: { content: chunkText }, index: 0, finish_reason: null }] |
|
|
}; |
|
|
await writer.write(encoder.encode(`data: ${JSON.stringify(openAIChunk)}\n\n`)); |
|
|
} |
|
|
|
|
|
await writer.write(encoder.encode(`data: [DONE]\n\n`)); |
|
|
} catch (e) { |
|
|
console.error("Error while transforming stream:", e); |
|
|
} finally { |
|
|
await writer.close(); |
|
|
} |
|
|
})(); |
|
|
|
|
|
return new Response(readable, { |
|
|
status: 200, |
|
|
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }, |
|
|
}); |
|
|
} else { |
|
|
|
|
|
const text = await response.text(); |
|
|
return new Response(JSON.stringify({ |
|
|
id: `chatcmpl-${crypto.randomUUID()}`, |
|
|
object: "chat.completion", |
|
|
created: Math.floor(now() / 1000), |
|
|
model: params.model, |
|
|
choices: [{ message: { role: "assistant", content: text }, index: 0, finish_reason: "stop" }], |
|
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } |
|
|
}), { |
|
|
status: 200, |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
}); |
|
|
} |
|
|
} catch (err) { |
|
|
tokenObj.errorCount++; |
|
|
console.error("Fetch to CodeGeeX failed:", err); |
|
|
return new Response(JSON.stringify({ error: { message: err.message, type: "server_error" } }), { status: 500, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function handler(req: Request): Promise<Response> { |
|
|
const url = new URL(req.url); |
|
|
console.log(`Received request: ${req.method} ${url.pathname}`); |
|
|
|
|
|
|
|
|
if (req.method === 'OPTIONS') { |
|
|
return new Response(null, { |
|
|
status: 204, |
|
|
headers: { |
|
|
'Access-Control-Allow-Origin': '*', |
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', |
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization', |
|
|
}, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const auth = req.headers.get("Authorization")?.replace(/^Bearer\s+/, ""); |
|
|
if (CLIENT_API_KEYS.size === 0) { |
|
|
console.error("Server misconfigured: CLIENT_KEYS secret is not set or empty."); |
|
|
return new Response(JSON.stringify({ error: { message: "Server misconfigured: no client keys", type: "server_error" }}), { status: 503, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
if (!auth || !CLIENT_API_KEYS.has(auth)) { |
|
|
return new Response(JSON.stringify({ error: { message: "Invalid or missing API key", type: "auth_error" }}), { |
|
|
status: 401, |
|
|
headers: { "WWW-Authenticate": "Bearer", "Content-Type": "application/json" }, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (url.pathname === "/v1/models" && req.method === "GET") { |
|
|
const modelData = [ |
|
|
{ id: "codegeex-4", object: "model", created: Math.floor(now() / 1000), owned_by: "codegeex" }, |
|
|
{ id: "codegeex-pro", object: "model", created: Math.floor(now() / 1000), owned_by: "codegeex" } |
|
|
]; |
|
|
return new Response(JSON.stringify({ object: "list", data: modelData }), { |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (url.pathname === "/v1/chat/completions" && req.method === "POST") { |
|
|
try { |
|
|
const body = await req.json(); |
|
|
const { model, messages, stream = true } = body; |
|
|
if (!model || !Array.isArray(messages) || messages.length === 0) { |
|
|
return new Response(JSON.stringify({ error: { message: "Bad Request: 'model' and 'messages' are required.", type: "invalid_request_error" } }), { status: 400, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
return proxyChat(req, { model, messages, stream }); |
|
|
} catch (e) { |
|
|
return new Response(JSON.stringify({ error: { message: "Invalid JSON body.", type: "invalid_request_error" } }), { status: 400, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return new Response(JSON.stringify({ error: "Not Found" }), { status: 404, headers: { "Content-Type": "application/json" }}); |
|
|
} |
|
|
|
|
|
|
|
|
const PORT = 7860; |
|
|
console.log(`Starting Deno CodeGeeX Adapter on http://0.0.0.0:${PORT}`); |
|
|
serve(handler, { port: PORT }); |
|
|
|