abbasyk60 commited on
Commit
ec55fef
·
1 Parent(s): e1a5ef1

Add DeepSeek R1 7B local model

Browse files
Files changed (2) hide show
  1. bot.js +107 -158
  2. index.html +25 -8
bot.js CHANGED
@@ -1,251 +1,200 @@
1
- const { Bot, GrammyError, HttpError } = require("grammy");
2
 
3
- // Environment variables
4
  const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
5
  const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
6
  const ZEN_API_KEY = process.env.OPENCODE_ZEN_KEY;
 
7
 
8
- if (!BOT_TOKEN) {
9
- console.error("[ERROR] TELEGRAM_BOT_TOKEN not set");
10
- process.exit(1);
11
- }
12
- if (!ZEN_API_KEY) {
13
- console.error("[ERROR] OPENCODE_ZEN_KEY not set");
14
- process.exit(1);
15
- }
16
 
17
- console.log("[CONFIG] CHAT_ID filter:", CHAT_ID || "DISABLED (accepting all chats)");
18
- console.log("[CONFIG] Zen API Key:", ZEN_API_KEY ? ZEN_API_KEY.substring(0, 12) + "..." : "NOT SET");
 
19
 
20
  const ZEN_BASE = "https://opencode.ai/zen/v1";
 
 
 
21
  const bot = new Bot(BOT_TOKEN);
22
-
23
- // User sessions: chatId -> { model, history }
24
  const userSessions = new Map();
25
 
26
- function getEndpoint(modelId) {
27
- const openaiPrefixes = ["gpt-", "deepseek-", "grok-", "minimax-", "glm-", "kimi-", "big-pickle", "mimo-", "laguna-", "ling-", "north-", "nemotron-"];
28
- if (openaiPrefixes.some((p) => modelId.startsWith(p) || modelId.includes(p))) {
29
- return `${ZEN_BASE}/chat/completions`;
30
- }
31
- if (modelId.startsWith("claude-") || modelId.startsWith("qwen3")) {
32
- return `${ZEN_BASE}/messages`;
33
  }
34
- return `${ZEN_BASE}/chat/completions`;
 
 
 
35
  }
36
 
37
- async function callZenAPI(messages, modelId) {
38
- const endpoint = getEndpoint(modelId);
39
- const headers = {
40
- Authorization: `Bearer ${ZEN_API_KEY}`,
41
- "Content-Type": "application/json",
42
- };
43
-
44
- let body;
45
- if (endpoint.includes("/messages")) {
46
- body = { model: modelId, messages, max_tokens: 2048 };
47
- } else {
48
- body = { model: modelId, messages, max_tokens: 2048 };
49
  }
50
 
51
- console.log("[API] Calling:", endpoint, "model:", modelId);
52
-
53
- const response = await fetch(endpoint, {
54
- method: "POST",
55
- headers,
56
- body: JSON.stringify(body),
57
- });
 
 
 
 
 
 
 
 
58
 
59
- if (!response.ok) {
60
- const errText = await response.text();
61
- console.error("[API ERROR]", response.status, errText.substring(0, 500));
62
- throw new Error(`API ${response.status}: ${errText.substring(0, 300)}`);
 
 
 
 
63
  }
64
 
65
- const data = await response.json();
66
- console.log("[API] Response keys:", Object.keys(data));
67
-
68
- // Handle /messages format (Anthropic-style)
69
- if (data.content) {
70
- const result = data.content.map((c) => c.text || "").join("");
71
- if (result.trim()) return result;
72
- }
73
- // Handle /chat/completions format (OpenAI-style)
74
- if (data.choices && data.choices[0]) {
75
- const msg = data.choices[0].message;
76
- if (msg.content && msg.content.trim()) return msg.content;
77
- if (msg.reasoning_content && msg.reasoning_content.trim()) {
78
- return msg.reasoning_content;
79
- }
80
- return "(empty response from model - try another model)";
81
  }
82
- return JSON.stringify(data).substring(0, 2000);
 
83
  }
84
 
85
- // /start
86
  bot.command("start", async (ctx) => {
87
  const chatId = ctx.chat.id;
88
- console.log("[LOG] /start from chat:", chatId, "type:", ctx.chat.type);
89
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
90
-
91
  userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
92
  await ctx.reply(
93
- "🤖 Welcome to OpenCode AI Agent!\n\n" +
94
- "I'm connected to OpenCode Zen with 40+ AI models.\n\n" +
95
- "Commands:\n" +
96
- "/model <name> - Switch model\n" +
97
- "/models - List available models\n" +
98
- "/new - New conversation\n" +
99
- "/help - Show help\n\n" +
100
- "Just send me a message to start chatting!\n" +
101
- "Default model: DeepSeek V4 Flash (FREE)"
102
  );
103
  });
104
 
105
- // /help
106
  bot.command("help", async (ctx) => {
107
  await ctx.reply(
108
- "📖 Help\n\n" +
109
- "Send any message and I'll respond using AI.\n\n" +
110
- "Commands:\n" +
111
- "/start - Initialize bot\n" +
112
- "/models - List all models\n" +
113
- "/model <name> - Switch model\n" +
114
- "/new - Clear history\n" +
115
- "/help - This message\n\n" +
116
- "Free models: deepseek-v4-flash-free, mimo-v2.5-free, big-pickle, laguna-s-2.1-free"
117
  );
118
  });
119
 
120
- // /models
121
  bot.command("models", async (ctx) => {
122
  await ctx.reply(
123
- "📋 Available Models:\n\n" +
124
- "🆓 FREE:\n" +
125
- " deepseek-v4-flash-free\n" +
126
- " mimo-v2.5-free\n" +
127
- " big-pickle\n" +
128
- " laguna-s-2.1-free\n" +
129
- " ling-3.0-flash-free\n" +
130
- " north-mini-code-free\n" +
131
- " nemotron-3-ultra-free\n\n" +
132
- "💰 Paid:\n" +
133
- " gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5-nano\n" +
134
- " claude-sonnet-5, claude-opus-5, claude-haiku-4-5\n" +
135
- "• gemini-3.6-flash, gemini-3.1-pro\n" +
136
- "• deepseek-v4-pro, qwen3.7-max, grok-4.5\n\n" +
137
- "Use: /model <model-id>"
138
  );
139
  });
140
 
141
- // /model
142
  bot.command("model", async (ctx) => {
143
  const chatId = ctx.chat.id;
144
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
145
-
146
  const args = ctx.message.text.split(" ").slice(1);
147
  if (args.length === 0) {
148
  const session = userSessions.get(chatId);
149
- await ctx.reply(`Current model: ${session?.model || "deepseek-v4-flash-free"}`);
150
  return;
151
  }
152
-
153
- const modelId = args[0].toLowerCase();
154
- if (!userSessions.has(chatId)) {
155
- userSessions.set(chatId, { model: modelId, history: [] });
156
- } else {
157
- userSessions.get(chatId).model = modelId;
158
- userSessions.get(chatId).history = [];
159
- }
160
-
161
- await ctx.reply(`✅ Model switched to: ${modelId}\nHistory cleared. Send me a message!`);
162
  });
163
 
164
- // /new
165
  bot.command("new", async (ctx) => {
166
  const chatId = ctx.chat.id;
167
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
168
-
169
  const session = userSessions.get(chatId);
170
- const model = session?.model || "deepseek-v4-flash-free";
171
- userSessions.set(chatId, { model, history: [] });
172
- await ctx.reply("🔄 New conversation started!");
173
  });
174
 
175
- // Handle text messages
176
  bot.on("message:text", async (ctx) => {
177
  const chatId = ctx.chat.id;
178
  const message = ctx.message.text;
179
- const msgTime = ctx.message.date;
180
- const now = Math.floor(Date.now() / 1000);
181
 
182
- // Skip old messages (sent more than 30 seconds ago)
183
- if (now - msgTime > 30) {
184
- console.log("[SKIP] Old message (", now - msgTime, "s ago):", message.substring(0, 30));
185
- return;
186
- }
187
-
188
- console.log("[LOG] Message from chat:", chatId, "type:", ctx.chat.type, "text:", message.substring(0, 50));
189
-
190
- // Check allowed chat
191
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) {
192
- console.log("[BLOCKED] Chat", chatId, "not in allowed list. CHAT_ID:", CHAT_ID);
193
- await ctx.reply("Sorry, I'm only available in the designated group.");
194
  return;
195
  }
196
 
 
197
  await ctx.replyWithChatAction("typing");
198
 
199
- // Get or create session
200
  if (!userSessions.has(chatId)) {
201
  userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
202
  }
203
  const session = userSessions.get(chatId);
204
-
205
- // Build messages
206
  const messages = [...session.history, { role: "user", content: message }];
207
 
208
  try {
209
- const response = await callZenAPI(messages, session.model);
210
 
211
- // Save to history
212
  session.history.push({ role: "user", content: message });
213
  session.history.push({ role: "assistant", content: response });
 
214
 
215
- // Keep history under 20 messages
216
- if (session.history.length > 20) {
217
- session.history = session.history.slice(-20);
218
- }
219
-
220
- // Send (split if too long for Telegram's 4096 char limit)
221
  if (response.length > 4000) {
222
  const chunks = response.match(/.{1,4000}/gs) || [response];
223
- for (const chunk of chunks) {
224
- await ctx.reply(chunk);
225
- console.log("[REPLY] Sent chunk:", chunk.length, "chars");
226
- }
227
  } else {
228
  await ctx.reply(response);
229
- console.log("[REPLY] Sent:", response.length, "chars");
230
  }
 
231
  } catch (error) {
232
- console.error("[ERROR] Chat API failed:", error.message);
233
- await ctx.reply(`❌ Error: ${error.message}`);
234
  }
235
  });
236
 
237
- // Error handler
238
- bot.catch((err) => {
239
- console.error("[BOT ERROR]", err);
240
- });
241
 
242
- // Start
243
  console.log("[OK] Starting Telegram bot...");
244
  bot.start({
245
  onStart: () => {
246
- console.log("[OK] Telegram bot is running!");
247
- console.log("[OK] Chat ID filter:", CHAT_ID || "none (accepting all)");
248
- console.log("[OK] Waiting for NEW messages only...");
249
  },
250
  drop_pending_updates: true,
251
  });
 
1
+ const { Bot } = require("grammy");
2
 
 
3
  const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
4
  const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
5
  const ZEN_API_KEY = process.env.OPENCODE_ZEN_KEY;
6
+ const ROUTER_URL = process.env.ROUTER_URL || "http://127.0.0.1:7861";
7
 
8
+ if (!BOT_TOKEN) { console.error("[ERROR] TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
9
+ if (!ZEN_API_KEY) { console.error("[ERROR] OPENCODE_ZEN_KEY not set"); process.exit(1); }
 
 
 
 
 
 
10
 
11
+ console.log("[CONFIG] CHAT_ID filter:", CHAT_ID || "ALL");
12
+ console.log("[CONFIG] Zen Key:", ZEN_API_KEY.substring(0, 12) + "...");
13
+ console.log("[CONFIG] Router:", ROUTER_URL);
14
 
15
  const ZEN_BASE = "https://opencode.ai/zen/v1";
16
+ const LOCAL_MODEL = "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF";
17
+ const LOCAL_API = "https://abbasyk60-ai-telegram-bot.hf.space/v1/chat/completions";
18
+ 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"];
19
  const bot = new Bot(BOT_TOKEN);
 
 
20
  const userSessions = new Map();
21
 
22
+ async function callAPI(messages, modelId, url, headers, bodyExtra = {}) {
23
+ const body = { model: modelId, messages, max_tokens: 2048, ...bodyExtra };
24
+ const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
25
+ if (!response.ok) {
26
+ const err = await response.text();
27
+ throw new Error(`API ${response.status}: ${err.substring(0, 200)}`);
 
28
  }
29
+ const data = await response.json();
30
+ if (data.choices?.[0]?.message?.content?.trim()) return data.choices[0].message.content;
31
+ if (data.choices?.[0]?.message?.reasoning_content?.trim()) return data.choices[0].message.reasoning_content;
32
+ throw new Error("Empty response");
33
  }
34
 
35
+ async function callWithFallback(messages, preferredModel) {
36
+ // If user wants local model, try it first
37
+ if (preferredModel === LOCAL_MODEL || preferredModel === "deepseek-r1-7b") {
38
+ try {
39
+ const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
40
+ console.log("[OK] Local model responded");
41
+ return response;
42
+ } catch (e) {
43
+ console.log("[SKIP] Local failed:", e.message.substring(0, 80));
44
+ }
 
 
45
  }
46
 
47
+ // Try preferred model on Zen
48
+ const modelsToTry = preferredModel === LOCAL_MODEL ? FREE_MODELS : [preferredModel, ...FREE_MODELS.filter((m) => m !== preferredModel)];
49
+
50
+ for (const model of modelsToTry) {
51
+ // Try Zen API
52
+ try {
53
+ const response = await callAPI(messages, model, `${ZEN_BASE}/chat/completions`, {
54
+ Authorization: `Bearer ${ZEN_API_KEY}`,
55
+ "Content-Type": "application/json",
56
+ });
57
+ console.log("[OK] Zen responded with:", model);
58
+ return response;
59
+ } catch (e) {
60
+ console.log("[SKIP] Zen failed:", model, e.message.substring(0, 80));
61
+ }
62
 
63
+ // Try local model as fallback
64
+ try {
65
+ const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
66
+ console.log("[OK] Local model responded (fallback)");
67
+ return response;
68
+ } catch (e) {
69
+ console.log("[SKIP] Local fallback failed:", e.message.substring(0, 80));
70
+ }
71
  }
72
 
73
+ // Last resort: try local model alone
74
+ try {
75
+ const response = await callAPI(messages, LOCAL_MODEL, LOCAL_API, { "Content-Type": "application/json" });
76
+ console.log("[OK] Local model responded (last resort)");
77
+ return response;
78
+ } catch (e) {
79
+ console.log("[SKIP] Local last resort failed:", e.message.substring(0, 80));
 
 
 
 
 
 
 
 
 
80
  }
81
+
82
+ throw new Error("All models failed. Try again in a minute.");
83
  }
84
 
 
85
  bot.command("start", async (ctx) => {
86
  const chatId = ctx.chat.id;
 
87
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
 
88
  userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
89
  await ctx.reply(
90
+ "Welcome to OpenCode AI Agent!\n\n" +
91
+ "Commands:\n" +
92
+ "/model <name> - Switch model\n" +
93
+ "/models - List available models\n" +
94
+ "/new - New conversation\n" +
95
+ "/help - Show help\n\n" +
96
+ "Default model: DeepSeek V4 Flash (FREE)\n" +
97
+ "Local model: DeepSeek R1 7B (free, runs on your server)"
 
98
  );
99
  });
100
 
 
101
  bot.command("help", async (ctx) => {
102
  await ctx.reply(
103
+ "Send any message and I'll respond using AI.\n\n" +
104
+ "Commands:\n" +
105
+ "/start - Initialize bot\n" +
106
+ "/models - List models\n" +
107
+ "/model <name> - Switch model\n" +
108
+ "/new - Clear history"
 
 
 
109
  );
110
  });
111
 
 
112
  bot.command("models", async (ctx) => {
113
  await ctx.reply(
114
+ "FREE Models (Zen API):\n" +
115
+ "- deepseek-v4-flash-free\n" +
116
+ "- mimo-v2.5-free\n" +
117
+ "- big-pickle\n" +
118
+ "- laguna-s-2.1-free\n" +
119
+ "- ling-3.0-flash-free\n" +
120
+ "- north-mini-code-free\n" +
121
+ "- nemotron-3-ultra-free\n\n" +
122
+ "LOCAL Model (Your Server):\n" +
123
+ "- deepseek-r1-7b (DeepSeek R1 7B)\n\n" +
124
+ "Paid: gpt-5.5, gpt-5.4, claude-sonnet-5, gemini-3.6-flash, etc.\n\n" +
125
+ "/model <model-id>"
 
 
 
126
  );
127
  });
128
 
 
129
  bot.command("model", async (ctx) => {
130
  const chatId = ctx.chat.id;
131
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
 
132
  const args = ctx.message.text.split(" ").slice(1);
133
  if (args.length === 0) {
134
  const session = userSessions.get(chatId);
135
+ await ctx.reply("Current: " + (session?.model || "deepseek-v4-flash-free"));
136
  return;
137
  }
138
+ let modelId = args[0].toLowerCase();
139
+ if (modelId === "deepseek-r1-7b" || modelId === "local") modelId = LOCAL_MODEL;
140
+ const session = userSessions.get(chatId) || { history: [] };
141
+ userSessions.set(chatId, { model: modelId, history: [] });
142
+ await ctx.reply("Switched to: " + modelId);
 
 
 
 
 
143
  });
144
 
 
145
  bot.command("new", async (ctx) => {
146
  const chatId = ctx.chat.id;
147
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) return;
 
148
  const session = userSessions.get(chatId);
149
+ userSessions.set(chatId, { model: session?.model || "deepseek-v4-flash-free", history: [] });
150
+ await ctx.reply("New conversation started!");
 
151
  });
152
 
 
153
  bot.on("message:text", async (ctx) => {
154
  const chatId = ctx.chat.id;
155
  const message = ctx.message.text;
 
 
156
 
 
 
 
 
 
 
 
 
 
157
  if (CHAT_ID && chatId.toString() !== CHAT_ID.toString()) {
158
+ console.log("[BLOCKED] Chat", chatId);
 
159
  return;
160
  }
161
 
162
+ console.log("[MSG]", chatId, ":", message.substring(0, 50));
163
  await ctx.replyWithChatAction("typing");
164
 
 
165
  if (!userSessions.has(chatId)) {
166
  userSessions.set(chatId, { model: "deepseek-v4-flash-free", history: [] });
167
  }
168
  const session = userSessions.get(chatId);
 
 
169
  const messages = [...session.history, { role: "user", content: message }];
170
 
171
  try {
172
+ const response = await callWithFallback(messages, session.model);
173
 
 
174
  session.history.push({ role: "user", content: message });
175
  session.history.push({ role: "assistant", content: response });
176
+ if (session.history.length > 20) session.history = session.history.slice(-20);
177
 
 
 
 
 
 
 
178
  if (response.length > 4000) {
179
  const chunks = response.match(/.{1,4000}/gs) || [response];
180
+ for (const chunk of chunks) await ctx.reply(chunk);
 
 
 
181
  } else {
182
  await ctx.reply(response);
 
183
  }
184
+ console.log("[REPLY]", response.length, "chars");
185
  } catch (error) {
186
+ console.error("[ERROR]", error.message);
187
+ await ctx.reply("Error: " + error.message);
188
  }
189
  });
190
 
191
+ bot.catch((err) => console.error("[BOT ERROR]", err));
 
 
 
192
 
 
193
  console.log("[OK] Starting Telegram bot...");
194
  bot.start({
195
  onStart: () => {
196
+ console.log("[OK] Telegram bot running!");
197
+ console.log("[OK] Chat ID:", CHAT_ID || "none");
 
198
  },
199
  drop_pending_updates: true,
200
  });
index.html CHANGED
@@ -36,7 +36,7 @@
36
  <body>
37
  <div class="header">
38
  <h1>🤖 OpenCode AI Agent</h1>
39
- <p>Chat with AI models powered by OpenCode Zen</p>
40
  <div class="model-select">
41
  <label>Model:</label>
42
  <select id="model-select">
@@ -85,6 +85,9 @@
85
  <option value="minimax-m3">MiniMax M3</option>
86
  <option value="glm-5.2">GLM 5.2</option>
87
  </optgroup>
 
 
 
88
  <optgroup label="Free Models">
89
  <option value="deepseek-v4-flash-free">DeepSeek V4 Flash Free <span class="free-tag">FREE</span></option>
90
  <option value="mimo-v2.5-free">MiMo-V2.5 Free <span class="free-tag">FREE</span></option>
@@ -144,7 +147,11 @@
144
  if (el) el.remove();
145
  }
146
 
 
 
 
147
  function getEndpoint(modelId) {
 
148
  const openaiPrefixes = ['gpt-', 'deepseek-', 'grok-', 'minimax-', 'glm-', 'kimi-', 'big-pickle', 'mimo-', 'laguna-', 'ling-', 'north-', 'nemotron-'];
149
  if (openaiPrefixes.some(p => modelId.startsWith(p) || modelId.includes(p))) {
150
  return 'https://opencode.ai/zen/v1/chat/completions';
@@ -161,25 +168,32 @@
161
  async function sendMessage() {
162
  const message = inputEl.value.trim();
163
  const apiKey = apiKeyEl.value.trim();
 
 
164
 
165
- if (!apiKey) { addMessage('error', 'Please enter your OpenCode Zen API key first.'); return; }
166
  if (!message) return;
167
 
168
- localStorage.setItem('opencode_zen_key', apiKey);
169
 
170
  addMessage('user', message);
171
  inputEl.value = '';
172
  sendBtn.disabled = true;
173
  addLoading();
174
 
175
- const modelId = modelSelect.value;
176
  const endpoint = getEndpoint(modelId);
177
  const messages = [...history, { role: 'user', content: message }];
178
 
179
  try {
180
  let response, data;
181
 
182
- if (endpoint.includes('/messages')) {
 
 
 
 
 
 
183
  response = await fetch(endpoint, {
184
  method: 'POST',
185
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
@@ -211,16 +225,19 @@
211
  data = await response.json();
212
  let botMessage = '';
213
 
214
- if (endpoint.includes('/messages') && data.content) {
 
 
 
215
  botMessage = data.content.map(c => c.text || '').join('');
216
- } else if (data.choices && data.choices[0]) {
217
- botMessage = data.choices[0].message.content;
218
  } else if (data.candidates && data.candidates[0]) {
219
  botMessage = data.candidates[0].content.parts.map(p => p.text).join('');
220
  } else {
221
  botMessage = JSON.stringify(data, null, 2).substring(0, 2000);
222
  }
223
 
 
 
224
  addMessage('bot', botMessage);
225
  history.push({ role: 'user', content: message });
226
  history.push({ role: 'assistant', content: botMessage });
 
36
  <body>
37
  <div class="header">
38
  <h1>🤖 OpenCode AI Agent</h1>
39
+ <p>Chat with AI models powered by OpenCode Zen + Local DeepSeek R1 7B</p>
40
  <div class="model-select">
41
  <label>Model:</label>
42
  <select id="model-select">
 
85
  <option value="minimax-m3">MiniMax M3</option>
86
  <option value="glm-5.2">GLM 5.2</option>
87
  </optgroup>
88
+ <optgroup label="Local Models (Your Server)">
89
+ <option value="bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF">DeepSeek R1 7B (Local) <span class="free-tag">FREE</span></option>
90
+ </optgroup>
91
  <optgroup label="Free Models">
92
  <option value="deepseek-v4-flash-free">DeepSeek V4 Flash Free <span class="free-tag">FREE</span></option>
93
  <option value="mimo-v2.5-free">MiMo-V2.5 Free <span class="free-tag">FREE</span></option>
 
147
  if (el) el.remove();
148
  }
149
 
150
+ const LOCAL_API = 'https://abbasyk60-ai-telegram-bot.hf.space/v1/chat/completions';
151
+ const LOCAL_MODEL = 'bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF';
152
+
153
  function getEndpoint(modelId) {
154
+ if (modelId === LOCAL_MODEL) return 'local';
155
  const openaiPrefixes = ['gpt-', 'deepseek-', 'grok-', 'minimax-', 'glm-', 'kimi-', 'big-pickle', 'mimo-', 'laguna-', 'ling-', 'north-', 'nemotron-'];
156
  if (openaiPrefixes.some(p => modelId.startsWith(p) || modelId.includes(p))) {
157
  return 'https://opencode.ai/zen/v1/chat/completions';
 
168
  async function sendMessage() {
169
  const message = inputEl.value.trim();
170
  const apiKey = apiKeyEl.value.trim();
171
+ const modelId = modelSelect.value;
172
+ const isLocal = modelId === LOCAL_MODEL;
173
 
174
+ if (!isLocal && !apiKey) { addMessage('error', 'Please enter your OpenCode Zen API key first. (Not needed for local model)'); return; }
175
  if (!message) return;
176
 
177
+ if (!isLocal) localStorage.setItem('opencode_zen_key', apiKey);
178
 
179
  addMessage('user', message);
180
  inputEl.value = '';
181
  sendBtn.disabled = true;
182
  addLoading();
183
 
 
184
  const endpoint = getEndpoint(modelId);
185
  const messages = [...history, { role: 'user', content: message }];
186
 
187
  try {
188
  let response, data;
189
 
190
+ if (isLocal) {
191
+ response = await fetch(LOCAL_API, {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ model: LOCAL_MODEL, messages: messages, max_tokens: 4096 })
195
+ });
196
+ } else if (endpoint.includes('/messages')) {
197
  response = await fetch(endpoint, {
198
  method: 'POST',
199
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
 
225
  data = await response.json();
226
  let botMessage = '';
227
 
228
+ if (data.choices && data.choices[0]) {
229
+ const msg = data.choices[0].message;
230
+ botMessage = msg.content || msg.reasoning_content || '';
231
+ } else if (endpoint.includes('/messages') && data.content) {
232
  botMessage = data.content.map(c => c.text || '').join('');
 
 
233
  } else if (data.candidates && data.candidates[0]) {
234
  botMessage = data.candidates[0].content.parts.map(p => p.text).join('');
235
  } else {
236
  botMessage = JSON.stringify(data, null, 2).substring(0, 2000);
237
  }
238
 
239
+ if (!botMessage) botMessage = '(empty response - try again)';
240
+
241
  addMessage('bot', botMessage);
242
  history.push({ role: 'user', content: message });
243
  history.push({ role: 'assistant', content: botMessage });