| |
|
|
| 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 }); |
|
|