const { Bot } = require("grammy"); const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; const CHAT_ID = process.env.TELEGRAM_CHAT_ID; const ZEN_API_KEY = process.env.OPENCODE_ZEN_KEY; const ROUTER_URL = process.env.ROUTER_URL || "http://127.0.0.1:7861"; if (!BOT_TOKEN) { console.error("[ERROR] TELEGRAM_BOT_TOKEN not set"); process.exit(1); } if (!ZEN_API_KEY) { console.error("[ERROR] OPENCODE_ZEN_KEY not set"); process.exit(1); } console.log("[CONFIG] CHAT_ID filter:", CHAT_ID || "ALL"); console.log("[CONFIG] Zen Key:", ZEN_API_KEY.substring(0, 12) + "..."); console.log("[CONFIG] Router:", ROUTER_URL); const ZEN_BASE = "https://opencode.ai/zen/v1"; const LOCAL_MODEL = "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF"; const LOCAL_API = "https://abbasyk60-ai-telegram-bot.hf.space/v1/chat/completions"; const FREE_MODELS = ["deepseek-v4-flash-free", "mimo-v2.5-free", "big-pickle", "laguna-s-2.1-free", "ling-3.0-flash-free", "north-mini-code-free", "nemotron-3-ultra-free"]; const bot = new Bot(BOT_TOKEN); const userSessions = new Map(); async function callAPI(messages, modelId, url, headers, bodyExtra = {}) { const body = { model: modelId, messages, max_tokens: 2048, ...bodyExtra }; const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) }); if (!response.ok) { const err = await response.text(); throw new Error(`API ${response.status}: ${err.substring(0, 200)}`); } const data = await response.json(); if (data.choices?.[0]?.message?.content?.trim()) return data.choices[0].message.content; if (data.choices?.[0]?.message?.reasoning_content?.trim()) return data.choices[0].message.reasoning_content; throw new Error("Empty response"); } async function callWithFallback(messages, preferredModel) { // If user wants local model, try it first if (preferredModel === LOCAL_MODEL || preferredModel === "deepseek-r1-7b") { try { const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" }); console.log("[OK] Local model responded"); return response; } catch (e) { console.log("[SKIP] Local failed:", e.message.substring(0, 80)); } } // Try preferred model on Zen const modelsToTry = preferredModel === LOCAL_MODEL ? FREE_MODELS : [preferredModel, ...FREE_MODELS.filter((m) => m !== preferredModel)]; for (const model of modelsToTry) { // Try Zen API try { const response = await callAPI(messages, model, `${ZEN_BASE}/chat/completions`, { Authorization: `Bearer ${ZEN_API_KEY}`, "Content-Type": "application/json", }); console.log("[OK] Zen responded with:", model); return response; } catch (e) { console.log("[SKIP] Zen failed:", model, e.message.substring(0, 80)); } // Try local model as fallback try { const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" }); console.log("[OK] Local model responded (fallback)"); return response; } catch (e) { console.log("[SKIP] Local fallback failed:", e.message.substring(0, 80)); } } // Last resort: try local model alone try { const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" }); console.log("[OK] Local model responded (last resort)"); return response; } catch (e) { console.log("[SKIP] Local last resort failed:", e.message.substring(0, 80)); } throw new Error("All models failed. Try again in a minute."); } bot.command("start", async (ctx) => { const chatId = ctx.chat.id; if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return; userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] }); await ctx.reply( "Welcome to OpenCode AI Agent!\n\n" + "Commands:\n" + "/model - Switch model\n" + "/models - List available models\n" + "/new - New conversation\n" + "/help - Show help\n\n" + "Default model: DeepSeek V4 Flash (FREE)\n" + "Local model: DeepSeek R1 7B (free, runs on your server)" ); }); bot.command("help", async (ctx) => { await ctx.reply( "Send any message and I'll respond using AI.\n\n" + "Commands:\n" + "/start - Initialize bot\n" + "/models - List models\n" + "/model - Switch model\n" + "/new - Clear history" ); }); bot.command("models", async (ctx) => { await ctx.reply( "FREE Models (Zen API):\n" + "- deepseek-v4-flash-free\n" + "- mimo-v2.5-free\n" + "- big-pickle\n" + "- laguna-s-2.1-free\n" + "- ling-3.0-flash-free\n" + "- north-mini-code-free\n" + "- nemotron-3-ultra-free\n\n" + "LOCAL Model (Your Server):\n" + "- deepseek-r1-7b (DeepSeek R1 7B)\n\n" + "Paid: gpt-5.5, gpt-5.4, claude-sonnet-5, gemini-3.6-flash, etc.\n\n" + "/model " ); }); bot.command("model", async (ctx) => { const chatId = ctx.chat.id; if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return; const args = ctx.message.text.split(" ").slice(1); if (args.length === 0) { const session = userSessions.get(chatId); await ctx.reply("Current: " + (session?.model || "deepseek-v4-flash-free")); return; } let modelId = args[0].toLowerCase(); if (modelId === "deepseek-r1-7b" || modelId === "local") modelId = LOCAL_MODEL; const session = userSessions.get(chatId) || { history: [] }; userSessions.set(chatId, { model: modelId, history: [] }); await ctx.reply("Switched to: " + modelId); }); bot.command("new", async (ctx) => { const chatId = ctx.chat.id; if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return; const session = userSessions.get(chatId); userSessions.set(chatId, { model: session?.model || "deepseek-v4-flash-free", history: [] }); await ctx.reply("New conversation started!"); }); bot.on("message:text", async (ctx) => { const chatId = ctx.chat.id; const message = ctx.message.text; if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) { console.log("[BLOCKED] Chat", chatId); return; } console.log("[MSG]", chatId, ":", message.substring(0, 50)); await ctx.replyWithChatAction("typing"); if (!userSessions.has(chatId)) { userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] }); } const session = userSessions.get(chatId); const messages = [...session.history, { role: "user", content: message }]; try { const response = await callWithFallback(messages, session.model); session.history.push({ role: "user", content: message }); session.history.push({ role: "assistant", content: response }); if (session.history.length > 20) session.history = session.history.slice(-20); if (response.length > 4000) { const chunks = response.match(/.{1,4000}/gs) || [response]; for (const chunk of chunks) await ctx.reply(chunk); } else { await ctx.reply(response); } console.log("[REPLY]", response.length, "chars"); } catch (error) { console.error("[ERROR]", error.message); await ctx.reply("Error: " + error.message); } }); bot.catch((err) => console.error("[BOT ERROR]", err)); console.log("[OK] Starting Telegram bot..."); bot.start({ onStart: () => { console.log("[OK] Telegram bot running!"); console.log("[OK] Chat ID:", CHAT_ID || "none"); }, drop_pending_updates: true, });