unified-ai-router 3.1.7 โ†’ 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -9,5 +9,4 @@ VERCEL_AI_GATEWAY_API_KEY=API_KEY
9
9
  COHERE_API_KEY=API_KEY
10
10
  CEREBRAS_API_KEY=API_KEY
11
11
  LLM7_API_KEY=API_KEY
12
- SEARX_URL=https://searx.perennialte.ch
13
12
  PORT=3000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unified-ai-router",
3
- "version": "3.1.7",
3
+ "version": "3.1.8",
4
4
  "description": "A unified interface for multiple LLM providers with automatic fallback. This project includes an OpenAI-compatible server and a deployable Telegram bot with a Mini App interface. It supports major providers like OpenAI, Google, Grok, and more, ensuring reliability and flexibility for your AI applications.",
5
5
  "license": "ISC",
6
6
  "author": "mlibre",
package/readme.md CHANGED
@@ -15,16 +15,12 @@ It supports major providers like OpenAI, Google, Grok, and more, ensuring reliab
15
15
  - [๐Ÿงช Testing](#-testing)
16
16
  - [๐Ÿ”ง Supported Providers](#-supported-providers)
17
17
  - [๐Ÿ”‘ API Keys](#-api-keys)
18
- - [๐Ÿ”ผ Vercel Deployment (Telegram Bot)](#-vercel-deployment-telegram-bot)
19
- - [๐Ÿ“‹ Prerequisites](#-prerequisites)
20
- - [๐Ÿš€ Deployment Steps](#-deployment-steps)
21
- - [๐Ÿ“ฑ Enable Telegram Mini App](#-enable-telegram-mini-app)
22
18
  - [๐Ÿ“ Project Structure](#-project-structure)
23
19
  - [๐Ÿ“„ License](#-license)
24
20
 
25
21
  ## ๐Ÿš€ Features
26
22
 
27
- - **Multi-Provider Support**: Works with OpenAI, Google, Grok, OpenRouter, Z.ai, Qroq, Cohere, Vercel, Cerebras, and LLM7
23
+ - **Multi-Provider Support**: Works with OpenAI, Google, Grok, OpenRouter, Z.ai, Qroq, Cohere, Cerebras, and LLM7
28
24
  - **Automatic Fallback**: If one provider fails, automatically tries the next
29
25
  - **Simple API**: Easy-to-use interface for all supported providers
30
26
  - **OpenAI-Compatible Server**: Drop-in replacement for the OpenAI API, enabling easy integration with existing tools and clients
@@ -136,7 +132,6 @@ node tests/tools.js
136
132
  - Z.ai
137
133
  - Qroq
138
134
  - Cohere
139
- - Vercel
140
135
  - Cerebras
141
136
  - LLM7
142
137
  - Any Other OpenAI Compatible Server
@@ -156,77 +151,6 @@ Get your API keys from the following providers:
156
151
  - **Cerebras**: [cloud.cerebras.ai](https://cloud.cerebras.ai)
157
152
  - **LLM7**: [token.llm7.io](https://token.llm7.io/)
158
153
 
159
- ## ๐Ÿ”ผ Vercel Deployment (Telegram Bot)
160
-
161
- This section describes how to deploy the AIRouter as a Telegram bot using Vercel. This is a separate deployment from the core library.
162
-
163
- ### ๐Ÿ“‹ Prerequisites
164
-
165
- - A Telegram Bot Token (@BotFather)
166
- - API keys for various AI providers
167
- - Vercel account
168
-
169
- ### ๐Ÿš€ Deployment Steps
170
-
171
- ```bash
172
- # Create the vercel project: vercel.com
173
- # name: ai-router
174
-
175
- npm i -g vercel
176
- vercel login
177
-
178
- nano .env
179
- TELEGRAM_BOT_TOKEN=TOKEN
180
- GOOGLE_API_KEY=API_KEY
181
- OPENROUTER_API_KEY=API_KEY
182
- ZAI_API_KEY=API_KEY
183
- QROQ_API_KEY=API_KEY
184
- COHERE_API_KEY=API_KEY
185
- VERCEL_AI_GATEWAY_API_KEY=API_KEY
186
- CEREBRAS_API_KEY=API_KEY
187
- LLM7_API_KEY=API_KEY
188
- VERCEL_URL=VERCEL_URL
189
- SEARX_URL=SEARX_URL
190
-
191
- vercel env add TELEGRAM_BOT_TOKEN
192
- vercel env add GOOGLE_API_KEY
193
- vercel env add OPENROUTER_API_KEY
194
- vercel env add ZAI_API_KEY
195
- vercel env add QROQ_API_KEY
196
- vercel env add COHERE_API_KEY
197
- vercel env add VERCEL_AI_GATEWAY_API_KEY
198
- vercel env add CEREBRAS_API_KEY
199
- vercel env add LLM7_API_KEY
200
- vercel env add VERCEL_URL
201
- vercel env add SEARX_URL
202
-
203
- # Deploy to Vercel
204
- vercel
205
- vercel --prod
206
-
207
- # Check logs
208
- vercel logs https://ai-router-flame.vercel.app
209
-
210
- # Register webhook for Telegram
211
- # https://ai-router-flame.vercel.app/api?register_webhook=true
212
- curl "https://ai-router-flame.vercel.app/api?register_webhook=true"
213
- ```
214
-
215
- ### ๐Ÿ“ฑ Enable Telegram Mini App
216
-
217
- After deploying the bot, you need to configure the Telegram Mini App and menu button:
218
-
219
- **Configure Mini App:**
220
-
221
- - Go to [@BotFather](https://t.me/botfather)
222
- - Send `/mybots` and select your bot
223
- - Go to `Bot Settings` โ†’ `Configure Mini App`
224
- - Set the Mini App URL to: `https://ai-router-flame.vercel.app`
225
-
226
- Once configured, users can access the Mini App by sending `/start` or `/app` to your bot.
227
-
228
- An example of a deployed bot is accessible on Telegram: [https://t.me/freePulseAIbot](https://t.me/freePulseAIbot)
229
-
230
154
  ## ๐Ÿ“ Project Structure
231
155
 
232
156
  - `main.js` - Core AIRouter library implementing the unified interface and fallback logic
@@ -234,9 +158,9 @@ An example of a deployed bot is accessible on Telegram: [https://t.me/freePulseA
234
158
  - `openai-compatible-server/index.js` - OpenAI-compatible API server
235
159
  - `tests/` - Comprehensive tests for the library, server, and tools
236
160
  - `bruno/` - Bruno API collection for testing endpoints
237
- - `vercel-project/` - Ready-to-deploy Vercel setup for the Telegram bot
238
- - `api/index.js` - Telegram webhook handler
239
- - `api/search.js` - Search proxy endpoint
161
+ - `cloud-flare/` - Ready-to-deploy Cloudflare Pages setup for the Telegram bot
162
+ - `functions/api/index.js` - Telegram webhook handler
163
+ - `functions/api/search.js` - Search proxy endpoint
240
164
  - `public/` - Mini App frontend (HTML, CSS, JS)
241
165
  - `src/config.js` - Bot configuration
242
166
  - `src/telegram.js` - Telegram API integration
@@ -1,109 +0,0 @@
1
- const TelegramClient = require( "../src/telegram" );
2
- const { productionUrl, token } = require( "../src/config" );
3
- const telegramClient = new TelegramClient();
4
- const crypto = require( "crypto" );
5
-
6
- // --- Function to validate Mini App data ---
7
- function validateTelegramData ( telegramData, botToken )
8
- {
9
- if ( !telegramData )
10
- {
11
- return false;
12
- }
13
- const data = new URLSearchParams( telegramData );
14
- const hash = data.get( "hash" );
15
- data.delete( "hash" );
16
-
17
- const dataCheckString = Array.from( data.entries() )
18
- .sort( ( [a], [b] ) => { return a.localeCompare( b ) })
19
- .map( ( [key, value] ) => { return `${key}=${value}` })
20
- .join( "\n" );
21
-
22
- const secretKey = crypto.createHmac( "sha256", "WebAppData" ).update( botToken ).digest();
23
- const calculatedHash = crypto.createHmac( "sha256", secretKey ).update( dataCheckString ).digest( "hex" );
24
-
25
- return calculatedHash === hash;
26
- }
27
-
28
- module.exports = async ( req, res ) =>
29
- {
30
- // --- Handle Mini App API requests ---
31
- if ( req.url.startsWith( "/api/chat" ) )
32
- {
33
- try
34
- {
35
- const telegramData = req.headers["telegram-data"];
36
- if ( !validateTelegramData( telegramData, token ) )
37
- {
38
- return res.status( 403 ).json({ error: "Unauthorized: Invalid Telegram data" });
39
- }
40
-
41
- // Expect 'messages' array instead of 'prompt'
42
- const { messages } = req.body;
43
- if ( !messages || !Array.isArray( messages ) || messages.length === 0 )
44
- {
45
- return res.status( 400 ).json({ error: "Messages array is required" });
46
- }
47
-
48
- const params = new URLSearchParams( telegramData );
49
- const user = JSON.parse( params.get( "user" ) );
50
- console.log( `Processing chat for user ${user.id}. History length: ${messages.length}` );
51
-
52
- // Use your existing AI Router with the full message history
53
- const aiResponse = await telegramClient.aiRouter.chatCompletion( messages );
54
-
55
- return res.status( 200 ).json({ response: aiResponse.content });
56
-
57
- }
58
- catch ( error )
59
- {
60
- console.error( "Error in /api/chat:", error );
61
- return res.status( 500 ).json({ error: "Internal Server Error" });
62
- }
63
- }
64
-
65
- // ... (rest of the webhook handling code remains the same) ...
66
- if ( req.query.register_webhook === "true" )
67
- {
68
- try
69
- {
70
- const response = await telegramClient.registerWebhook( productionUrl );
71
- return res.status( 200 ).json({ success: true, message: "Webhook setup completed", response });
72
- }
73
- catch ( error )
74
- {
75
- console.error( "Error setting up webhook:", error );
76
- return res.status( 500 ).json({ success: false, error: error.message });
77
- }
78
- }
79
- else if ( req.query.unregister_webhook === "true" )
80
- {
81
- try
82
- {
83
- await telegramClient.unRegisterWebhook();
84
- return res.status( 200 ).json({ success: true, message: "Webhook unregistered" });
85
- }
86
- catch ( error )
87
- {
88
- console.error( "Error unregistering webhook:", error );
89
- return res.status( 500 ).json({ success: false, error: error.message });
90
- }
91
- }
92
- try
93
- {
94
- const update = req.body;
95
- if ( !update )
96
- {
97
- console.error( "Received empty request body" );
98
- return res.status( 400 ).send( "Bad Request: Empty body" );
99
- }
100
-
101
- await telegramClient.handleUpdate( update );
102
- res.status( 200 ).send( "OK" );
103
- }
104
- catch ( error )
105
- {
106
- console.error( "Error processing update:", error );
107
- res.status( 500 ).send( "Internal Server Error" );
108
- }
109
- };
@@ -1,114 +0,0 @@
1
- module.exports = async ( req, res ) =>
2
- {
3
- try
4
- {
5
- const q = req.query && req.query.q || req.body && req.body.q;
6
- if ( !q )
7
- {
8
- return res.status( 400 ).json({ error: "q (query) parameter is required" });
9
- }
10
-
11
- const base = ( process.env.SEARX_URL || "https://searx.perennialte.ch" ).replace( /\/+$/, "" );
12
- const url = `${base}/search?q=${encodeURIComponent( q )}&format=json&language=all`;
13
-
14
- // Use sensible browser-like headers and prefer the client's User-Agent when available
15
- const incomingUA = req.headers && req.headers["user-agent"] || "";
16
- const ua = incomingUA || "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0";
17
- const acceptHeader = req.headers && req.headers.accept || "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
18
- const acceptLang = req.headers && req.headers["accept-language"] || "en-US,en;q=0.5";
19
-
20
- const fetchHeaders = {
21
- "User-Agent": ua,
22
- "Accept": acceptHeader,
23
- "Accept-Language": acceptLang,
24
- "DNT": req.headers && req.headers["dnt"] || "1",
25
- "Sec-GPC": req.headers && req.headers["sec-gpc"] || "1",
26
- "Upgrade-Insecure-Requests": "1",
27
- "Sec-Fetch-Dest": "document",
28
- "Sec-Fetch-Mode": "navigate",
29
- "Sec-Fetch-Site": "none",
30
- "Sec-Fetch-User": "?1",
31
- "Referer": base,
32
- };
33
-
34
- // Try a few times on 429 or transient failures with exponential backoff
35
- const maxAttempts = 3;
36
- let attempt = 0;
37
- let r = null;
38
- while ( attempt < maxAttempts )
39
- {
40
- attempt++;
41
- try
42
- {
43
- // Use fetch available on Node 18+ / Vercel runtime
44
- r = await fetch( url, {
45
- method: "GET",
46
- headers: fetchHeaders,
47
- });
48
-
49
- if ( r.ok )
50
- {
51
- // success โ€” parse below
52
- break;
53
- }
54
-
55
- // If rate-limited, wait and retry (honor Retry-After header if present)
56
- if ( r.status === 429 )
57
- {
58
- const retryAfter = r.headers.get( "retry-after" );
59
- const waitMs = retryAfter ? parseInt( retryAfter, 10 ) * 1000 : 200 * attempt ; // small backoff
60
- await new Promise( resolve => { return setTimeout( resolve, waitMs ) });
61
- continue; // retry
62
- }
63
-
64
- // non-429 non-ok โ€” parse body if possible and return helpful info
65
- const t = await r.text().catch( () => { return null });
66
- return res.status( 502 ).json({
67
- error: "Searx returned non-OK",
68
- status: r.status,
69
- details: t,
70
- });
71
- }
72
- catch ( err )
73
- {
74
- // transient network failure โ€” small backoff and retry
75
- if ( attempt < maxAttempts )
76
- {
77
- await new Promise( resolve => { return setTimeout( resolve, 150 * attempt ) });
78
- continue;
79
- }
80
- else
81
- {
82
- console.error( "search proxy fetch error:", err );
83
- return res.status( 502 ).json({ error: "Searx fetch failed", details: String( err && err.message ? err.message : err ) });
84
- }
85
- }
86
- } // end retry loop
87
-
88
- if ( !r )
89
- {
90
- return res.status( 502 ).json({ error: "No response from Searx" });
91
- }
92
-
93
- if ( !r.ok )
94
- {
95
- const t = await r.text().catch( () => { return null });
96
- return res.status( 502 ).json({
97
- error: "Searx returned non-OK after retries",
98
- status: r.status,
99
- details: t,
100
- });
101
- }
102
-
103
- const json = await r.json();
104
-
105
- // Lightweight caching headers for Vercel CDN
106
- res.setHeader( "Cache-Control", "s-maxage=60, stale-while-revalidate=120" );
107
- return res.status( 200 ).json( json );
108
- }
109
- catch ( err )
110
- {
111
- console.error( "search proxy error:", err );
112
- return res.status( 500 ).json({ error: err.message });
113
- }
114
- };
@@ -1,351 +0,0 @@
1
- // public/app.js - Vanilla JS front-end for Telegram Mini App (drop-in)
2
- // Updated: sanitize assistant text, friendly search error handling, theme variables mapping,
3
- // removed separate search toggle (single composer + include-search checkbox used)
4
-
5
- ( function ()
6
- {
7
- const WebApp = window.Telegram?.WebApp;
8
- if ( !WebApp )
9
- {
10
- console.warn( "Telegram WebApp object not available." );
11
- }
12
-
13
- // UI elements
14
- const messagesEl = document.getElementById( "messages" );
15
- const inputEl = document.getElementById( "input" );
16
- const formEl = document.getElementById( "composer" );
17
- const sendBtn = document.getElementById( "send" );
18
- const fullscreenBtn = document.getElementById( "fullscreen-btn" );
19
- const searchPanel = document.getElementById( "search-panel" );
20
- const searchResultsEl = document.getElementById( "search-results" );
21
- const searchStatusEl = document.getElementById( "search-status" );
22
- const includeSearchCheckbox = document.getElementById( "include-search" );
23
-
24
- let conversation = [];
25
- let lastSearchResults = [];
26
-
27
- // Initialize WebApp
28
- try
29
- {
30
- WebApp?.ready?.();
31
- WebApp?.expand?.();
32
- }
33
- catch ( e ) {}
34
-
35
- // Apply theme params to CSS variables (use Telegram's themeParams fields)
36
- function applyTheme ()
37
- {
38
- try
39
- {
40
- const t = WebApp?.themeParams || {};
41
- // Map many theme params to CSS variables so style.css can pick them up
42
- const mappings = {
43
- "--tg-theme-bg-color": t.bg_color || "#ffffff",
44
- "--tg-theme-text-color": t.text_color || "#111827",
45
- "--tg-theme-hint-color": t.hint_color || "#6b7280",
46
- "--tg-theme-link-color": t.link_color || "#2563eb",
47
- "--tg-theme-button-color": t.button_color || "#2563eb",
48
- "--tg-theme-button-text-color": t.button_text_color || "#ffffff",
49
- "--tg-theme-secondary-bg-color": t.secondary_bg_color || "#f3f4f6",
50
- "--tg-theme-header-bg-color": t.header_bg_color || "#ffffff",
51
- "--tg-theme-bottom-bar-bg-color": t.bottom_bar_bg_color || "#ffffff",
52
- "--tg-theme-accent-text-color": t.accent_text_color || "#111827",
53
- "--tg-theme-section-bg-color": t.section_bg_color || "#ffffff",
54
- "--tg-theme-section-header-text-color": t.section_header_text_color || "#111827",
55
- "--tg-theme-section-separator-color": t.section_separator_color || "#e5e7eb",
56
- "--tg-theme-subtitle-text-color": t.subtitle_text_color || "#6b7280",
57
- "--tg-theme-destructive-text-color": t.destructive_text_color || "#ef4444",
58
- };
59
- Object.entries( mappings ).forEach( ( [k, v] ) => { return document.documentElement.style.setProperty( k, v ) });
60
- }
61
- catch ( e )
62
- {
63
- console.warn( "applyTheme failed", e );
64
- }
65
- }
66
- applyTheme();
67
- WebApp?.onEvent && WebApp.onEvent( "themeChanged", applyTheme );
68
-
69
- // Cloud storage helpers
70
- function saveHistory ()
71
- {
72
- try
73
- {
74
- WebApp.CloudStorage.setItem( "chat_history", JSON.stringify( conversation ), ( err ) =>
75
- {
76
- if ( err ) console.warn( "CloudStorage save failed", err );
77
- });
78
- }
79
- catch ( e )
80
- {
81
- // ignore
82
- }
83
- }
84
- function loadHistory ()
85
- {
86
- try
87
- {
88
- WebApp.CloudStorage.getItem( "chat_history", ( err, value ) =>
89
- {
90
- if ( err || !value )
91
- {
92
- conversation = [{ role: "assistant", content: "Hi โ€” welcome to the new AI Router! Use the checkbox to include web results in your request." }];
93
- renderMessages();
94
- saveHistory();
95
- return;
96
- }
97
- try
98
- {
99
- conversation = JSON.parse( value );
100
- }
101
- catch ( e )
102
- {
103
- conversation = [{ role: "assistant", content: "Hi โ€” welcome to the new AI Router!" }];
104
- }
105
- renderMessages();
106
- });
107
- }
108
- catch ( e )
109
- {
110
- conversation = [{ role: "assistant", content: "Hi โ€” welcome to the new AI Router!" }];
111
- renderMessages();
112
- }
113
- }
114
-
115
- // Rendering
116
- function renderMessages ()
117
- {
118
- messagesEl.innerHTML = "";
119
- conversation.forEach( ( m ) =>
120
- {
121
- const div = document.createElement( "div" );
122
- div.className = `message ${ m.role === "user" ? "user" : "assistant"}`;
123
- // allow HTML-safe line breaks - keep textContent to avoid XSS
124
- div.textContent = m.content;
125
- messagesEl.appendChild( div );
126
- });
127
- messagesEl.scrollTop = messagesEl.scrollHeight;
128
- }
129
-
130
- // Add message (and persist)
131
- function pushMessage ( role, content )
132
- {
133
- conversation.push({ role, content });
134
- renderMessages();
135
- saveHistory();
136
- }
137
-
138
- // Simple helper to sanitize assistant text:
139
- // - remove leading/trailing whitespace/newlines
140
- // - collapse 3+ consecutive newlines into max 2
141
- // - preserve paragraph breaks up to 2 newlines
142
- function sanitizeAssistantText ( text )
143
- {
144
- if ( !text && text !== "" ) return text;
145
- // normalize CRLF
146
- let s = String( text ).replace( /\r/g, "" );
147
- // remove leading blank lines
148
- s = s.replace( /^\s*\n+/, "" );
149
- // remove trailing blank lines
150
- s = s.replace( /\n+\s*$/, "" );
151
- // collapse >2 blank lines into 2
152
- s = s.replace( /\n{3,}/g, "\n\n" );
153
- // trim surrounding whitespace
154
- s = s.trim();
155
- return s;
156
- }
157
-
158
- // Render search results list
159
- function renderSearchResults ( items )
160
- {
161
- searchResultsEl.innerHTML = "";
162
- if ( !items || items.length === 0 )
163
- {
164
- searchResultsEl.innerHTML = "<li class=\"no-results\">No results</li>";
165
- return;
166
- }
167
- items.forEach( ( r, idx ) =>
168
- {
169
- const li = document.createElement( "li" );
170
- li.className = "search-item";
171
- const a = document.createElement( "a" );
172
- a.href = r.link || "#";
173
- a.target = "_blank";
174
- a.rel = "noopener noreferrer";
175
- a.textContent = r.title || r.link || `Result ${idx + 1}`;
176
- const p = document.createElement( "p" );
177
- p.className = "snippet";
178
- p.textContent = r.snippet || "";
179
- li.appendChild( a );
180
- li.appendChild( p );
181
- searchResultsEl.appendChild( li );
182
- });
183
- }
184
-
185
- // Search function that returns normalized results or throws an Error with friendly message
186
- async function doSearchFetch ( query )
187
- {
188
- if ( !query || !query.trim() ) return [];
189
- const url = `/api/search?q=${encodeURIComponent( query )}`;
190
- const resp = await fetch( url, { headers: { "Telegram-Data": WebApp?.initData || "" } });
191
- if ( !resp.ok )
192
- {
193
- // try to parse helpful info
194
- let body;
195
- try { body = await resp.json(); }
196
- catch ( e ) { body = await resp.text().catch( () => { return null }); }
197
- // map status codes to friendly messages when possible
198
- if ( resp.status === 429 || body && ( body.details && String( body.details ).toLowerCase().includes( "too many requests" ) ) )
199
- {
200
- throw new Error( "Too Many Requests โ€” search rate limited. Please try again later." );
201
- }
202
- else
203
- {
204
- const message = body && ( body.error || body.details || JSON.stringify( body ) ) || `${resp.status} ${resp.statusText}`;
205
- throw new Error( `Search failed: ${message}` );
206
- }
207
- }
208
- const js = await resp.json();
209
-
210
- // Normalize common shapes of Searx responses
211
- let rawResults = [];
212
- if ( Array.isArray( js.results ) && js.results.length )
213
- {
214
- rawResults = js.results;
215
- }
216
- else if ( js.categories && js.categories.general && Array.isArray( js.categories.general.results ) )
217
- {
218
- rawResults = js.categories.general.results;
219
- }
220
- else if ( Array.isArray( js ) )
221
- {
222
- rawResults = js;
223
- }
224
- else if ( Array.isArray( js.raw ) )
225
- {
226
- rawResults = js.raw;
227
- }
228
- else if ( js.results && Array.isArray( js.results ) )
229
- {
230
- rawResults = js.results;
231
- }
232
-
233
- const normalized = rawResults.slice( 0, 8 ).map( r =>
234
- {
235
- return {
236
- title: r.title || r.name || r.headers?.title || r.url || r.link || "",
237
- snippet: r.content || r.snippet || r.excerpt || r.description || "",
238
- link: r.url || r.link || r.source || r.original_url || "",
239
- }
240
- });
241
-
242
- return normalized;
243
- }
244
-
245
- // Main submit handler
246
- formEl.addEventListener( "submit", async function ( ev )
247
- {
248
- ev.preventDefault();
249
- const text = inputEl.value.trim();
250
- if ( !text ) return;
251
-
252
- // push user message immediately
253
- pushMessage( "user", text );
254
-
255
- // If includeSearch is checked -> do search first, show results, and include them in chat request
256
- if ( includeSearchCheckbox.checked )
257
- {
258
- // show searching placeholder and clear previous results
259
- searchStatusEl.textContent = "Searchingโ€ฆ";
260
- searchResultsEl.innerHTML = "";
261
- searchPanel.classList.remove( "hidden" );
262
-
263
- try
264
- {
265
- const results = await doSearchFetch( text );
266
- lastSearchResults = results;
267
- renderSearchResults( results );
268
- searchStatusEl.textContent = `Found ${results.length} result${results.length === 1 ? "" : "s"}. These will be included in your request.`;
269
- }
270
- catch ( err )
271
- {
272
- // friendly error for the user; DO NOT place raw JSON object into the assistant reply
273
- const friendly = err?.message || "Search failed";
274
- pushMessage( "assistant", friendly );
275
- // stop here โ€” do not automatically call /api/chat when search failed
276
- inputEl.value = "";
277
- return;
278
- }
279
- }
280
-
281
- // show thinking placeholder
282
- pushMessage( "assistant", "Thinking..." );
283
-
284
- // Build payload for /api/chat. If we've just done search (includeSearch checked), include snippets
285
- const includeSearch = includeSearchCheckbox.checked ? lastSearchResults.slice( 0, 3 ) || [] : [];
286
-
287
- const payload = {
288
- messages: conversation.concat( [{ role: "user", content: text }] ),
289
- includeSearch,
290
- };
291
-
292
- try
293
- {
294
- const resp = await fetch( "/api/chat", {
295
- method: "POST",
296
- headers: {
297
- "Content-Type": "application/json",
298
- "Telegram-Data": WebApp?.initData || "",
299
- },
300
- body: JSON.stringify( payload ),
301
- });
302
-
303
- if ( !resp.ok )
304
- {
305
- const data = await resp.json().catch( () => { return {} });
306
- throw new Error( data.error || `${resp.status} ${resp.statusText}` );
307
- }
308
-
309
- const body = await resp.json();
310
- // sanitize response text
311
- const raw = body && ( body.response || "" ) || "";
312
- const sanitized = sanitizeAssistantText( raw );
313
-
314
- // replace the last 'Thinking...' assistant message with the real response
315
- if ( conversation.length > 0 && conversation[conversation.length - 1].role === "assistant" && conversation[conversation.length - 1].content === "Thinking..." )
316
- {
317
- conversation = conversation.slice( 0, -1 );
318
- }
319
- conversation.push({ role: "assistant", content: sanitized });
320
- renderMessages();
321
- saveHistory();
322
- }
323
- catch ( err )
324
- {
325
- console.error( "Chat error", err );
326
- // replace 'Thinking...' with friendly error
327
- if ( conversation.length > 0 && conversation[conversation.length - 1].role === "assistant" && conversation[conversation.length - 1].content === "Thinking..." )
328
- {
329
- conversation = conversation.slice( 0, -1 );
330
- }
331
- conversation.push({ role: "assistant", content: `Error: ${err.message}` });
332
- renderMessages();
333
- saveHistory();
334
- }
335
- finally
336
- {
337
- inputEl.value = "";
338
- }
339
- });
340
-
341
- // Fullscreen button
342
- fullscreenBtn.addEventListener( "click", () =>
343
- {
344
- if ( !WebApp ) return;
345
- if ( WebApp.isFullscreen ) WebApp.exitFullscreen();
346
- else WebApp.requestFullscreen();
347
- });
348
-
349
- // initial load
350
- loadHistory();
351
- })();
@@ -1,44 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="utf-8" />
6
- <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,user-scalable=no" />
7
- <title>AI Router โ€” New UI</title>
8
- <link rel="stylesheet" href="style.css" />
9
- <script src="https://telegram.org/js/telegram-web-app.js"></script>
10
- </head>
11
-
12
- <body>
13
- <div class="app-shell">
14
- <header class="app-header">
15
- <div class="header-actions">
16
- <button id="fullscreen-btn" class="icon-btn" aria-label="Toggle fullscreen">โคข</button>
17
- </div>
18
- </header>
19
-
20
- <main class="chat-area">
21
- <div id="messages" class="messages" role="log" aria-live="polite"></div>
22
-
23
- <div id="search-panel" class="search-panel hidden">
24
- <div id="search-status" class="search-status">Search results will appear here when you use the "Include
25
- search" option.</div>
26
- <ul id="search-results" class="search-results"></ul>
27
- </div>
28
- </main>
29
-
30
- <form id="composer" class="composer" autocomplete="off">
31
- <input id="input" type="text" placeholder="Ask anything..." aria-label="Message input" />
32
- <label class="checkbox-inline" title="Include most recent search snippets when asking">
33
- <input id="include-search" type="checkbox" />
34
- <span class="small">Include search</span>
35
- </label>
36
- <button id="send" type="submit" class="send-btn">โ†’</button>
37
- </form>
38
-
39
- </div>
40
-
41
- <script src="app.js"></script>
42
- </body>
43
-
44
- </html>
Binary file
@@ -1,243 +0,0 @@
1
- :root {
2
- --tg-theme-bg-color: #ffffff;
3
- --tg-theme-text-color: #111827;
4
- --tg-theme-hint-color: #6b7280;
5
- --tg-theme-link-color: #2563eb;
6
- --tg-theme-button-color: #2563eb;
7
- --tg-theme-button-text-color: #ffffff;
8
- --tg-theme-secondary-bg-color: #f3f4f6;
9
- --tg-theme-header-bg-color: #ffffff;
10
- --tg-theme-bottom-bar-bg-color: #ffffff;
11
- --tg-theme-accent-text-color: #111827;
12
- --tg-theme-section-bg-color: #ffffff;
13
- --tg-theme-section-header-text-color: #111827;
14
- --tg-theme-section-separator-color: #e5e7eb;
15
- --tg-theme-subtitle-text-color: #6b7280;
16
- --tg-theme-destructive-text-color: #ef4444;
17
-
18
- /* Map to the variables used in the stylesheet */
19
- --bg: var(--tg-theme-bg-color);
20
- --text: var(--tg-theme-text-color);
21
- --hint: var(--tg-theme-hint-color);
22
- --accent: var(--tg-theme-button-color);
23
- --muted: var(--tg-theme-secondary-bg-color);
24
- }
25
-
26
- * {
27
- box-sizing: border-box
28
- }
29
-
30
- html,
31
- body,
32
- #root {
33
- height: 100%
34
- }
35
-
36
- body {
37
- font-family: inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
38
- margin: 0;
39
- height: 100vh;
40
- background: var(--bg);
41
- color: var(--text);
42
- -webkit-font-smoothing: antialiased;
43
- -moz-osx-font-smoothing: grayscale;
44
- display: flex;
45
- align-items: stretch;
46
- justify-content: center;
47
- }
48
-
49
- .app-shell {
50
- width: 100%;
51
- max-width: 820px;
52
- height: 100vh;
53
- display: flex;
54
- flex-direction: column;
55
- background: var(--bg);
56
- }
57
-
58
- .app-header {
59
- display: flex;
60
- align-items: center;
61
- justify-content: space-between;
62
- padding: 12px 14px;
63
- border-bottom: 1px solid var(--tg-theme-section-separator-color);
64
- /* use Telegram-provided header background color instead of a hard white gradient */
65
- background: var(--tg-theme-header-bg-color);
66
- /* subtle inset separator for depth without forcing a light overlay */
67
- box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.04);
68
- /* remove forced blur/white overlay which looks bad in dark themes */
69
- backdrop-filter: none;
70
- }
71
-
72
- .brand-title {
73
- font-weight: 700;
74
- font-size: 18px
75
- }
76
-
77
- .brand-sub {
78
- font-size: 12px;
79
- color: var(--hint)
80
- }
81
-
82
- .header-actions {
83
- display: flex;
84
- gap: 8px;
85
- align-items: center
86
- }
87
-
88
- .icon-btn {
89
- background: transparent;
90
- border: 1px solid var(--tg-theme-section-separator-color);
91
- padding: 8px;
92
- border-radius: 10px;
93
- cursor: pointer;
94
- }
95
-
96
- .chat-area {
97
- flex: 1;
98
- overflow: hidden;
99
- display: flex;
100
- flex-direction: column;
101
- padding: 12px
102
- }
103
-
104
- .messages {
105
- flex: 1;
106
- overflow: auto;
107
- display: flex;
108
- flex-direction: column;
109
- gap: 10px;
110
- padding-bottom: 6px
111
- }
112
-
113
- .message {
114
- max-width: 78%;
115
- padding: 10px 14px;
116
- border-radius: 14px;
117
- line-height: 1.4;
118
- white-space: pre-wrap
119
- }
120
-
121
- .message.user {
122
- align-self: flex-end;
123
- background: var(--accent);
124
- color: white;
125
- border-bottom-right-radius: 6px
126
- }
127
-
128
- .message.assistant {
129
- align-self: flex-start;
130
- background: var(--muted);
131
- color: var(--text);
132
- border-bottom-left-radius: 6px
133
- }
134
-
135
- .composer {
136
- display: flex;
137
- gap: 8px;
138
- padding: 10px;
139
- background: var(--tg-theme-secondary-bg-color);
140
- border-top: 1px solid var(--tg-theme-section-separator-color);
141
- align-items: center
142
- }
143
-
144
- .composer input[type="text"] {
145
- flex: 1;
146
- padding: 10px 12px;
147
- border-radius: 18px;
148
- border: 1px solid var(--tg-theme-section-separator-color);
149
- background: var(--tg-theme-section-bg-color);
150
- color: var(--text);
151
- outline: none;
152
- }
153
-
154
- .send-btn {
155
- background: var(--accent);
156
- color: #fff;
157
- padding: 0;
158
- border-radius: 50%;
159
- border: none;
160
- cursor: pointer;
161
- min-width: 44px;
162
- height: 44px;
163
- display: flex;
164
- align-items: center;
165
- justify-content: center;
166
- font-size: 18px
167
- }
168
-
169
- .search-panel {
170
- padding: 8px 0
171
- }
172
-
173
- .search-panel.hidden {
174
- display: none
175
- }
176
-
177
- .search-results {
178
- list-style: none;
179
- margin: 0;
180
- padding: 0;
181
- display: flex;
182
- flex-direction: column;
183
- gap: 8px
184
- }
185
-
186
- .search-item {
187
- border-radius: 12px;
188
- padding: 10px;
189
- border: 1px solid #f1f5f9;
190
- background: #fff
191
- }
192
-
193
- .search-item a {
194
- font-weight: 600;
195
- text-decoration: none;
196
- color: var(--accent)
197
- }
198
-
199
- .search-item .snippet {
200
- font-size: 13px;
201
- color: var(--hint);
202
- margin-top: 6px
203
- }
204
-
205
- .checkbox-inline {
206
- display: flex;
207
- align-items: center;
208
- gap: 6px
209
- }
210
-
211
- .small {
212
- font-size: 12px;
213
- color: var(--hint)
214
- }
215
-
216
- .app-footer {
217
- text-align: center;
218
- font-size: 12px;
219
- color: var(--hint);
220
- padding: 8px 6px;
221
- /* footer border removed because footer text was removed */
222
- border-top: none;
223
- }
224
-
225
- .no-results {
226
- color: var(--hint);
227
- font-size: 13px;
228
- padding: 10px
229
- }
230
-
231
- @media (max-width:520px) {
232
- .app-shell {
233
- height: 100vh
234
- }
235
-
236
- .brand-title {
237
- font-size: 16px
238
- }
239
-
240
- .composer {
241
- padding: 8px
242
- }
243
- }
@@ -1,74 +0,0 @@
1
- require( "dotenv" ).config({ quiet: true });
2
-
3
- const productionUrl = `https://${ process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
4
- const webhookPath = "/api";
5
-
6
- const token = process.env.TELEGRAM_BOT_TOKEN;
7
- if ( !token )
8
- {
9
- throw new Error( "TELEGRAM_BOT_TOKEN is required" );
10
- }
11
-
12
- const providers = [
13
- {
14
- name: "openrouter",
15
- apiKey: process.env.OPENROUTER_API_KEY,
16
- model: "z-ai/glm-4.5-air:free",
17
- apiUrl: "https://openrouter.ai/api/v1",
18
- },
19
- {
20
- name: "google",
21
- apiKey: process.env.GOOGLE_API_KEY,
22
- model: "gemini-2.5-pro",
23
- apiUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
24
- },
25
- {
26
- name: "google",
27
- apiKey: process.env.GOOGLE_API_KEY,
28
- model: "gemini-2.5-flash",
29
- apiUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
30
- },
31
- {
32
- name: "z.ai",
33
- apiKey: process.env.ZAI_API_KEY,
34
- model: "glm-4.5-flash",
35
- apiUrl: "https://api.z.ai/api/paas/v4",
36
- },
37
- {
38
- name: "qroq",
39
- apiKey: process.env.QROQ_API_KEY,
40
- model: "openai/gpt-oss-120b",
41
- apiUrl: "https://api.groq.com/openai/v1",
42
- },
43
- {
44
- name: "vercel",
45
- apiKey: process.env.VERCEL_AI_GATEWAY_API_KEY,
46
- model: "openai/gpt-oss-120b",
47
- apiUrl: "https://ai-gateway.vercel.sh/v1",
48
- },
49
- {
50
- name: "cerebras",
51
- apiKey: process.env.CEREBRAS_API_KEY,
52
- model: "gpt-oss-120b",
53
- apiUrl: "https://api.cerebras.ai/v1",
54
- },
55
- {
56
- name: "llm7",
57
- apiKey: process.env.LLM7_API_KEY,
58
- model: "gpt-o4-mini-2025-04-16",
59
- apiUrl: "https://api.llm7.io/v1",
60
- },
61
- {
62
- name: "cohere",
63
- apiKey: process.env.COHERE_API_KEY,
64
- model: "command-a-03-2025",
65
- apiUrl: "https://api.cohere.ai/compatibility/v1",
66
- },
67
- ];
68
-
69
- module.exports = {
70
- productionUrl,
71
- webhookPath,
72
- token,
73
- providers
74
- };
@@ -1,161 +0,0 @@
1
- const { webhookPath, token, productionUrl } = require( "./config.js" );
2
- const AIRouter = require( "../../main.js" );
3
- const { providers } = require( "./config.js" );
4
-
5
- class TelegramClient
6
- {
7
- constructor ()
8
- {
9
- this.token = token;
10
- this.apiBaseUrl = `https://api.telegram.org/bot${token}`;
11
- this.aiRouter = new AIRouter( providers );
12
- this.chatHistories = {}; // In-memory store for chat histories
13
- }
14
- isNetworkError ( error )
15
- {
16
- // TODO: check with fetch
17
- return error.message.includes( "socket hang up" ) ||
18
- error.message.includes( "network socket disconnected" ) ||
19
- error.message.includes( "fetch failed" );
20
- }
21
-
22
- async sleep ( ms )
23
- {
24
- await new Promise( resolve => { return setTimeout( resolve, ms ) });
25
- }
26
-
27
- async sendMessageWithRetry ( chatId, message, options = {})
28
- {
29
- return await this.withRetry( () =>
30
- {
31
- return this.makeRequest( "sendMessage", {
32
- chat_id: chatId,
33
- text: message,
34
- ...options,
35
- });
36
- }, options );
37
- }
38
-
39
- async withRetry ( operation, options, retries = 10, delay = 50 )
40
- {
41
- console.log( `Starting operation with ${retries} retries` );
42
- for ( let i = 0; i < retries; i++ )
43
- {
44
- try
45
- {
46
- return await operation();
47
- }
48
- catch ( error )
49
- {
50
- if ( this.isNetworkError( error ) )
51
- {
52
- console.log( `Retrying... Attempts left: ${retries - i - 1}`, error.cause, error.message );
53
- await this.sleep( delay );
54
- }
55
- else
56
- {
57
- throw error;
58
- }
59
- }
60
- }
61
- throw new Error( "Max retries reached" );
62
- }
63
-
64
- async makeRequest ( method, params = {})
65
- {
66
- const url = `${this.apiBaseUrl}/${method}`;
67
- const response = await fetch( url, {
68
- method: "POST",
69
- headers: {
70
- "Content-Type": "application/json",
71
- },
72
- body: JSON.stringify( params ),
73
- });
74
-
75
- if ( !response.ok )
76
- {
77
- const error = await response.json();
78
- throw new Error( `Telegram API error: ${JSON.stringify( error )}` );
79
- }
80
-
81
- return await response.json();
82
- }
83
-
84
- async handleUpdate ( update )
85
- {
86
- if ( "message" in update && update.message.text )
87
- {
88
- const { text } = update.message;
89
- const chatId = update.message.chat.id;
90
- console.log( "text", text, "chatId", chatId );
91
- // console.log( "chat history for ", chatId, this.chatHistories[chatId] );
92
-
93
- // --- Command to launch the Mini App ---
94
- if ( text === "/start" || text === "/app" )
95
- {
96
- const miniAppUrl = productionUrl; // Your Vercel deployment URL
97
-
98
- const keyboard = {
99
- inline_keyboard: [
100
- [
101
- {
102
- text: "๐Ÿš€ Open AI Router App",
103
- web_app: { url: miniAppUrl }
104
- }
105
- ]
106
- ]
107
- };
108
-
109
- await this.sendMessageWithRetry( chatId, "Click the button below to open the AI Router Mini App!", {
110
- reply_markup: JSON.stringify( keyboard )
111
- });
112
- return; // Stop processing further to avoid AI response
113
- }
114
- // --- END ---
115
-
116
- if ( !this.chatHistories[chatId] )
117
- {
118
- this.chatHistories[chatId] = [];
119
- }
120
-
121
- const history = this.chatHistories[chatId];
122
- history.push({ role: "user", content: text });
123
- if ( history.length > 100 )
124
- {
125
- this.chatHistories[chatId] = history.slice( history.length - 100 );
126
- }
127
- // console.log( "chat history for ", chatId, "after update", this.chatHistories[chatId] );
128
- const systemPrompt = {
129
- role: "system",
130
- content: "You are a kind and selfless AI assistant. Be helpful and compassionate in your responses. Speak in user's language",
131
- };
132
-
133
- const messages = [
134
- systemPrompt,
135
- ...this.chatHistories[chatId],
136
- ];
137
-
138
- const response = await this.aiRouter.chatCompletion( messages );
139
- history.push({ role: "assistant", content: response.content });
140
- await this.sendMessageWithRetry( chatId, response.content );
141
- }
142
- }
143
-
144
- async registerWebhook ( productionUrl )
145
- {
146
- console.log( `Registering webhook with URL: ${productionUrl}${webhookPath}` );
147
- const response = await this.makeRequest( "setWebhook", {
148
- url: `${productionUrl}${webhookPath}`,
149
- drop_pending_updates: true,
150
- });
151
- return { response, url: `${productionUrl}${webhookPath}` };
152
- }
153
-
154
- async unRegisterWebhook ()
155
- {
156
- const response = await this.makeRequest( "setWebhook", { url: "" });
157
- return response.ok === true;
158
- }
159
- }
160
-
161
- module.exports = TelegramClient;
package/vercel.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "version": 2,
3
- "builds": [
4
- {
5
- "src": "vercel-project/api/index.js",
6
- "use": "@vercel/node"
7
- },
8
- {
9
- "src": "vercel-project/public/**",
10
- "use": "@vercel/static"
11
- }
12
- ],
13
- "routes": [
14
- {
15
- "src": "/api",
16
- "dest": "vercel-project/api/index.js"
17
- },
18
- {
19
- "src": "/api/(.*)",
20
- "dest": "vercel-project/api/index.js"
21
- },
22
- {
23
- "src": "/(.*)",
24
- "dest": "vercel-project/public/$1"
25
- }
26
- ]
27
- }