Exocore-gateway / server.js
Johnsteve Costanos
Add image upload support for Dola provider
8d47d01
Raw
History Blame Contribute Delete
82.7 kB
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})`,
);
}