// deno run --allow-net --allow-env adapter.ts import { serve } from "https://deno.land/std@0.203.0/http/server.ts"; // --- Configuration from Environment Variables (Safer for deployment) --- function getKeysFromEnv(envVarName: string): Set { const keysString = Deno.env.get(envVarName); if (!keysString) { console.warn(`Environment variable ${envVarName} is not set.`); return new Set(); } // Split by comma and trim whitespace, filter out empty strings return new Set(keysString.split(',').map(k => k.trim()).filter(Boolean)); } // Client keys will be read from Hugging Face Secrets const CLIENT_API_KEYS = getKeysFromEnv("CLIENT_KEYS"); // CodeGeeX tokens will also be read from Hugging Face Secrets 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; // ms // --- Utilities --- 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; // reset cooled-down tokens for (const t of available) { if (t.errorCount >= MAX_ERROR_COUNT && now() - t.lastUsed >= ERROR_COOLDOWN) { t.errorCount = 0; } } // pick the one least recently used, then lowest errorCount available.sort((a, b) => a.lastUsed - b.lastUsed || a.errorCount - b.errorCount); const tok = available[0]; tok.lastUsed = now(); return tok; } // This function translates the OpenAI format to CodeGeeX format function convertToCodeGeeXPayload(params: { model: string; messages: any[] }) { // CodeGeeX seems to use the last message's content as the main prompt. // The history part is more complex, here we simplify it. 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, // This seems to be a fixed value ide: "HuggingFace", // Let's identify the source prompt: lastMessage?.content || "", history: history, // Passing a simplified 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" }}); } // For stream, we must transform the raw CodeGeeX SSE to OpenAI format if (params.stream) { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); // This function processes the stream from CodeGeeX and sends OpenAI compatible chunks (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); // A simple transformation: assume the raw chunk is the content delta 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`)); } // Send the final DONE chunk 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 { // accumulate and return JSON 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 } // Placeholder usage }), { 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" }}); } } // --- Main Handler --- async function handler(req: Request): Promise { const url = new URL(req.url); console.log(`Received request: ${req.method} ${url.pathname}`); // CORS preflight request handler for web clients 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', }, }); } // Authentication middleware 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" }, }); } // GET /v1/models 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" }, }); } // POST /v1/chat/completions 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" }}); } } // Not found return new Response(JSON.stringify({ error: "Not Found" }), { status: 404, headers: { "Content-Type": "application/json" }}); } // --- Start Server --- const PORT = 7860; // Use the standard port for Hugging Face Spaces console.log(`Starting Deno CodeGeeX Adapter on http://0.0.0.0:${PORT}`); serve(handler, { port: PORT });