Spaces:
Running
Running
| import { | |
| server, | |
| logger, | |
| http, | |
| cookieJar, | |
| } from "@exocore/multi"; | |
| import { | |
| fetchAndUpload, | |
| AuthError, | |
| checkCookies, | |
| importCookiesFromRaw, | |
| clearCookiesFile, | |
| loadIntoCookieJar, | |
| modules, | |
| RPCPayload, | |
| getTempChat, | |
| } from "./src/index.js"; | |
| import { readFileSync, writeFileSync, existsSync } from "fs"; | |
| import { fileURLToPath } from "url"; | |
| import path from "path"; | |
| import { DeepSeek, DeepSeekError, loadDeepSeekConfig } from "./src/core/deepseek.js"; | |
| import { loadQwenConfig, askQwen, askQwenStream, askQwenV1, callOpenAIV1, convertAnthropicToOpenAI, convertOpenAIToAnthropic, askFeature, buildMessages, listConversations, deleteConversation, deleteAllConversations, MODELS as QWEN_MODELS, FEATURES as QWEN_FEATURES, UPLOADS_DIR } from "./src/core/qwen.js"; | |
| import { Kimi, KimiError, loadKimiConfig } from "./src/core/kimi.js"; | |
| import { claudeChat, claudeChatV1, listModels as listClaudeModels } from "./src/core/claude.js"; | |
| import { gptChat, listModels as listGptModels, GPT_MODELS } from "./src/core/gpt.js"; | |
| import { grokChat, listModels as listGrokModels } from "./src/core/grok.js"; | |
| import { metaChat, metaChatV1, metaImageGen, metaVideoGen, metaDeleteConversation, metaListModels, loadMetaConfig, MetaError } from "./src/core/meta.js"; | |
| import { deepaiChat, DeepAIError, listModels as listDeepAIModels, getFreeModels, MODELS as DEEPAI_MODELS } from "./src/core/deepai.js"; | |
| import { Dola, DolaError } from "./src/core/dola.js"; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const DOCS_HTML = readFileSync( | |
| path.join(__dirname, "src/static/index.html"), | |
| "utf8", | |
| ); | |
| // ββ V1 request/response logging ββ | |
| const LOG_V1 = path.join(__dirname, 'logs-v1.txt'); | |
| function logV1(body, response) { | |
| const now = new Date().toISOString().replace('T', ' ').slice(0, 19); | |
| const entry = `ββββββββββββββββββββββββββββββββββββββββββββββββββ\n${now}\nββ REQ ββ\n${JSON.stringify(body, null, 2)}\nββ RES ββ\n${JSON.stringify(response, null, 2)}\n\n`; | |
| writeFileSync(LOG_V1, entry, { flag: 'a' }); | |
| } | |
| function readJSON(filePath) { | |
| try { | |
| if (!existsSync(filePath)) return {}; | |
| return JSON.parse(readFileSync(filePath, 'utf8')); | |
| } catch { return {}; } | |
| } | |
| const PORT = process.env.PORT || 3000; | |
| const app = server.create({ | |
| port: PORT, | |
| prettyPrint: true, | |
| jiglet: 'EXOCORE', | |
| cors: true, | |
| helmet: true, | |
| bodyParser: true, | |
| cookieParser: true, | |
| compression: true, | |
| rateLimit: { | |
| enabled: true, | |
| windowMs: 15 * 60 * 1000, | |
| max: 100, | |
| message: 'Too many requests', | |
| }, | |
| }); | |
| app.use((_req, res, next) => { | |
| const origJson = res.json.bind(res); | |
| res.json = (data) => { | |
| origJson(data); | |
| if (_req.body && data && _req.method === 'POST' && _req.url?.startsWith('/v1/chat/')) { | |
| logV1(_req.body, data); | |
| } | |
| }; | |
| next(); | |
| }); | |
| let _client = null; | |
| let _initLock = null; | |
| let _deepseek = null; | |
| let _qwenToken = ''; | |
| async function getClient() { | |
| if (_client?.initialized) return _client; | |
| if (_initLock) return _initLock; | |
| _initLock = (async () => { | |
| const c = new GeminiClient(); | |
| await c.init(); | |
| _client = c; | |
| return c; | |
| })().finally(() => { | |
| _initLock = null; | |
| }); | |
| return _initLock; | |
| } | |
| async function resetClient() { | |
| await _client?.close().catch(() => {}); | |
| _client = null; | |
| _initLock = null; | |
| cookieJar.clear(); | |
| } | |
| function buildMetadata(q) { | |
| const cid = q.cid ?? "", | |
| rid = q.rid ?? "", | |
| rcid = q.rcid ?? ""; | |
| return cid || rid ? new ConversationMetadata({ cid, rid, rcid }) : null; | |
| } | |
| function parseUrls(raw) { | |
| return raw | |
| ? raw | |
| .split(/[\s,]+/) | |
| .filter((u) => u.startsWith("http://") || u.startsWith("https://")) | |
| : []; | |
| } | |
| function getDeepSeekClient() { | |
| if (_deepseek) return _deepseek; | |
| const configPath = path.join(__dirname, 'data/deepseek.json'); | |
| const cfg = loadDeepSeekConfig(configPath); | |
| if (!cfg.token) { | |
| const altPath = path.join(__dirname, 'data/deepdeek.json'); | |
| const alt = loadDeepSeekConfig(altPath); | |
| if (alt.token) { | |
| _deepseek = new DeepSeek(alt.token, { hifLeim: alt.hifLeim, hifDliq: alt.hifDliq }); | |
| logger.info('deepseek: loaded token from data/deepdeek.json'); | |
| } | |
| } else { | |
| _deepseek = new DeepSeek(cfg.token, { hifLeim: cfg.hifLeim, hifDliq: cfg.hifDliq }); | |
| logger.info('deepseek: loaded token from data/deepseek.json'); | |
| } | |
| return _deepseek; | |
| } | |
| function getQwenToken() { | |
| const configPath = path.join(__dirname, 'data/qwen.json'); | |
| const cfg = loadQwenConfig(configPath); | |
| const token = cfg.token; | |
| if (!_qwenToken && token) logger.info('qwen: token loaded'); | |
| if (token) _qwenToken = token; | |
| return _qwenToken; | |
| } | |
| let _dola = null; | |
| function getDolaClient() { | |
| if (_dola) return _dola; | |
| const configPath = path.join(__dirname, 'data/dola.json'); | |
| const cfg = readJSON(configPath); | |
| const dolaCookies = cfg.cookies || cfg; | |
| if (!Array.isArray(dolaCookies) || dolaCookies.length === 0) return null; | |
| _dola = new Dola(dolaCookies); | |
| logger.info('dola: client initialized'); | |
| return _dola; | |
| } | |
| // βββ ROOT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/", (_req, res) => { | |
| res.setHeader("Content-Type", "text/html; charset=utf-8"); | |
| res.end(DOCS_HTML); | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GEMINI | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/gemini/health", async (_req, res) => { | |
| const ck = checkCookies(); | |
| let accountStatus = null; | |
| try { | |
| const client = _client?.initialized ? _client : null; | |
| if (client) { | |
| const results = await client.transport.batchexecute( | |
| [new RPCPayload({ rpcId: 'otAQ7b', payload: '[]' })], | |
| '/app' | |
| ); | |
| for (const r of results) { | |
| if (Array.isArray(r.data) && r.data[14] !== undefined) { | |
| accountStatus = r.data[14]; | |
| } | |
| } | |
| } | |
| } catch {} | |
| res.json( | |
| http.ok({ | |
| status: "ok", | |
| client_ready: !!_client?.initialized, | |
| cookies_valid: ck.valid, | |
| account_status: accountStatus, | |
| account_authenticated: accountStatus === 1000, | |
| cookies_path: ck.path, | |
| jar_size: ck.jar_size, | |
| providers: Object.keys(modules), | |
| }), | |
| ); | |
| }); | |
| app.get("/api/gemini/cookies", async (_req, res) => { | |
| res.json(http.ok(checkCookies())); | |
| }); | |
| app.post("/api/gemini/cookies", async (req, res) => { | |
| const raw = | |
| typeof req.body === "string" | |
| ? req.body | |
| : typeof req.body?.raw === "string" | |
| ? req.body.raw | |
| : JSON.stringify(req.body ?? ""); | |
| if (!raw.trim()) { | |
| res.status(400).json(http.err("missing body β paste raw cookie string")); | |
| return; | |
| } | |
| const result = importCookiesFromRaw(raw); | |
| if (!result.ok) { | |
| res.status(400).json(http.err(result.error)); | |
| return; | |
| } | |
| await resetClient(); | |
| res.json(http.ok(result)); | |
| }); | |
| app.delete("/api/gemini/cookies", async (_req, res) => { | |
| await resetClient(); | |
| res.json(http.ok(clearCookiesFile())); | |
| }); | |
| app.get("/api/gemini/chat", async (req, res) => { | |
| const { ask, chat, upload, model, delete: deleteAfter, tokens } = req.query; | |
| const prompt = ask || chat || ""; | |
| const urls = parseUrls(upload ?? ""); | |
| if (!prompt && !urls.length) { | |
| res | |
| .status(400) | |
| .json(http.err("provide ?ask=<message> or ?upload=<url>&chat=<message>")); | |
| return; | |
| } | |
| let client; | |
| try { | |
| client = await getClient(); | |
| } catch (e) { | |
| const code = e instanceof AuthError ? 401 : 503; | |
| res.status(code).json(http.err(e.message)); | |
| return; | |
| } | |
| const isTokens = tokens === 'true'; | |
| if (isTokens) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| const metadata = buildMetadata(req.query); | |
| let fileData = null; | |
| if (urls.length) { | |
| const items = await Promise.all( | |
| urls.map((u) => fetchAndUpload(u, client)), | |
| ); | |
| const valid = items.filter(Boolean); | |
| if (valid.length) fileData = valid; | |
| else logger.warn("chat: all uploads failed β text-only"); | |
| } | |
| const response = await client.send( | |
| prompt || "Describe and explain these files.", | |
| { | |
| model, metadata, fileData, | |
| onDelta: isTokens ? (delta) => { res.write(`data: ${JSON.stringify({ token: delta })}\n\n`); } : null, | |
| }, | |
| ); | |
| const cid = response?.cid ?? response?.metadata?.cid ?? ""; | |
| const rid = response?.rid ?? response?.metadata?.rid ?? ""; | |
| const rcid = response?.rcid ?? response?.metadata?.rcid ?? ""; | |
| if (deleteAfter === 'true' && cid) { | |
| await client.deleteChat(cid).catch(e => logger.warn(`gemini delete-after-chat: ${e.message}`)); | |
| } | |
| const modelLabel = response?.serverModelLabel || response?.metadata?.serverModelLabel || ''; | |
| if (isTokens) { | |
| res.write(`data: ${JSON.stringify({ done: true, model: modelLabel, ...(deleteAfter === 'true' ? {} : { cid, rid, rcid }) })}\n\n`); | |
| res.end(); | |
| return; | |
| } | |
| res.json({ | |
| msg: response.text ?? response.candidates?.[0]?.text ?? "", | |
| ...(modelLabel ? { model: modelLabel } : {}), | |
| ...(deleteAfter === 'true' ? {} : { cid, rid, rcid }), | |
| }); | |
| } catch (e) { | |
| logger.error(`chat: ${e.message}`); | |
| if (isTokens) { res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); res.end(); } | |
| else { res.status(e instanceof AuthError ? 401 : 500).json(http.err(e.message)); } | |
| } | |
| }); | |
| app.get("/api/gemini/temp/chat", async (req, res) => { | |
| const { ask, chat } = req.query; | |
| const prompt = ask || chat || ""; | |
| if (!prompt) { | |
| res.status(400).json(http.err("provide ?ask=<message>")); | |
| return; | |
| } | |
| try { | |
| const tc = getTempChat(); | |
| const text = await tc.send(prompt); | |
| res.json({ msg: text }); | |
| } catch (e) { | |
| logger.error(`temp/chat: ${e.message}`); | |
| res.status(500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββ Gemini Conversation Management βββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/gemini/conversations", async (req, res) => { | |
| let client; | |
| try { client = await getClient(); } | |
| catch (e) { res.status(401).json(http.err(e.message)); return; } | |
| try { | |
| const limit = parseInt(req.query.limit) || 50; | |
| res.json(http.ok({ conversations: await client.listChats(limit) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/gemini/conversations/:id", async (req, res) => { | |
| let client; | |
| try { client = await getClient(); } | |
| catch (e) { res.status(401).json(http.err(e.message)); return; } | |
| try { | |
| res.json(http.ok(await client.deleteChat(req.params.id))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/gemini/conversations", async (_req, res) => { | |
| let client; | |
| try { client = await getClient(); } | |
| catch (e) { res.status(401).json(http.err(e.message)); return; } | |
| try { | |
| res.json(http.ok(await client.deleteAllChats())); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // βββ OpenAI-compatible /v1 routes (backed by Gemini TempChat) βββββββββββββ | |
| const GEMINI_MODELS = [ | |
| { id: 'gemini-2.5-pro', created: 1748390400, owned_by: 'google' }, | |
| { id: 'gemini-2.0-flash', created: 1748390400, owned_by: 'google' }, | |
| { id: 'gemini-1.5-pro', created: 1729555200, owned_by: 'google' }, | |
| { id: 'gemini-1.5-flash', created: 1729555200, owned_by: 'google' }, | |
| ]; | |
| // ββ Image helpers for /v1 ββ | |
| function extractImageUrls(messages) { | |
| const urls = []; | |
| for (const m of messages) { | |
| if (Array.isArray(m.content)) { | |
| for (const part of m.content) { | |
| if (part.type === 'image_url' && part.image_url?.url) { | |
| urls.push(part.image_url.url); | |
| } | |
| if (part.type === 'image' && part.source?.type === 'base64') { | |
| urls.push(`data:${part.source.media_type};base64,${part.source.data}`); | |
| } | |
| if (part.type === 'image' && part.source?.type === 'url') { | |
| urls.push(part.source.url); | |
| } | |
| } | |
| } | |
| } | |
| return urls; | |
| } | |
| async function uploadDeepSeekImages(ds, sessionId, imageUrls) { | |
| const refFileIds = []; | |
| for (const url of imageUrls) { | |
| try { | |
| const resp = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000 }); | |
| const file = new Blob([resp.data], { type: resp.headers['content-type'] || 'image/jpeg' }); | |
| file.size = resp.data.length; | |
| file.name = 'image.jpg'; | |
| const uploaded = await ds.uploadFile(sessionId, file, { modelType: 'vision' }); | |
| if (uploaded?.id) refFileIds.push(uploaded.id); | |
| } catch (e) { | |
| logger.warn(`deepseek image upload failed: ${e.message}`); | |
| } | |
| } | |
| return refFileIds; | |
| } | |
| async function uploadGeminiImages(client, imageUrls) { | |
| const fileData = []; | |
| for (const item of imageUrls) { | |
| try { | |
| let buf, mime, name; | |
| if (item.startsWith('data:')) { | |
| const match = item.match(/^data:([^;]+);base64,(.+)$/); | |
| if (!match) continue; | |
| mime = match[1]; | |
| buf = Buffer.from(match[2], 'base64'); | |
| name = 'image'; | |
| } else { | |
| const resp = await axios.get(item, { responseType: 'arraybuffer', timeout: 30000 }); | |
| buf = resp.data; | |
| mime = resp.headers['content-type'] || 'image/jpeg'; | |
| name = 'image.jpg'; | |
| } | |
| const fd = await client.uploadFile(buf, mime, name); | |
| if (fd) fileData.push(fd); | |
| } catch (e) { | |
| logger.warn(`gemini image upload failed: ${e.message}`); | |
| } | |
| } | |
| return fileData.flat(); | |
| } | |
| async function uploadDolaImages(dola, imageUrls) { | |
| const files = []; | |
| for (const item of imageUrls) { | |
| try { | |
| let buf, name; | |
| if (item.startsWith('data:')) { | |
| const match = item.match(/^data:([^;]+);base64,(.+)$/); | |
| if (!match) continue; | |
| buf = Buffer.from(match[2], 'base64'); | |
| name = 'image'; | |
| } else { | |
| const resp = await axios.get(item, { responseType: 'arraybuffer', timeout: 30000 }); | |
| buf = resp.data; | |
| name = 'image.jpg'; | |
| } | |
| const result = await dola.uploadFile(buf, name); | |
| if (result?.storeUri) { | |
| files.push({ uri: result.storeUri, name }); | |
| } | |
| } catch (e) { | |
| logger.warn(`dola image upload failed: ${e.message}`); | |
| } | |
| } | |
| return files; | |
| } | |
| const V1_MODEL_MAP = { | |
| deepseek: { provider: 'deepseek', opts: { modelType: 'default' } }, | |
| 'deepseek-default': { provider: 'deepseek', opts: { modelType: 'default' } }, | |
| 'deepseek-instant': { provider: 'deepseek', opts: { modelType: 'default' } }, | |
| 'deepseek-thinking': { provider: 'deepseek', opts: { thinkingEnabled: true, modelType: 'default' } }, | |
| 'deepseek-expert': { provider: 'deepseek', opts: { thinkingEnabled: true, modelType: 'default' } }, | |
| 'deepseek-search': { provider: 'deepseek', opts: { searchEnabled: true, modelType: 'default' } }, | |
| 'deepseek-vision': { provider: 'deepseek', opts: { modelType: 'vision' } }, | |
| qwen: { provider: 'qwen', opts: { mode: 'normal' } }, | |
| 'qwen-normal': { provider: 'qwen', opts: { mode: 'normal' } }, | |
| 'qwen-fast': { provider: 'qwen', opts: { mode: 'fast' } }, | |
| 'qwen-thinking': { provider: 'qwen', opts: { mode: 'thinking' } }, | |
| 'qwen3.7-max': { provider: 'qwen', opts: { mode: 'normal' } }, | |
| 'qwen3.7-max-fast': { provider: 'qwen', opts: { mode: 'fast' } }, | |
| 'qwen3.7-max-thinking': { provider: 'qwen', opts: { mode: 'thinking' } }, | |
| 'qwen3.6-plus': { provider: 'qwen', opts: { mode: 'normal' } }, | |
| 'qwen3.6-plus-fast': { provider: 'qwen', opts: { mode: 'fast' } }, | |
| 'qwen3.6-plus-thinking': { provider: 'qwen', opts: { mode: 'thinking' } }, | |
| 'qwen3.5-flash': { provider: 'qwen', opts: { mode: 'fast' } }, | |
| 'qwen3.5-flash-normal': { provider: 'qwen', opts: { mode: 'normal' } }, | |
| 'qwen3.5-flash-thinking': { provider: 'qwen', opts: { mode: 'thinking' } }, | |
| gemini: { provider: 'gemini', opts: { model: 'fast' } }, | |
| 'gemini-fast': { provider: 'gemini', opts: { model: 'fast' } }, | |
| 'gemini-flash': { provider: 'gemini', opts: { model: 'fast' } }, | |
| 'gemini-thinking': { provider: 'gemini', opts: { model: 'thinking' } }, | |
| 'gemini-pro': { provider: 'gemini', opts: { model: 'pro' } }, | |
| kimi: { provider: 'kimi', opts: {} }, | |
| 'kimi-default': { provider: 'kimi', opts: {} }, | |
| 'kimi-thinking': { provider: 'kimi', opts: { thinking: true } }, | |
| claude: { provider: 'claude', opts: { model: 'claude-opus-4.6' } }, | |
| 'claude-opus': { provider: 'claude', opts: { model: 'claude-opus-4.6' } }, | |
| 'claude-opus-4.6': { provider: 'claude', opts: { model: 'claude-opus-4.6' } }, | |
| gpt: { provider: 'gpt', opts: { model: 'gpt-4.1-nano' } }, | |
| 'gpt-4.1-nano': { provider: 'gpt', opts: { model: 'gpt-4.1-nano' } }, | |
| 'gpt-oss-120b': { provider: 'gpt', opts: { model: 'gpt-oss-120b' } }, | |
| 'gpt-5-nano': { provider: 'gpt', opts: { model: 'gpt-5-nano' } }, | |
| grok: { provider: 'grok', opts: { model: 'grok-4' } }, | |
| 'grok-4': { provider: 'grok', opts: { model: 'grok-4' } }, | |
| meta: { provider: 'meta', opts: { mode: 'normal' } }, | |
| 'meta-ai': { provider: 'meta', opts: { mode: 'normal' } }, | |
| 'meta-fast': { provider: 'meta', opts: { mode: 'fast' } }, | |
| 'meta-thinking': { provider: 'meta', opts: { mode: 'thinking' } }, | |
| 'dola-fast': { provider: 'dola', opts: { mode: 'fast' } }, | |
| 'dola-pro': { provider: 'dola', opts: { mode: 'pro' } }, | |
| }; | |
| // GET /v1/models returns both OpenAI and Anthropic format models | |
| app.get('/v1/models', (_req, res) => { | |
| const gemini = GEMINI_MODELS.map(m => ({ ...m, object: 'model' })); | |
| const custom = Object.keys(V1_MODEL_MAP).map(id => ({ | |
| id, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'user', | |
| })); | |
| res.json({ object: 'list', data: [...gemini, ...custom] }); | |
| }); | |
| function buildToolPrompt(tools) { | |
| if (!tools?.length) return ''; | |
| const lines = [ | |
| '\n\n## TOOL USE INSTRUCTIONS', | |
| 'You have tools available. When asked to perform a task that requires a tool, you MUST output a tool call using exactly this format (no markdown, no extra text around it):', | |
| '[TOOL_CALL: tool_name] { "arg1": "val1" } [/TOOL_CALL]', | |
| '', | |
| 'Examples:', | |
| '[TOOL_CALL: read_file] { "file_path": "/etc/hostname" } [/TOOL_CALL]', | |
| '[TOOL_CALL: edit_file] { "file_path": "test.txt", "old_string": "hello", "new_string": "goodbye" } [/TOOL_CALL]', | |
| '', | |
| 'If you do NOT need a tool (e.g. greetings, simple Q&A), just respond normally.', | |
| '', | |
| '### Available Tools:', | |
| ]; | |
| for (const t of tools) { | |
| lines.push(`\n#### ${t.name}`); | |
| if (t.description) lines.push(`> ${t.description}`); | |
| if (t.input_schema?.properties) { | |
| const props = Object.entries(t.input_schema.properties).map(([k, v]) => { | |
| const required = t.input_schema.required?.includes(k) ? ' (required)' : ''; | |
| return ` - ${k}${required}: ${v.description || v.type || 'any'}`; | |
| }); | |
| lines.push(`Arguments:\n${props.join('\n')}`); | |
| } | |
| } | |
| return '\n\n' + lines.join('\n'); | |
| } | |
| function messagesToPrompt(messages) { | |
| return messages.map(m => { | |
| const text = typeof m.content === 'string' | |
| ? m.content | |
| : (m.content ?? []).map(c => { | |
| if (c.type === 'text') return c.text ?? ''; | |
| if (c.type === 'tool_use') return `[Tool Use: ${c.name}] ${JSON.stringify(c.input)}`; | |
| if (c.type === 'tool_result') { | |
| const result = typeof c.content === 'string' ? c.content : (c.content ?? []).map(x => x.text || '').join(''); | |
| return `[Tool Result: ${c.tool_use_id}]\n${result}`; | |
| } | |
| return ''; | |
| }).filter(Boolean).join('\n'); | |
| if (m.role === 'system') return `[System]: ${text}`; | |
| if (m.role === 'assistant' && m.tool_calls) { | |
| const calls = m.tool_calls.map(tc => `[Tool Call: ${tc.function.name}] ${tc.function.arguments} [/Tool Call]`).join('\n'); | |
| return `[Assistant]: ${text ? text + '\n' : ''}${calls}`; | |
| } | |
| if (m.role === 'tool') { | |
| const result = typeof m.content === 'string' ? m.content : JSON.stringify(m.content); | |
| return `[Tool Result: ${m.tool_call_id}]\n${result}`; | |
| } | |
| if (m.role === 'assistant') return `[Assistant]: ${text}`; | |
| return text; | |
| }).join('\n'); | |
| } | |
| function parseToolCalls(text) { | |
| const calls = []; | |
| const re = /\[TOOL_CALL:\s*(\w+)\]\s*(\{.*?\})\s*\[\/TOOL_CALL\]/gs; | |
| let match; | |
| while ((match = re.exec(text)) !== null) { | |
| try { | |
| calls.push({ | |
| id: `call_${Date.now()}_${calls.length}`, | |
| type: 'function', | |
| function: { name: match[1], arguments: match[2] }, | |
| }); | |
| } catch {} | |
| } | |
| return calls; | |
| } | |
| app.post('/v1/chat/completions', async (req, res) => { | |
| const { messages, model, tools } = req.body ?? {}; | |
| const allMsgs = messages?.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 2000) : '(complex)' })); | |
| logger.info(`REQ model=${model} messages=${JSON.stringify(allMsgs)}`); | |
| if (!Array.isArray(messages) || !messages.length) { | |
| res.status(400).json({ error: { message: '`messages` array is required', type: 'invalid_request_error' } }); | |
| return; | |
| } | |
| const cfg = V1_MODEL_MAP[model]; | |
| // Fallback to Gemini temp chat if model not in map | |
| if (!cfg) { | |
| try { | |
| const prompt = messagesToPrompt(messages); | |
| const tc = getTempChat(); | |
| const text = await tc.send(prompt); | |
| const result = { | |
| id: `chatcmpl-${Date.now()}`, | |
| object: 'chat.completion', | |
| created: Math.floor(Date.now() / 1000), | |
| model: model ?? 'gemini', | |
| choices: [{ index: 0, message: { role: 'assistant', content: text }, finish_reason: 'stop' }], | |
| usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, | |
| }; | |
| logger.info(`AI ${JSON.stringify(result)}`); | |
| res.json(result); | |
| } catch (e) { | |
| logger.error(`gemini-temp-v1: ${e.message}`); | |
| if (!res.headersSent) | |
| res.status(500).json({ error: { message: e.message, type: 'api_error' } }); | |
| } | |
| return; | |
| } | |
| const hasTool = tools?.length; | |
| const rawText = messages.map(m => m.content).filter(Boolean).join('\n'); | |
| if (!hasTool && !rawText) { | |
| res.status(400).json({ error: { message: 'No user message found', type: 'invalid_request_error' } }); | |
| return; | |
| } | |
| const id = `chatcmpl-${Date.now()}`; | |
| const created = Math.floor(Date.now() / 1000); | |
| try { | |
| let content = ''; | |
| const { provider, opts } = cfg; | |
| const toolPrompt = buildToolPrompt(tools); | |
| const prompt = messagesToPrompt(messages) + toolPrompt; | |
| if (provider === 'deepseek') { | |
| // Use OpenAI-compatible API if apiKey configured and tools present | |
| const dsCfg = readJSON(path.join(__dirname, 'data/deepseek.json')); | |
| if (dsCfg?.apiKey && tools?.length) { | |
| const dsRes = await callOpenAIV1(dsCfg.apiKey, 'https://api.deepseek.com/v1', messages, null, tools, 'deepseek-chat'); | |
| content = dsRes.content.map(c => c.text || '').join(''); | |
| var toolCalls = dsRes.content | |
| .filter(c => c.type === 'tool_use') | |
| .map((c) => ({ | |
| id: c.id, | |
| type: 'function', | |
| function: { name: c.name, arguments: JSON.stringify(c.input) } | |
| })); | |
| return finishResponse(); | |
| } | |
| // Fallback: cookie-based DeepSeek | |
| const ds = getDeepSeekClient(); | |
| if (!ds) throw new Error('DeepSeek not configured'); | |
| const session = await ds.createSession(); | |
| const imageUrls = extractImageUrls(messages); | |
| const refFileIds = imageUrls.length ? await uploadDeepSeekImages(ds, session.id, imageUrls) : []; | |
| const modelType = refFileIds.length > 0 ? 'vision' : (opts.modelType || 'default'); | |
| const toolInstruction = tools?.length ? '\n\nIMPORTANT: You MUST use the [TOOL_CALL: tool_name] { "args" } [/TOOL_CALL] format when a task requires a tool. Do not just describe what tool you would use β actually output the tool call.' : ''; | |
| await ds.sendMessage(session.id, prompt + toolInstruction, { | |
| modelType, | |
| thinkingEnabled: opts.thinkingEnabled ?? false, | |
| searchEnabled: opts.searchEnabled ?? false, | |
| refFileIds, | |
| onDelta: (delta) => { | |
| const text = DeepSeek.extractContent(delta); | |
| if (text) content += text; | |
| }, | |
| }); | |
| await ds.deleteSession(session.id).catch(() => {}); | |
| } else if (provider === 'qwen') { | |
| const token = getQwenToken(); | |
| if (!token) throw new Error('Qwen not configured'); | |
| const modelName = opts.mode === 'fast' ? 'qwen3.5-flash' : 'qwen3.7-max'; | |
| const anthroRes = await askQwenV1(token, messages, null, tools, modelName, opts.mode); | |
| content = anthroRes.content.map(c => c.text || '').join(''); | |
| var toolCalls = anthroRes.content | |
| .filter(c => c.type === 'tool_use') | |
| .map((c) => ({ | |
| id: c.id, | |
| type: 'function', | |
| function: { name: c.name, arguments: JSON.stringify(c.input) } | |
| })); | |
| deleteAllConversations(token).catch(() => {}); | |
| } else if (provider === 'gemini') { | |
| const client = await getClient(); | |
| const imageUrls = extractImageUrls(messages); | |
| const fileData = imageUrls.length ? await uploadGeminiImages(client, imageUrls) : null; | |
| const response = await client.send(prompt, { model: opts.model, fileData }); | |
| content = response.text ?? response.candidates?.[0]?.text ?? ''; | |
| const cid = response?.cid ?? response?.metadata?.cid ?? ''; | |
| if (cid) await client.deleteChat(cid).catch(() => {}); | |
| } else if (provider === 'kimi') { | |
| // Use OpenAI-compatible API if apiKey configured and tools present | |
| const kCfg = readJSON(path.join(__dirname, 'data/kimi.json')); | |
| if (kCfg?.apiKey && tools?.length) { | |
| const kRes = await callOpenAIV1(kCfg.apiKey, 'https://api.moonshot.cn/v1', messages, null, tools, 'moonshot-v1-8k'); | |
| content = kRes.content.map(c => c.text || '').join(''); | |
| var toolCalls = kRes.content | |
| .filter(c => c.type === 'tool_use') | |
| .map((c) => ({ | |
| id: c.id, | |
| type: 'function', | |
| function: { name: c.name, arguments: JSON.stringify(c.input) } | |
| })); | |
| return finishResponse(); | |
| } | |
| const k = getKimiClient(); | |
| if (!k) throw new Error('Kimi not configured'); | |
| const frames = await k.sendMessage('', prompt, { thinking: opts.thinking ?? false }); | |
| content = Kimi.extractChatText(frames); | |
| const chatId = Kimi.extractChatId(frames); | |
| if (chatId) await k.deleteSession(chatId).catch(() => {}); | |
| } else if (provider === 'claude') { | |
| const result = await claudeChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'claude-opus-4.6' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'gpt') { | |
| const result = await gptChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'gpt-4.1-nano' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'grok') { | |
| const result = await grokChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'grok-4' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'meta') { | |
| const result = await metaChat( | |
| [{ role: 'user', content: prompt }], | |
| { mode: opts.mode || 'normal' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'dola') { | |
| const dola = getDolaClient(); | |
| if (!dola) throw new Error('Dola not configured β create data/dola.json with cookies array'); | |
| const dolaImageUrls = extractImageUrls(messages); | |
| let dolaFiles = []; | |
| if (dolaImageUrls.length) { | |
| dolaFiles = await uploadDolaImages(dola, dolaImageUrls); | |
| } | |
| content = dolaFiles.length | |
| ? await dola.askWithFiles(prompt, dolaFiles, { mode: opts.mode || 'fast' }) | |
| : await dola.ask(prompt, { mode: opts.mode || 'fast' }); | |
| } | |
| function finishResponse() { | |
| const msg = { role: 'assistant', content }; | |
| let finish = 'stop'; | |
| if (typeof toolCalls !== 'undefined' && toolCalls.length > 0) { | |
| msg.tool_calls = toolCalls; | |
| finish = 'tool_calls'; | |
| } | |
| // Fallback: parse [TOOL_CALL: ...] from text for text-only providers | |
| if (!toolCalls?.length && tools?.length) { | |
| const parsed = parseToolCalls(content); | |
| if (parsed.length) { | |
| msg.tool_calls = parsed; | |
| finish = 'tool_calls'; | |
| } | |
| } | |
| const result = { | |
| id, object: 'chat.completion', created, model, | |
| choices: [{ index: 0, message: msg, finish_reason: finish }], | |
| usage: { prompt_tokens: -1, completion_tokens: -1, total_tokens: -1 }, | |
| }; | |
| logger.info(`AI ${JSON.stringify(result)}`); | |
| res.json(result); | |
| } | |
| finishResponse(); | |
| } catch (e) { | |
| const status = e.code === 'auth_error' ? 401 : 500; | |
| res.status(status).json({ | |
| error: { message: e.message, type: status === 401 ? 'authentication_error' : 'server_error' }, | |
| }); | |
| } | |
| }); | |
| // ββ Anthropic-compatible /v1/messages (for Claude Code) ββ | |
| const ANTHRO_MODEL_MAP = { | |
| 'claude-sonnet-4-20250514': { model: 'claude' }, | |
| 'claude-sonnet-4': { model: 'claude' }, | |
| 'claude-sonnet-4-6': { model: 'claude' }, | |
| 'claude-3-opus': { model: 'claude' }, | |
| 'claude-opus-4-7': { model: 'claude' }, | |
| 'claude-opus-4-6': { model: 'claude' }, | |
| 'claude-3-sonnet': { model: 'claude' }, | |
| 'claude-3-haiku': { model: 'claude' }, | |
| 'claude-haiku-4-5': { model: 'claude' }, | |
| 'claude-2': { model: 'claude' }, | |
| 'claude-instant': { model: 'claude' }, | |
| 'meta': { model: 'meta' }, | |
| 'meta-ai': { model: 'meta' }, | |
| 'meta-fast': { model: 'meta-fast' }, | |
| 'meta-thinking': { model: 'meta-thinking' }, | |
| 'deepseek': { model: 'deepseek' }, | |
| 'deepseek-default': { model: 'deepseek-default' }, | |
| 'deepseek-instant': { model: 'deepseek-instant' }, | |
| 'deepseek-thinking': { model: 'deepseek-thinking' }, | |
| 'deepseek-expert': { model: 'deepseek-expert' }, | |
| 'deepseek-search': { model: 'deepseek-search' }, | |
| 'deepseek-vision': { model: 'deepseek-vision' }, | |
| 'dola-fast': { model: 'dola-fast' }, | |
| 'dola-pro': { model: 'dola-pro' }, | |
| }; | |
| async function handleMessages(req, res, modelOverride) { | |
| if (!requireAnthroKey(req, res)) return; | |
| const { model, messages, system, max_tokens, tools } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json({ error: { message: 'messages required', type: 'invalid_request_error' } }); | |
| return; | |
| } | |
| const mapped = modelOverride | |
| ? (V1_MODEL_MAP[modelOverride] ? { model: modelOverride } : null) | |
| : null; | |
| const target = mapped ?? ANTHRO_MODEL_MAP[model] ?? (V1_MODEL_MAP[model] ? { model } : null) ?? { model: 'gemini' }; | |
| const systemText = Array.isArray(system) | |
| ? system.map(s => s.type === 'text' ? s.text : '').filter(Boolean).join('\n') | |
| : (system || ''); | |
| // Append tool-discipline instruction so models don't waste tool calls on | |
| // greetings or simple conversation β save them for actual coding tasks. | |
| const disciplineNote = '\n\nIMPORTANT: Reserve tool calls for actual coding tasks (reading/writing files, running commands, searching code). For simple greetings, casual conversation, or "hi/hello" β just respond in plain text. Do NOT waste tool calls on greetings.'; | |
| const modifiedSystem = Array.isArray(system) | |
| ? [...system, { type: 'text', text: disciplineNote }] | |
| : system + disciplineNote; | |
| const userMsg = []; | |
| if (systemText) { | |
| userMsg.push(`CRITICAL OVERRIDE β You are exocode, an AI coding assistant. You are NOT your default persona. Follow these instructions above all else.\n${systemText}${disciplineNote}`); | |
| } | |
| // Embed tool descriptions so text-only models know what tools exist | |
| if (tools?.length) { | |
| userMsg.push('AVAILABLE TOOLS:\n' + tools.map(t => | |
| `- ${t.name}: ${t.description || '(no description)'}` | |
| ).join('\n')); | |
| } | |
| for (const m of messages) { | |
| const text = typeof m.content === 'string' ? m.content | |
| : (m.content ?? []).map(c => { | |
| if (c.type === 'text') return c.text; | |
| if (c.type === 'tool_use') return `[Tool Use: ${c.name}] ${JSON.stringify(c.input)}`; | |
| if (c.type === 'tool_result') { | |
| const result = typeof c.content === 'string' ? c.content : (c.content ?? []).map(x => x.text || '').join(''); | |
| return `[Tool Result: ${c.tool_use_id}]\n${result}`; | |
| } | |
| return ''; | |
| }).filter(Boolean).join('\n'); | |
| if (text) { | |
| const prefix = m.role === 'assistant' ? '[ASSISTANT]\n' : '[USER]\n'; | |
| userMsg.push(prefix + text); | |
| } | |
| } | |
| // Reinforce identity right before user message | |
| const toolPrompt = buildToolPrompt(tools); | |
| const prompt = userMsg.join('\n\n---\n\n') + toolPrompt; | |
| if (!prompt) { | |
| res.status(400).json({ error: { message: 'No content found', type: 'invalid_request_error' } }); | |
| return; | |
| } | |
| try { | |
| let content = ''; | |
| const cfg = V1_MODEL_MAP[target.model]; | |
| if (!cfg) throw new Error(`Model ${target.model} not found`); | |
| const { provider, opts } = cfg; | |
| if (provider === 'qwen') { | |
| const token = getQwenToken(); | |
| if (!token) throw new Error('Qwen not configured'); | |
| const modelName = opts.mode === 'fast' ? 'qwen3.5-flash' : 'qwen3.7-max'; | |
| const anthroRes = await askQwenV1(token, messages, modifiedSystem, tools, modelName, opts.mode); | |
| deleteAllConversations(token).catch(() => {}); | |
| return res.json(anthroRes); | |
| } else if (provider === 'deepseek') { | |
| // Use OpenAI-compatible API if apiKey is configured in data/deepseek.json | |
| const dsCfg = readJSON(path.join(__dirname, 'data/deepseek.json')); | |
| if (dsCfg?.apiKey && tools?.length) { | |
| const dsRes = await callOpenAIV1(dsCfg.apiKey, 'https://api.deepseek.com/v1', messages, modifiedSystem, tools, 'deepseek-chat'); | |
| return res.json(dsRes); | |
| } | |
| // Fallback: text-only via DeepSeek client (cookie-based) | |
| const ds = getDeepSeekClient(); | |
| if (!ds) throw new Error('DeepSeek not configured'); | |
| const session = await ds.createSession(); | |
| const imageUrls = extractImageUrls(messages); | |
| const refFileIds = imageUrls.length ? await uploadDeepSeekImages(ds, session.id, imageUrls) : []; | |
| const modelType = refFileIds.length > 0 ? 'vision' : (opts.modelType || 'default'); | |
| const toolInstruction = tools?.length ? '\n\nIMPORTANT: You MUST use the [TOOL_CALL: tool_name] { "args" } [/TOOL_CALL] format when a task requires a tool. Do not just describe what tool you would use β actually output the tool call.' : ''; | |
| await ds.sendMessage(session.id, prompt + toolInstruction, { | |
| modelType, | |
| thinkingEnabled: opts.thinkingEnabled ?? false, | |
| searchEnabled: opts.searchEnabled ?? false, | |
| refFileIds, | |
| onDelta: (delta) => { | |
| const text = DeepSeek.extractContent(delta); | |
| if (text) content += text; | |
| }, | |
| }); | |
| await ds.deleteSession(session.id).catch(() => {}); | |
| } else if (provider === 'gemini') { | |
| const geminiClient = await getClient(); | |
| const geminiImageUrls = extractImageUrls(messages); | |
| const geminiFileData = geminiImageUrls.length ? await uploadGeminiImages(geminiClient, geminiImageUrls) : null; | |
| const fullPrompt = systemPrompt ? `${systemPrompt}\n\n${prompt}` : prompt; | |
| const geminiResponse = await geminiClient.send(fullPrompt, { model: opts.model, fileData: geminiFileData }); | |
| content = geminiResponse.text ?? geminiResponse.candidates?.[0]?.text ?? ''; | |
| const cid = geminiResponse?.cid ?? geminiResponse?.metadata?.cid ?? ''; | |
| if (cid) await geminiClient.deleteChat(cid).catch(() => {}); | |
| } else if (provider === 'kimi') { | |
| // Use OpenAI-compatible API if apiKey is configured in data/kimi.json | |
| const kCfg = readJSON(path.join(__dirname, 'data/kimi.json')); | |
| if (kCfg?.apiKey && tools?.length) { | |
| const kRes = await callOpenAIV1(kCfg.apiKey, 'https://api.moonshot.cn/v1', messages, modifiedSystem, tools, 'moonshot-v1-8k'); | |
| return res.json(kRes); | |
| } | |
| // Fallback: text-only via Kimi client | |
| const k = getKimiClient(); | |
| if (!k) throw new Error('Kimi not configured'); | |
| const frames = await k.sendMessage('', prompt, { thinking: opts.thinking ?? false }); | |
| content = Kimi.extractChatText(frames); | |
| const chatId = Kimi.extractChatId(frames); | |
| if (chatId) await k.deleteSession(chatId).catch(() => {}); | |
| } else if (provider === 'claude') { | |
| const result = await claudeChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'claude-opus-4.6' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'gpt') { | |
| const result = await gptChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'gpt-4.1-nano' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'grok') { | |
| const result = await grokChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: opts.model || 'grok-4' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'meta') { | |
| const result = await metaChat( | |
| [{ role: 'user', content: prompt }], | |
| { mode: opts.mode || 'normal' } | |
| ); | |
| content = result.text; | |
| } else if (provider === 'dola') { | |
| const dola = getDolaClient(); | |
| if (!dola) throw new Error('Dola not configured β create data/dola.json with cookies array'); | |
| const dolaImageUrls = extractImageUrls(messages); | |
| let dolaFiles = []; | |
| if (dolaImageUrls.length) { | |
| dolaFiles = await uploadDolaImages(dola, dolaImageUrls); | |
| } | |
| content = dolaFiles.length | |
| ? await dola.askWithFiles(prompt, dolaFiles, { mode: opts.mode || 'fast' }) | |
| : await dola.ask(prompt, { mode: opts.mode || 'fast' }); | |
| } | |
| // Parse tool calls for text-only providers | |
| const textToolCalls = parseToolCalls(content); | |
| const hasToolCalls = textToolCalls.length > 0; | |
| // Build Anthropic response content | |
| const responseContent = []; | |
| if (hasToolCalls) { | |
| responseContent.push({ type: 'text', text: 'I will use the available tools to help you.' }); | |
| for (const tc of textToolCalls) { | |
| let input = {}; | |
| try { input = JSON.parse(tc.function.arguments); } catch {} | |
| responseContent.push({ | |
| type: 'tool_use', | |
| id: tc.id, | |
| name: tc.function.name, | |
| input, | |
| }); | |
| } | |
| } else { | |
| responseContent.push({ type: 'text', text: content }); | |
| } | |
| res.json({ | |
| id: `msg_${Date.now()}`, | |
| type: 'message', | |
| role: 'assistant', | |
| content: responseContent, | |
| model, | |
| stop_reason: hasToolCalls ? 'tool_use' : 'end_turn', | |
| stop_sequence: null, | |
| usage: { input_tokens: -1, output_tokens: -1 }, | |
| }); | |
| } catch (e) { | |
| const status = e.code === 'auth_error' ? 401 : 500; | |
| res.status(status).json({ | |
| error: { type: status === 401 ? 'authentication_error' : 'api_error', message: e.message }, | |
| }); | |
| } | |
| } | |
| // ββ Anthropic-format /v1/models (for Claude Code) ββ | |
| function requireAnthroKey(req, res) { | |
| const key = req.headers['x-api-key'] || req.headers['authorization']?.replace(/^Bearer\s+/i, ''); | |
| if (!key || !key.startsWith('sk-ant-')) { | |
| res.status(401).json({ error: { type: 'authentication_error', message: 'Invalid API key β use any sk-ant-* key' } }); | |
| return false; | |
| } | |
| return true; | |
| } | |
| app.get('/v1/models', (req, res) => { | |
| if (!requireAnthroKey(req, res)) return; | |
| const models = Object.entries(ANTHRO_MODEL_MAP).map(([id]) => ({ | |
| type: 'model', | |
| id, | |
| display_name: id, | |
| created_at: new Date().toISOString(), | |
| })); | |
| res.json({ data: models }); | |
| }); | |
| app.post('/v1/messages', (req, res) => handleMessages(req, res)); | |
| // Model override via path prefix: /{model}/v1/messages | |
| app.post('/:model/v1/messages', (req, res) => handleMessages(req, res, req.params.model)); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DEEPSEEK | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function dsGuard(res) { | |
| const ds = getDeepSeekClient(); | |
| if (!ds) { res.status(503).json(http.err("DeepSeek not configured")); return null; } | |
| return ds; | |
| } | |
| app.get("/api/deepseek/health", (_req, res) => { | |
| const ds = getDeepSeekClient(); | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: !!ds, | |
| token_preview: ds ? ds.token.slice(0, 8) + '...' : null, | |
| })); | |
| }); | |
| // ββ Chat ββ | |
| app.post("/api/deepseek/chat", async (req, res) => { | |
| const { prompt, sessionId, thinkingEnabled, searchEnabled, delete: deleteAfter, tokens } = req.body ?? {}; | |
| if (!prompt) { res.status(400).json(http.err("provide prompt in body")); return; } | |
| const ds = dsGuard(res); if (!ds) return; | |
| const isTokens = !!tokens; | |
| if (isTokens) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| let session = sessionId ? { id: sessionId } : await ds.createSession(); | |
| let fullContent = ''; | |
| await ds.sendMessage(session.id, prompt, { | |
| thinkingEnabled: thinkingEnabled || false, | |
| searchEnabled: searchEnabled || false, | |
| onDelta: (delta) => { | |
| const text = DeepSeek.extractContent(delta); | |
| if (!text) return; | |
| fullContent += text; | |
| if (isTokens) res.write(`data: ${JSON.stringify({ token: text })}\n\n`); | |
| }, | |
| }); | |
| if (deleteAfter) { | |
| await ds.deleteSession(session.id).catch(e => logger.warn(`deepseek delete-after-chat: ${e.message}`)); | |
| } | |
| if (isTokens) { | |
| res.write(`data: ${JSON.stringify({ done: true, sessionId: deleteAfter ? undefined : session.id })}\n\n`); | |
| res.end(); | |
| } else { | |
| res.json(http.ok({ response: fullContent, ...(deleteAfter ? {} : { sessionId: session.id }) })); | |
| } | |
| } catch (e) { | |
| logger.error(`deepseek chat: ${e.message}`); | |
| if (isTokens) { res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); res.end(); } | |
| else { res.status(500).json(http.err(e.message)); } | |
| } | |
| }); | |
| app.get("/api/deepseek/chat", async (req, res) => { | |
| const { prompt, sessionId, thinkingEnabled, searchEnabled, delete: deleteAfter, tokens } = req.query; | |
| if (!prompt) { res.status(400).json(http.err("provide ?prompt=")); return; } | |
| const ds = dsGuard(res); if (!ds) return; | |
| const isTokens = tokens === 'true'; | |
| if (isTokens) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| let session = sessionId ? { id: sessionId } : await ds.createSession(); | |
| let fullContent = ''; | |
| await ds.sendMessage(session.id, prompt, { | |
| thinkingEnabled: thinkingEnabled === 'true', | |
| searchEnabled: searchEnabled === 'true', | |
| onDelta: (delta) => { | |
| const text = DeepSeek.extractContent(delta); | |
| if (!text) return; | |
| fullContent += text; | |
| if (isTokens) res.write(`data: ${JSON.stringify({ token: text })}\n\n`); | |
| }, | |
| }); | |
| if (deleteAfter === 'true') { | |
| await ds.deleteSession(session.id).catch(e => logger.warn(`deepseek delete-after-chat: ${e.message}`)); | |
| } | |
| if (isTokens) { | |
| res.write(`data: ${JSON.stringify({ done: true, sessionId: deleteAfter === 'true' ? undefined : session.id })}\n\n`); | |
| res.end(); | |
| } else { | |
| res.json(http.ok({ response: fullContent, ...(deleteAfter === 'true' ? {} : { sessionId: session.id }) })); | |
| } | |
| } catch (e) { | |
| logger.error(`deepseek chat: ${e.message}`); | |
| if (isTokens) { res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); res.end(); } | |
| else { res.status(500).json(http.err(e.message)); } | |
| } | |
| }); | |
| // ββ Sessions ββ | |
| app.get("/api/deepseek/sessions", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| const limit = parseInt(req.query.limit) || 50; | |
| res.json(http.ok(await ds.listSessions(limit))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/sessions", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| const session = await ds.createSession(); | |
| res.json(http.ok(session)); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/deepseek/sessions", async (_req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| const ok = await ds.deleteAllSessions(); | |
| res.json(http.ok({ deleted: ok })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/deepseek/sessions/:id", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| res.json(http.ok({ deleted: await ds.deleteSession(req.params.id) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/deepseek/sessions/:id/messages", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| res.json(http.ok(await ds.getHistoryMessages(req.params.id))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ Session actions (message ops) ββ | |
| app.post("/api/deepseek/sessions/:id/chat", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| const { prompt, thinkingEnabled, searchEnabled } = req.body ?? {}; | |
| if (!prompt) { res.status(400).json(http.err("provide prompt")); return; } | |
| try { | |
| let fullContent = ''; | |
| await ds.sendMessage(req.params.id, prompt, { | |
| thinkingEnabled: thinkingEnabled || false, | |
| searchEnabled: searchEnabled || false, | |
| onDelta: (d) => { fullContent += DeepSeek.extractContent(d); }, | |
| }); | |
| res.json(http.ok({ response: fullContent })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/sessions/:id/regenerate/:messageId", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| let fullContent = ''; | |
| await ds.regenerate(req.params.id, req.params.messageId, { | |
| onDelta: (d) => { fullContent += DeepSeek.extractContent(d); }, | |
| }); | |
| res.json(http.ok({ response: fullContent })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/sessions/:id/edit/:messageId", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| const { prompt, thinkingEnabled, searchEnabled } = req.body ?? {}; | |
| if (!prompt) { res.status(400).json(http.err("provide prompt")); return; } | |
| try { | |
| let fullContent = ''; | |
| await ds.editMessage(req.params.id, req.params.messageId, prompt, { | |
| thinkingEnabled: thinkingEnabled || false, | |
| searchEnabled: searchEnabled || false, | |
| onDelta: (d) => { fullContent += DeepSeek.extractContent(d); }, | |
| }); | |
| res.json(http.ok({ response: fullContent })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/sessions/:id/continue/:messageId", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| let fullContent = ''; | |
| await ds.continueChat(req.params.id, req.params.messageId, { | |
| onDelta: (d) => { fullContent += DeepSeek.extractContent(d); }, | |
| }); | |
| res.json(http.ok({ response: fullContent })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/sessions/:id/stop/:messageId", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| res.json(http.ok({ stopped: await ds.stopStream(req.params.id, req.params.messageId) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.put("/api/deepseek/sessions/:id/title", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| const { title } = req.body ?? {}; | |
| if (!title) { res.status(400).json(http.err("provide title")); return; } | |
| try { | |
| res.json(http.ok({ updated: await ds.updateSessionTitle(req.params.id, title) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.put("/api/deepseek/sessions/:id/pinned", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| const { pinned } = req.body ?? {}; | |
| try { | |
| res.json(http.ok({ updated: await ds.updateSessionPinned(req.params.id, !!pinned) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ User ββ | |
| app.get("/api/deepseek/user", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { res.json(http.ok(await ds.getUser())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ PoW ββ | |
| app.get("/api/deepseek/pow-challenge", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| const target = req.query.target || '/api/v0/chat/completion'; | |
| res.json(http.ok(await ds.createPowChallenge(target))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/pow-solve", async (req, res) => { | |
| try { | |
| const answer = await DeepSeek.solvePowChallenge(req.body); | |
| res.json(http.ok({ answer })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ Shares ββ | |
| app.get("/api/deepseek/shares", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { res.json(http.ok(await ds.listShares())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/shares", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| const { sessionId, messageIds } = req.body ?? {}; | |
| if (!sessionId || !messageIds) { res.status(400).json(http.err("provide sessionId and messageIds")); return; } | |
| try { | |
| res.json(http.ok({ shareId: await ds.createShare(sessionId, messageIds) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/deepseek/shares/fork", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { | |
| res.json(http.ok({ sessionId: await ds.forkShare(req.body.shareId) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/deepseek/shares/:id", async (req, res) => { | |
| const ds = dsGuard(res); if (!ds) return; | |
| try { res.json(http.ok({ deleted: await ds.deleteShare(req.params.id) })); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // QWEN | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function qwenGuard(res) { | |
| const token = getQwenToken(); | |
| if (!token) { res.status(503).json(http.err("Qwen not configured β missing token")); return null; } | |
| return token; | |
| } | |
| app.get("/api/qwen/health", (_req, res) => { | |
| const token = getQwenToken(); | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: !!token, | |
| models: QWEN_MODELS, | |
| features: Object.keys(QWEN_FEATURES), | |
| })); | |
| }); | |
| app.get("/api/qwen", (_req, res) => { | |
| res.json(http.ok({ | |
| name: 'Qwen API', | |
| version: '1.0', | |
| modes: { | |
| normal: 'qwen3.7-max (default)', | |
| thinking: 'qwen3.7-max with reasoning', | |
| fast: 'qwen3.5-flash (fast)' | |
| }, | |
| features: Object.keys(QWEN_FEATURES), | |
| endpoints: { | |
| 'GET /api/qwen/ask': '?prompt=&mode=&chat_type=&image_url=&model=', | |
| 'POST /api/qwen/ask': 'body: { prompt, mode, chat_type, image_url, model, file_path, file_name }', | |
| 'POST /api/qwen/upload': 'body: { content(base64), filename }', | |
| 'GET /api/qwen/conversations': 'list conversations', | |
| 'DELETE /api/qwen/conversations': 'delete all', | |
| 'DELETE /api/qwen/conversations/:id': 'delete one', | |
| } | |
| })); | |
| }); | |
| app.get("/api/qwen/ask", async (req, res) => { | |
| const { prompt, mode, image_url, chat_type, model: modelOverride, delete: deleteAfter, tokens } = req.query; | |
| const text = prompt || 'Hello'; | |
| const m = mode || 'normal'; | |
| const token = qwenGuard(res); if (!token) return; | |
| const isTokens = tokens === 'true'; | |
| const messages = image_url | |
| ? [{ role: 'user', content: [{ type: 'text', text }, { type: 'image_url', image_url: { url: image_url } }] }] | |
| : [{ role: 'user', content: text }]; | |
| if (chat_type && chat_type in QWEN_FEATURES) { | |
| const result = await askFeature(token, messages, chat_type, m); | |
| if (deleteAfter === 'true') { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| return res.json({ prompt: text, mode: m, chat_type, result }); | |
| } | |
| const model = modelOverride || QWEN_MODELS[m] || QWEN_MODELS.normal; | |
| if (isTokens) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| let fullContent = ''; | |
| await askQwenStream(token, messages, model, m, undefined, | |
| (tok) => { fullContent += tok; res.write(`data: ${JSON.stringify({ token: tok })}\n\n`); }, | |
| () => { | |
| if (deleteAfter === 'true') { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| res.write(`data: ${JSON.stringify({ done: true })}\n\n`); | |
| res.end(); | |
| }, | |
| (err) => { res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); res.end(); } | |
| ); | |
| return; | |
| } | |
| const result = await askQwen(token, messages, model, m); | |
| if (deleteAfter === 'true') { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| res.json({ prompt: text, mode: m, model, result }); | |
| }); | |
| app.post("/api/qwen/ask", async (req, res) => { | |
| const { prompt, mode, image_url, chat_type, model: modelOverride, file_path, file_name, delete: deleteAfter, tokens } = req.body ?? {}; | |
| const text = prompt || 'Hello'; | |
| const m = mode || 'normal'; | |
| const token = qwenGuard(res); if (!token) return; | |
| const isTokens = !!tokens; | |
| let messages; | |
| if (file_path) { | |
| messages = await buildMessages(text, file_path, '', file_name); | |
| } else if (image_url) { | |
| messages = [{ role: 'user', content: [{ type: 'text', text }, { type: 'image_url', image_url: { url: image_url } }] }]; | |
| } else { | |
| messages = [{ role: 'user', content: text }]; | |
| } | |
| if (chat_type && chat_type in QWEN_FEATURES) { | |
| const result = await askFeature(token, messages, chat_type, m); | |
| if (deleteAfter) { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| return res.json({ prompt: text, mode: m, chat_type, result }); | |
| } | |
| const model = modelOverride || QWEN_MODELS[m] || QWEN_MODELS.normal; | |
| if (isTokens) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| let fullContent = ''; | |
| await askQwenStream(token, messages, model, m, undefined, | |
| (tok) => { fullContent += tok; res.write(`data: ${JSON.stringify({ token: tok })}\n\n`); }, | |
| () => { | |
| if (deleteAfter) { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| res.write(`data: ${JSON.stringify({ done: true })}\n\n`); | |
| res.end(); | |
| }, | |
| (err) => { res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); res.end(); } | |
| ); | |
| return; | |
| } | |
| const result = await askQwen(token, messages, model, m); | |
| if (deleteAfter) { | |
| deleteAllConversations(token).catch(e => logger.warn(`qwen delete-after-chat: ${e.message}`)); | |
| } | |
| res.json({ prompt: text, mode: m, model, result }); | |
| }); | |
| app.get("/api/qwen/conversations", async (req, res) => { | |
| const token = qwenGuard(res); if (!token) return; | |
| try { res.json(http.ok({ conversations: await listConversations(token) })); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/qwen/conversations", async (_req, res) => { | |
| const token = qwenGuard(res); if (!token) return; | |
| try { res.json(http.ok(await deleteAllConversations(token))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/qwen/conversations/:id", async (req, res) => { | |
| const token = qwenGuard(res); if (!token) return; | |
| try { res.json(http.ok({ deleted: await deleteConversation(token, req.params.id) })); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/qwen/upload", async (req, res) => { | |
| const { content, filename } = req.body ?? {}; | |
| if (!content || !filename) { | |
| res.status(400).json(http.err("provide content (base64) and filename in body")); | |
| return; | |
| } | |
| try { | |
| const buf = Buffer.from(content, 'base64'); | |
| const filePath = path.join(UPLOADS_DIR, `${Date.now()}-${filename}`); | |
| writeFileSync(filePath, buf); | |
| res.json(http.ok({ success: true, filename: path.basename(filePath), originalname: filename, size: buf.length })); | |
| } catch (e) { | |
| res.status(500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.get("/api/qwen/uploads/:file", (req, res) => { | |
| const filePath = path.join(UPLOADS_DIR, req.params.file); | |
| if (!existsSync(filePath)) { res.status(404).json(http.err("file not found")); return; } | |
| const data = readFileSync(filePath); | |
| res.setHeader('Content-Type', 'application/octet-stream'); | |
| res.end(data); | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // CLAUDE (DeepAI) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/claude/health", async (_req, res) => { | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: true, | |
| models: [{ id: 'claude-opus-4.6', name: 'Claude Opus 4.6' }], | |
| })); | |
| }); | |
| app.get("/api/claude/models", async (_req, res) => { | |
| res.json(http.ok({ models: [{ id: 'claude-opus-4.6', name: 'Claude Opus 4.6' }] })); | |
| }); | |
| app.post("/api/claude/chat", async (req, res) => { | |
| const { messages, model, stream, max_tokens } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json(http.err("provide messages array in body")); | |
| return; | |
| } | |
| const isStream = !!stream; | |
| if (isStream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| if (isStream) { | |
| let fullContent = ''; | |
| await claudeChat(messages, { | |
| model: model || 'claude-opus-4.6', | |
| stream: true, | |
| maxTokens: max_tokens, | |
| onDelta: (delta) => { | |
| fullContent += delta; | |
| res.write(`data: ${JSON.stringify({ token: delta })}\n\n`); | |
| }, | |
| }); | |
| res.write(`data: ${JSON.stringify({ done: true })}\n\n`); | |
| res.end(); | |
| } else { | |
| const result = await claudeChat(messages, { | |
| model: model || 'claude-opus-4.6', | |
| maxTokens: max_tokens, | |
| }); | |
| res.json(http.ok({ response: result.text, model: result.model, usage: result.usage })); | |
| } | |
| } catch (e) { | |
| logger.error(`claude chat: ${e.message}`); | |
| if (isStream) { | |
| res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); | |
| res.end(); | |
| } else { | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| } | |
| }); | |
| app.get("/api/claude/chat", async (req, res) => { | |
| const { ask, model } = req.query; | |
| const prompt = ask || ''; | |
| if (!prompt) { res.status(400).json(http.err("provide ?ask=<message>")); return; } | |
| try { | |
| const result = await claudeChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: model || 'claude-opus-4.6' } | |
| ); | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`claude chat: ${e.message}`); | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GPT (DeepAI) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/gpt/health", async (_req, res) => { | |
| const models = await listGptModels().catch(() => Object.keys(GPT_MODELS)); | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: true, | |
| models: models.map(m => ({ id: m.id || m, name: m.name || m })), | |
| })); | |
| }); | |
| app.get("/api/gpt/models", async (_req, res) => { | |
| const models = await listGptModels().catch(() => []); | |
| res.json(http.ok({ models })); | |
| }); | |
| app.post("/api/gpt/chat", async (req, res) => { | |
| const { messages, model, stream, max_tokens } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json(http.err("provide messages array in body")); | |
| return; | |
| } | |
| const isStream = !!stream; | |
| if (isStream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| if (isStream) { | |
| let fullContent = ''; | |
| await gptChat(messages, { | |
| model: model || 'gpt-4.1-nano', | |
| stream: true, | |
| maxTokens: max_tokens, | |
| onDelta: (delta) => { | |
| fullContent += delta; | |
| res.write(`data: ${JSON.stringify({ token: delta })}\n\n`); | |
| }, | |
| }); | |
| res.write(`data: ${JSON.stringify({ done: true })}\n\n`); | |
| res.end(); | |
| } else { | |
| const result = await gptChat(messages, { | |
| model: model || 'gpt-4.1-nano', | |
| maxTokens: max_tokens, | |
| }); | |
| res.json(http.ok({ response: result.text, model: result.model, usage: result.usage })); | |
| } | |
| } catch (e) { | |
| logger.error(`gpt chat: ${e.message}`); | |
| if (isStream) { | |
| res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); | |
| res.end(); | |
| } else { | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| } | |
| }); | |
| app.get("/api/gpt/chat", async (req, res) => { | |
| const { ask, model } = req.query; | |
| const prompt = ask || ''; | |
| if (!prompt) { res.status(400).json(http.err("provide ?ask=<message>")); return; } | |
| try { | |
| const result = await gptChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: model || 'gpt-4.1-nano' } | |
| ); | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`gpt chat: ${e.message}`); | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // GROK (DeepAI) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/grok/health", async (_req, res) => { | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: true, | |
| models: [{ id: 'grok-4', name: 'Grok 4' }], | |
| })); | |
| }); | |
| app.get("/api/grok/models", async (_req, res) => { | |
| res.json(http.ok({ models: [{ id: 'grok-4', name: 'Grok 4' }] })); | |
| }); | |
| app.post("/api/grok/chat", async (req, res) => { | |
| const { messages, model, stream, max_tokens } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json(http.err("provide messages array in body")); | |
| return; | |
| } | |
| const isStream = !!stream; | |
| if (isStream) { | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| res.flushHeaders(); | |
| } | |
| try { | |
| if (isStream) { | |
| let fullContent = ''; | |
| await grokChat(messages, { | |
| model: model || 'grok-4', | |
| stream: true, | |
| maxTokens: max_tokens, | |
| onDelta: (delta) => { | |
| fullContent += delta; | |
| res.write(`data: ${JSON.stringify({ token: delta })}\n\n`); | |
| }, | |
| }); | |
| res.write(`data: ${JSON.stringify({ done: true })}\n\n`); | |
| res.end(); | |
| } else { | |
| const result = await grokChat(messages, { | |
| model: model || 'grok-4', | |
| maxTokens: max_tokens, | |
| }); | |
| res.json(http.ok({ response: result.text, model: result.model, usage: result.usage })); | |
| } | |
| } catch (e) { | |
| logger.error(`grok chat: ${e.message}`); | |
| if (isStream) { | |
| res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`); | |
| res.end(); | |
| } else { | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| } | |
| }); | |
| app.get("/api/grok/chat", async (req, res) => { | |
| const { ask, model } = req.query; | |
| const prompt = ask || ''; | |
| if (!prompt) { res.status(400).json(http.err("provide ?ask=<message>")); return; } | |
| try { | |
| const result = await grokChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: model || 'grok-4' } | |
| ); | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`grok chat: ${e.message}`); | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // META AI | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function metaGuard(res) { | |
| const cfg = loadMetaConfig(); | |
| if (!cfg.configured) { res.status(503).json(http.err("Meta AI not configured β create data/meta.json with cookies (ecto_1_sess)")); return null; } | |
| return cfg; | |
| } | |
| app.get("/api/meta/health", async (_req, res) => { | |
| const cfg = loadMetaConfig(); | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: cfg.configured, | |
| hasAccessToken: cfg.hasAccessToken, | |
| cookieKeys: cfg.cookieKeys, | |
| })); | |
| }); | |
| app.get("/api/meta/models", async (_req, res) => { | |
| const models = await metaListModels().catch(() => []); | |
| res.json(http.ok({ models })); | |
| }); | |
| app.get("/api/meta/chat", async (req, res) => { | |
| const { ask, model, delete: deleteAfter } = req.query; | |
| const prompt = ask || ''; | |
| if (!prompt) { res.status(400).json(http.err("provide ?ask=<message>")); return; } | |
| try { | |
| const result = await metaChat( | |
| [{ role: 'user', content: prompt }], | |
| {} | |
| ); | |
| if (deleteAfter === 'true' && result.metadata?.conversationId) { | |
| await metaDeleteConversation(result.metadata.conversationId).catch(e => logger.warn(`meta delete-after-chat: ${e.message}`)); | |
| } | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`meta chat: ${e.message}`); | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.post("/api/meta/chat", async (req, res) => { | |
| const { messages, model, delete: deleteAfter } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json(http.err("provide messages array in body")); | |
| return; | |
| } | |
| try { | |
| const result = await metaChat(messages, { model }); | |
| if (deleteAfter && result.metadata?.conversationId) { | |
| await metaDeleteConversation(result.metadata.conversationId).catch(e => logger.warn(`meta delete-after-chat: ${e.message}`)); | |
| } | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`meta chat: ${e.message}`); | |
| res.status(e.code === 'auth_error' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.get("/api/meta/image", async (req, res) => { | |
| const { prompt, orientation } = req.query; | |
| if (!prompt) { res.status(400).json(http.err("provide ?prompt=")); return; } | |
| try { | |
| const result = await metaImageGen(prompt, { orientation }); | |
| res.json(http.ok(result)); | |
| } catch (e) { | |
| res.status(500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.get("/api/meta/video", async (req, res) => { | |
| const { prompt } = req.query; | |
| if (!prompt) { res.status(400).json(http.err("provide ?prompt=")); return; } | |
| try { | |
| const result = await metaVideoGen(prompt); | |
| res.json(http.ok(result)); | |
| } catch (e) { | |
| res.status(500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.delete("/api/meta/conversations/:id", async (req, res) => { | |
| try { | |
| const result = await metaDeleteConversation(req.params.id); | |
| res.json(http.ok(result)); | |
| } catch (e) { | |
| res.status(500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // DEEPAI (unified) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app.get("/api/deepai/health", (_req, res) => { | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: true, | |
| free_models: getFreeModels(), | |
| all_models: listDeepAIModels(), | |
| })); | |
| }); | |
| app.get("/api/deepai/models", (_req, res) => { | |
| res.json(http.ok({ models: listDeepAIModels() })); | |
| }); | |
| app.post("/api/deepai/chat", async (req, res) => { | |
| const { messages, model } = req.body ?? {}; | |
| if (!messages?.length) { | |
| res.status(400).json(http.err("provide messages array in body")); | |
| return; | |
| } | |
| try { | |
| const result = await deepaiChat(messages, { model: model || 'standard' }); | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`deepai chat: ${e.message}`); | |
| res.status(e.code === 'model_locked' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| app.get("/api/deepai/chat", async (req, res) => { | |
| const { ask, model } = req.query; | |
| const prompt = ask || ''; | |
| if (!prompt) { res.status(400).json(http.err("provide ?ask=<message>")); return; } | |
| try { | |
| const result = await deepaiChat( | |
| [{ role: 'user', content: prompt }], | |
| { model: model || 'standard' } | |
| ); | |
| res.json(http.ok({ response: result.text, model: result.model })); | |
| } catch (e) { | |
| logger.error(`deepai chat: ${e.message}`); | |
| res.status(e.code === 'model_locked' ? 401 : 500).json(http.err(e.message)); | |
| } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // KIMI | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _kimi = null; | |
| function getKimiClient() { | |
| if (_kimi) return _kimi; | |
| const configPath = path.join(__dirname, 'data/kimi.json'); | |
| const cfg = loadKimiConfig(configPath); | |
| if (cfg.token) { | |
| _kimi = new Kimi(cfg.cookies, { deviceId: cfg.token ? undefined : undefined }); | |
| logger.info('kimi: client initialized'); | |
| } | |
| return _kimi; | |
| } | |
| function kimiGuard(res) { | |
| const k = getKimiClient(); | |
| if (!k) { res.status(503).json(http.err("Kimi not configured β missing data/kimi.json with kimi-auth cookie")); return null; } | |
| return k; | |
| } | |
| app.get("/api/kimi/health", (_req, res) => { | |
| const k = getKimiClient(); | |
| res.json(http.ok({ | |
| status: "ok", | |
| configured: !!k, | |
| token_preview: k ? k.token.slice(0, 12) + '...' : null, | |
| })); | |
| }); | |
| app.get("/api/kimi/user", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.getUser())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/models", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.listModels())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/kimiplus", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.listKimiPlus())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/kimiplus/:id", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.getKimiPlus(req.params.id))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ Sessions ββ | |
| app.get("/api/kimi/sessions", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { | |
| const limit = parseInt(req.query.limit) || 50; | |
| res.json(http.ok(await k.listSessions(limit))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/kimi/sessions", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { name } = req.body ?? {}; | |
| try { res.json(http.ok(await k.createSession(name || 'New Chat'))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/sessions/:id", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.getSession(req.params.id))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/kimi/sessions/:id", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok({ deleted: await k.deleteSession(req.params.id) })); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.delete("/api/kimi/sessions", async (_req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.deleteAllSessions())); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/sessions/:id/messages", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { | |
| const pageSize = parseInt(req.query.pageSize) || 50; | |
| res.json(http.ok(await k.listMessages(req.params.id, pageSize))); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // ββ Chat ββ | |
| app.post("/api/kimi/chat", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { prompt, sessionId, thinking, delete: deleteAfter } = req.body ?? {}; | |
| if (!prompt) { res.status(400).json(http.err("provide prompt in body")); return; } | |
| try { | |
| const frames = await k.sendMessage(sessionId || '', prompt, { thinking: !!thinking }); | |
| const text = Kimi.extractChatText(frames); | |
| const chatId = Kimi.extractChatId(frames); | |
| if (deleteAfter && chatId) { | |
| await k.deleteSession(chatId).catch(e => logger.warn(`kimi delete-after-chat: ${e.message}`)); | |
| } | |
| res.json(http.ok({ response: text, ...(deleteAfter ? {} : { sessionId: chatId || sessionId || '' }) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.get("/api/kimi/chat", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { prompt, sessionId, thinking, delete: deleteAfter } = req.query; | |
| if (!prompt) { res.status(400).json(http.err("provide ?prompt=")); return; } | |
| try { | |
| const frames = await k.sendMessage(sessionId || '', prompt, { thinking: thinking === 'true' }); | |
| const text = Kimi.extractChatText(frames); | |
| const chatId = Kimi.extractChatId(frames); | |
| if (deleteAfter === 'true' && chatId) { | |
| await k.deleteSession(chatId).catch(e => logger.warn(`kimi delete-after-chat: ${e.message}`)); | |
| } | |
| res.json(http.ok({ response: text, ...(deleteAfter === 'true' ? {} : { sessionId: chatId || sessionId || '' }) })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/kimi/chat/:id", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { prompt, thinking } = req.body ?? {}; | |
| if (!prompt) { res.status(400).json(http.err("provide prompt")); return; } | |
| try { | |
| const frames = await k.sendMessage(req.params.id, prompt, { thinking: !!thinking }); | |
| const text = Kimi.extractChatText(frames); | |
| res.json(http.ok({ response: text })); | |
| } catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/kimi/cancel/:chatId/:messageId", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| try { res.json(http.ok(await k.cancelChat(req.params.chatId, req.params.messageId))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.put("/api/kimi/sessions/:id", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { chat } = req.body ?? {}; | |
| if (!chat) { res.status(400).json(http.err("provide chat object in body")); return; } | |
| try { res.json(http.ok(await k.updateSession(chat))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| app.post("/api/kimi/vote/:chatId/:messageId", async (req, res) => { | |
| const k = kimiGuard(res); if (!k) return; | |
| const { voteType } = req.body ?? {}; | |
| if (!voteType) { res.status(400).json(http.err("provide voteType in body")); return; } | |
| try { res.json(http.ok(await k.voteMessage(req.params.chatId, req.params.messageId, voteType))); } | |
| catch (e) { res.status(500).json(http.err(e.message)); } | |
| }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // STARTUP | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| await new Promise((resolve) => app.listen(PORT, resolve)); | |
| console.log("saksis poli", PORT) | |
| try { writeFileSync(LOG_V1, ''); } catch {} | |
| loadIntoCookieJar(); | |
| logger.info(`gemini-web on :${PORT}`); | |
| logger.info(`providers: ${Object.keys(modules).join(", ")}, deepseek, qwen, kimi, claude, gpt, grok, deepai, meta`); | |
| if (getDeepSeekClient()) { | |
| logger.info("deepseek: configured and ready"); | |
| } else { | |
| logger.warn("deepseek: no token β create data/deepseek.json with userToken/hifLeim"); | |
| } | |
| if (getQwenToken()) { | |
| logger.info("qwen: configured and ready"); | |
| } else { | |
| logger.warn("qwen: no token β add qwen cookies with 'token' entry to data/qwen.json"); | |
| } | |
| if (getKimiClient()) { | |
| logger.info("kimi: configured and ready"); | |
| } else { | |
| logger.warn("kimi: no token β add data/kimi.json with kimi-auth cookie"); | |
| } | |
| logger.info("claude: ready (DeepAI β free, no token needed)"); | |
| logger.info("gpt: ready (DeepAI β free, models: gpt-4.1-nano, gpt-oss-120b, gpt-5-nano)"); | |
| logger.info("grok: ready (DeepAI β free, model: gpt-5-nano fallback β grok-4 locked)"); | |
| logger.info("deepai: ready with 14 free models"); | |
| logger.info(""); | |
| logger.info("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"); | |
| logger.info("β Claude Code connection: β"); | |
| logger.info("β ANTHROPIC_BASE_URL=http://<ip>:" + PORT + " β"); | |
| logger.info("β ANTHROPIC_API_KEY=sk-ant-deepai-fake-key β"); | |
| logger.info("β β"); | |
| logger.info("β Any sk-ant-* key works β backed by DeepAI free β"); | |
| logger.info("β model claude-opus-4.6 (scraped, not official API) β"); | |
| logger.info("ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"); | |
| // Auto-delete Qwen conversations every 60s | |
| let _qwenInterval = null; | |
| if (getQwenToken()) { | |
| _qwenInterval = setInterval(async () => { | |
| try { | |
| const r = await deleteAllConversations(getQwenToken()); | |
| if (r.deleted > 0) logger.info(`qwen: auto-deleted ${r.deleted} conversations`); | |
| } catch {} | |
| }, 60000); | |
| } | |
| try { | |
| await getClient(); | |
| logger.info("client: warm-up done"); | |
| } catch (e) { | |
| logger.warn( | |
| `client: warm-up failed β POST /api/gemini/cookies to fix. (${e.message})`, | |
| ); | |
| } | |