levix-bot 2.2.1 → 2.3.0

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/README.md CHANGED
@@ -1,127 +1,196 @@
1
- # Levix
1
+ <p align="center">
2
+ <img src="public/brand/banner.webp" alt="Levix - Personal WhatsApp Bot" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/Abdodiab2005/levix/actions/workflows/ci.yml"><img src="https://github.com/Abdodiab2005/levix/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://www.npmjs.com/package/levix-bot"><img src="https://img.shields.io/npm/v/levix-bot?color=2563eb" alt="npm version"></a>
8
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-10b981" alt="MIT License"></a>
9
+ <img src="https://img.shields.io/badge/Node.js-24%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 24+">
10
+ </p>
11
+
12
+ <p align="center">
13
+ <strong>Free, open-source, self-hosted WhatsApp automation that stays under your control.</strong>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://levix.leviro.net">Website</a> ·
18
+ <a href="SETUP.md">Setup guide</a> ·
19
+ <a href="https://github.com/Abdodiab2005/levix/releases/latest">Downloads</a> ·
20
+ <a href="https://github.com/Abdodiab2005/levix/issues">Issues</a>
21
+ </p>
22
+
23
+ Levix is a personal WhatsApp bot with 55 commands, group moderation, scheduled
24
+ messages, an AI agent, media tools, and a web control panel. You run it on your
25
+ own computer or server, keep its database and session files yourself, and can
26
+ change almost everything while it is running.
27
+
28
+ There is no paid tier, hosted account, external database, or configuration file
29
+ to maintain. Levix is released under the MIT License.
30
+
31
+ > [!WARNING]
32
+ > Levix uses [Baileys](https://github.com/WhiskeySockets/Baileys), an unofficial
33
+ > WhatsApp Web client, and is not affiliated with or endorsed by WhatsApp or
34
+ > Meta. Unofficial automation can lead to temporary restrictions or an account
35
+ > ban. Do not use Levix for spam or unsolicited bulk messaging. If an outage or
36
+ > account restriction is unacceptable, use the official WhatsApp Business
37
+ > Platform instead.
38
+
39
+ ## Quick start
40
+
41
+ ### Linux server
42
+
43
+ The public installer installs the latest stable release and configures Levix as
44
+ a service:
2
45
 
3
- A self-hosted personal WhatsApp bot: 55 commands, group moderation, an AI agent
4
- on Google Gemini, scheduled messages, and a web control panel that can change
5
- almost all of it while the bot runs.
46
+ ```bash
47
+ curl -fsSL https://levix.leviro.net/install.sh | bash
48
+ ```
49
+
50
+ You can [read the installer](deploy/install.sh) before running it.
6
51
 
7
- Built by Abdelrhman Diab (Leviro).
52
+ ### npm
53
+
54
+ Requires Node.js 24 or newer:
8
55
 
9
56
  ```bash
10
57
  npm install -g levix-bot
11
58
  levix
12
59
  ```
13
60
 
14
- The npm package is named `levix-bot`; the installed CLI command stays `levix`.
61
+ ### Docker
15
62
 
16
- On a desktop that opens the panel in your browser by itself. Pick a password,
17
- go to **Connection**, press **Start session** and scan the QR — see
18
- [SETUP.md](SETUP.md) for the longer version, Docker, and running it as a
19
- service.
63
+ ```bash
64
+ git clone https://github.com/Abdodiab2005/levix
65
+ cd levix
66
+ docker compose up -d
67
+ docker compose logs levix
68
+ ```
20
69
 
21
- On a brand-new install, starting Levix does not begin WhatsApp pairing by
22
- itself: the panel, database and commands come up first, and the Connection
23
- screen starts the first session when you ask. After that first successful link,
24
- the saved WhatsApp session resumes automatically after a process, Docker or
25
- systemd restart unless you explicitly stop or unlink it.
70
+ Standalone Linux, macOS ARM64, and Windows binaries are available from the
71
+ [latest GitHub release](https://github.com/Abdodiab2005/levix/releases/latest).
26
72
 
27
- ```
28
- levix start Levix with the web panel
29
- levix headless start Levix with no web UI at all
30
- levix where print the data directory
31
- levix reset-password reset the panel password
32
- levix domain [name] point a domain at Levix (may need sudo)
33
- ```
73
+ When Levix starts:
34
74
 
35
- ---
75
+ 1. Open the panel URL printed in the terminal.
76
+ 2. Choose a panel password. A remote first-time setup also asks for the printed
77
+ setup code.
78
+ 3. Open **Connection**, press **Start session**, and scan the QR from WhatsApp.
79
+ 4. Send `!ping` in a chat.
36
80
 
37
- ## What it does
81
+ A successfully linked WhatsApp session resumes automatically after a process,
82
+ Docker, or systemd restart. See [SETUP.md](SETUP.md) for domains, reverse
83
+ proxies, headless mode, backups, and troubleshooting.
38
84
 
39
- **Commands** — `!ping`, `!help`, `!calc`, `!poll`, `!rand`, `!loop`, `!todo`,
40
- `!notes`, `!debt`, `!weather`, `!prayer`, `!tts`, `!stt`, `!shortlink`,
41
- `!qr`, `!score`… every one of them can be renamed, re-permissioned or turned
42
- off from the panel.
85
+ ## What you get
43
86
 
44
- **Group moderation** — welcome messages, anti-link, anti-spam, media rules,
45
- warnings with auto-kick, rules and notes, plus the usual `!group kick`,
46
- `promote`, `demote`, `add`, `tagadmins`.
87
+ | Area | What Levix provides |
88
+ | --- | --- |
89
+ | Commands | 55 commands for utilities, notes, reminders, polls, media, speech, weather, prayer times, and more |
90
+ | Group moderation | Welcome messages, anti-link, anti-spam, media rules, warnings, auto-kick, roles, rules, and notes |
91
+ | AI agent | Pick Google Gemini, OpenAI / any OpenAI-compatible server, or Anthropic from the panel; each provider has its own endpoint, API key, and model, with tools for web search, page reading, memory, and role management |
92
+ | Scheduling | One-off, daily, and weekly messages with durable jobs, delivery status, and manual retry |
93
+ | Control panel | Live connection state, command settings, roles, permissions, keys, memory, schedules, and logs |
94
+ | Media | Text, images, video, audio, QR codes, text-to-speech, and speech-to-text |
95
+ | Deployment | npm, Docker, systemd installer, standalone binaries, headless mode, and safe domain setup |
96
+ | Storage | One SQLite database and one data directory for settings, sessions, memory, and logs |
47
97
 
48
- **An AI agent, not a chat box** — `!gemini` can search the web, open a page and
49
- read it, save something to its long-term memory and hand out bot roles, across
50
- several tool rounds, narrating the whole run inside a single message it keeps
51
- editing. Gemini's own Google Search is available to it too, so questions about
52
- current things get grounded answers with their sources listed. Its personality
53
- is a Markdown file you can edit from the panel.
98
+ Every command can be enabled, disabled, renamed, and assigned permissions from
99
+ the panel without restarting the bot.
54
100
 
55
- **Long-term memory** — "remember that…" writes to `memory/global.md` or a
56
- per-chat file. Plain Markdown, hand-editable, injected into every prompt.
101
+ ### AI that can act
57
102
 
58
- **Scheduled messages** — one-off (`!schedule`) or recurring (`!autoschedule`),
59
- including daily and weekly schedules with Arabic or English day names. Jobs
60
- survive restarts; their latest delivery result is shown in the panel, where a
61
- failed message can be retried manually.
103
+ The `!gemini` command is an agent rather than a plain chat box. It can run
104
+ several tool rounds, search the web, read a page, save long-term memories, and
105
+ manage bot roles. It narrates progress by editing one WhatsApp message and lists
106
+ sources when it uses web search.
62
107
 
63
- **Optional proxy** — Settings → WhatsApp proxy routes the WhatsApp connection
64
- through an HTTP, HTTPS or SOCKS5 proxy, including the media it sends and
65
- receives. Nothing else changes: the control panel and the AI still connect
66
- directly.
108
+ AI is optional. The rest of Levix works without an AI key.
67
109
 
68
- **A connection you control** — the panel starts, stops and unlinks the
69
- WhatsApp session, and shows what it is actually doing: waiting for a scan,
70
- connected, reconnecting (5s, 10s, 15s, 20s, 25s), or stopped. Reconnects happen
71
- in the bot whether or not a browser is open, and a WhatsApp connection that will
72
- not come back never takes the panel down with it.
110
+ ### Persistent schedules and memory
73
111
 
74
- **A panel you can also do without** — `levix headless` runs the bot with no
75
- Express, no socket.io and no port open at all; having no screen to press Start
76
- on, it connects by itself and prints its QR straight to the terminal.
112
+ One-off and recurring schedules survive restarts and record their latest
113
+ delivery outcome. Long-term memory is stored as plain Markdown in
114
+ `memory/global.md` or per-chat files, so it remains readable and editable
115
+ outside the panel.
77
116
 
78
- **A domain, without a takeover** — `levix domain bot.example.com` looks at what
79
- the server already runs and works with it: it adds one nginx or Caddy site and
80
- validates the whole configuration before reloading, and on a panel-managed or
81
- containerised host it changes nothing and prints the exact reverse-proxy
82
- settings instead.
117
+ ### A connection you control
83
118
 
84
- ---
119
+ The panel can start, stop, reconnect, and unlink WhatsApp while showing the
120
+ actual state: idle, waiting for a scan, connected, reconnecting, or stopped.
121
+ Reconnect attempts continue inside the bot even when no browser is open.
85
122
 
86
- ## How it is put together
123
+ An optional HTTP, HTTPS, or SOCKS5 proxy can route WhatsApp traffic, including
124
+ media, without changing how the panel or AI connects.
87
125
 
88
- - **Node 24+**, ES modules and CommonJS side by side (see `CLAUDE.md`).
89
- - **[Baileys v7](https://github.com/WhiskeySockets/Baileys)** for WhatsApp,
90
- including the LID system.
91
- - **SQLite through `node:sqlite`** — Node's own module, so the whole datastore
92
- is one file and zero dependencies. No database server, nothing to compile.
93
- - **Express + EJS** for the panel. No framework, no CDN, no build step.
94
- - **Google Gemini** for the AI, with an optional Groq fallback.
126
+ ## CLI
95
127
 
96
- Three things follow from that, and they are the point of the design:
128
+ ```text
129
+ levix start Levix with the web panel
130
+ levix headless start Levix with no web UI or open port
131
+ levix where print the data directory
132
+ levix reset-password reset the panel password
133
+ levix domain [name] configure a domain safely (may need sudo)
134
+ ```
135
+
136
+ A fresh panel install waits for you to start the first WhatsApp pairing.
137
+ `levix headless` has no button to press, so it starts the session itself and
138
+ prints the QR in the terminal when needed.
139
+
140
+ ## Data and configuration
141
+
142
+ Everything mutable lives in one data directory:
143
+
144
+ - npm installation: `~/.levix`
145
+ - source checkout: `./data`
146
+ - Docker: the `levix-data` volume
97
147
 
98
- **The database is the source of truth, the panel is how you edit it.** There is
99
- no `.env` file and no config file — the prefix, every permission, every API key
100
- and every tuning value is a row in `bot_settings`, and every read goes through
101
- an accessor at call time so a change applies to the next message.
148
+ Copying this directory is a full backup, including the SQLite database, WhatsApp
149
+ session, memory, and logs.
102
150
 
103
- **Secrets are generated, not configured.** The session signing key is made on
104
- first start. The panel password is chosen once, in the browser, and stored as
105
- an scrypt hash.
151
+ The database is the source of truth. The prefix, permissions, API keys, server
152
+ settings, and feature options are stored in `bot_settings` and read at use
153
+ time. There is no `.env` or application configuration file to keep in sync.
106
154
 
107
- **Everything mutable lives in one directory** — database, WhatsApp session,
108
- memory files, logs. `levix where` prints it; copying it is a full backup.
155
+ Secrets are generated rather than shipped with defaults. The panel password is
156
+ stored as an scrypt hash, and the session-signing key is generated on first
157
+ start.
109
158
 
110
- ---
159
+ ## Architecture
111
160
 
112
- ## For developers
161
+ - **Node.js 24+** with ES modules and CommonJS where needed.
162
+ - **Baileys v7** for the WhatsApp connection and LID support.
163
+ - **`node:sqlite`** for a single-file datastore with no database server or
164
+ native SQLite dependency.
165
+ - **Express + EJS** for a panel with no frontend build step or CDN dependency.
166
+ - **Pluggable AI providers** — Google Gemini, OpenAI / any OpenAI-compatible endpoint, or
167
+ Anthropic — each with its own endpoint, API key, and model in the control panel, no env file.
168
+ - **GitHub Actions** validation for tests, npm packaging, Docker persistence,
169
+ and standalone executables on Linux, macOS, and Windows.
170
+
171
+ [AGENTS.md](AGENTS.md) documents the module layout, message flow, storage API,
172
+ and architectural constraints. [PACKAGING.md](PACKAGING.md) covers npm, Docker,
173
+ systemd, installers, and single-executable releases.
174
+
175
+ ## Development
113
176
 
114
177
  ```bash
115
178
  git clone https://github.com/Abdodiab2005/levix
116
179
  cd levix
117
- npm install
118
- npm start # data lands in ./data
180
+ npm ci
181
+ npm test
182
+ npm start
119
183
  ```
120
184
 
121
- `CLAUDE.md` is the architecture document: module layout, the storage API, the
122
- message flow, and the rules a change has to respect. `PACKAGING.md` covers
123
- shipping it — npm, Docker, systemd, and the single-executable build.
185
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a substantial
186
+ change. Bug reports and focused pull requests are welcome.
187
+
188
+ For security issues, follow [SECURITY.md](SECURITY.md) and report them
189
+ privately rather than opening a public issue.
124
190
 
125
191
  ## License
126
192
 
127
- MIT.
193
+ Levix is free and open source under the [MIT License](LICENSE).
194
+
195
+ Built by [Abdelrhman Diab](https://github.com/Abdodiab2005) under
196
+ [Leviro](https://leviro.net).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -18,6 +18,9 @@
18
18
  "bot",
19
19
  "baileys",
20
20
  "gemini",
21
+ "openai",
22
+ "anthropic",
23
+ "openai-compatible",
21
24
  "ai-agent",
22
25
  "sqlite",
23
26
  "self-hosted"
@@ -1167,7 +1167,7 @@
1167
1167
  ${setting.choices
1168
1168
  .map(
1169
1169
  (choice) =>
1170
- `<option value="${esc(choice)}"${choice === setting.value ? " selected" : ""}>${esc(choice)}</option>`
1170
+ `<option value="${esc(choice)}"${choice === setting.value ? " selected" : ""}>${esc(setting.choiceLabels?.[choice] || choice)}</option>`
1171
1171
  )
1172
1172
  .join("")}
1173
1173
  </select>
@@ -6,8 +6,12 @@
6
6
  // * the multi-message context buffer (`!gemini add` ... `!gemini send`),
7
7
  // * image generation (`!generate`),
8
8
  // * memory management shortcuts (`!del`, `!delall`),
9
- // * the fallbacks: sanitize-and-retry when an uploaded file URI expired, and
10
- // Groq when Gemini is rate limited.
9
+ // * the fallback: sanitize-and-retry when an uploaded file URI expired.
10
+ //
11
+ // The active provider itself (gemini / openai / anthropic) is a setting, and
12
+ // the loop it dispatches to lives in services/aiAgent.cjs +
13
+ // services/aiProviders.cjs. Media and !generate/!stt stay Gemini-only — the
14
+ // other providers have no Files API to upload to.
11
15
  //
12
16
  // What moved out:
13
17
  // * the system prompt -> config/ai-persona.md (editable, hot-reloaded)
@@ -21,7 +25,6 @@
21
25
  const { GoogleGenAI } = require("@google/genai");
22
26
 
23
27
  const logger = require("../utils/logger.cjs");
24
- const aiIdentity = require("../config/ai-identity.cjs");
25
28
  const settings = require("../config/settings.cjs");
26
29
  const {
27
30
  getChatHistoryAsync,
@@ -41,6 +44,7 @@ const {
41
44
  formatSources,
42
45
  isFileReferenceError,
43
46
  sanitizeHistoryForFiles,
47
+ activeProviderKeySetting,
44
48
  } = require("../services/aiAgent.cjs");
45
49
  const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
46
50
  const fs = require("fs").promises;
@@ -69,79 +73,34 @@ function setBuffer(msg, entries) {
69
73
  // One client does everything now: @google/genai folded the separate
70
74
  // GoogleAIFileManager into `ai.files`, and the model is named per request
71
75
  // instead of being baked into a model object.
72
- let geminiCache = { key: null, genAI: null };
76
+ let geminiCache = { key: null, baseUrl: null, genAI: null };
73
77
 
74
78
  function geminiClients() {
75
79
  const key = settings.get("gemini_api_key");
76
80
  if (!key) return { genAI: null };
77
- if (geminiCache.key !== key) {
78
- geminiCache = { key, genAI: new GoogleGenAI({ apiKey: key }) };
81
+ const baseUrl = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
82
+ if (geminiCache.key !== key || geminiCache.baseUrl !== baseUrl) {
83
+ geminiCache = {
84
+ key,
85
+ baseUrl,
86
+ genAI: new GoogleGenAI({ apiKey: key, httpOptions: { baseUrl } }),
87
+ };
79
88
  }
80
89
  return geminiCache;
81
90
  }
82
91
 
83
- function isGeminiRateLimitError(error) {
84
- const message = `${error?.message || ""} ${error?.details || ""}`.toLowerCase();
85
- // @google/genai's ApiError carries a numeric `status`; the older shapes are
86
- // kept because they cost nothing and a queued error can predate an upgrade.
87
- const status =
88
- error?.status ||
89
- error?.statusCode ||
90
- error?.code ||
91
- error?.response?.status ||
92
- error?.error?.code;
93
-
94
- return (
95
- status === 429 ||
96
- message.includes("429") ||
97
- message.includes("rate limit") ||
98
- message.includes("resource_exhausted") ||
99
- message.includes("quota")
100
- );
101
- }
102
-
103
- async function getGroqFallbackResponse(parts) {
104
- const groqKey = settings.get("groq_api_key");
105
- if (!groqKey) throw new Error("GROQ_API_KEY is not configured");
106
-
107
- const mergedPrompt = parts
108
- .map((part) => {
109
- if (part?.text) return part.text;
110
- if (part?.fileData) return "[تم إرفاق ملف/وسائط في الرسالة]";
111
- return "";
112
- })
113
- .filter(Boolean)
114
- .join("\n");
115
-
116
- const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
117
- method: "POST",
118
- headers: {
119
- Authorization: `Bearer ${groqKey}`,
120
- "Content-Type": "application/json",
121
- },
122
- body: JSON.stringify({
123
- model: settings.get("groq_model"),
124
- messages: [
125
- {
126
- role: "system",
127
- // Same code-owned identity the Gemini path gets — the fallback must
128
- // not answer "who made you?" differently from the main model.
129
- content: `${aiIdentity.systemBlock}\n\nBe brief, direct and conversational. Answer in the language the user wrote in.`,
130
- },
131
- { role: "user", content: mergedPrompt },
132
- ],
133
- temperature: 0.7,
134
- }),
135
- });
136
-
137
- const payload = await response.json();
138
- if (!response.ok) {
139
- throw new Error(payload?.error?.message || "Groq API request failed");
92
+ async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
93
+ // Reading media is a Gemini capability: the Files API it uploads to does not
94
+ // exist on the openai/anthropic paths. There the turn carries a note and the
95
+ // caption/question text still reaches the model.
96
+ if (settings.get("ai_provider") !== "gemini") {
97
+ const mime = mimeOverride || mediaMessage.mimetype || "ملف";
98
+ parts.push({
99
+ text: `[تم إرفاق وسائط (${mime}) — المزود الحالي لا يستطيع قراءة الوسائط]`,
100
+ });
101
+ return null;
140
102
  }
141
- return payload?.choices?.[0]?.message?.content?.trim();
142
- }
143
103
 
144
- async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
145
104
  const { genAI } = geminiClients();
146
105
  if (!genAI) throw new Error("Gemini client unavailable");
147
106
  const tempFilePath = path.join(__dirname, `temp_media_${Date.now()}`);
@@ -392,6 +351,14 @@ module.exports = {
392
351
  );
393
352
 
394
353
  try {
354
+ // Image generation needs a model that returns image bytes, which only
355
+ // the Gemini image models do — it stays on Gemini whatever the chat
356
+ // provider is.
357
+ if (!geminiClients().genAI) {
358
+ throw new Error(
359
+ "مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط"
360
+ );
361
+ }
395
362
  const fullPrompt =
396
363
  imagePrompt && quotedMsg
397
364
  ? `Prompt: ${imagePrompt} ,using quote: ${quotedMsg}`
@@ -636,11 +603,16 @@ module.exports = {
636
603
  }
637
604
  }
638
605
 
639
- if (!geminiClients().genAI) {
606
+ // Provider-aware, on purpose: when the panel picked openai or anthropic,
607
+ // the bot answers with that key and a missing Gemini key is irrelevant.
608
+ if (!settings.get(activeProviderKeySetting())) {
609
+ const provider = settings.get("ai_provider");
640
610
  return sendBotMessage(
641
611
  sock,
642
612
  chatId,
643
- { text: "خطأ في الإعدادات: مفتاح Gemini API غير معرف." },
613
+ {
614
+ text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${provider}) غير معرف — عدّله من لوحة التحكم.`,
615
+ },
644
616
  { replyTo: msg }
645
617
  );
646
618
  }
@@ -718,26 +690,6 @@ module.exports = {
718
690
  await status.finish(`${text}${formatSources(result.sources)}`);
719
691
  } catch (error) {
720
692
  logger.error({ err: error }, "Error in !gemini command");
721
-
722
- // Rate-limit fallback to Groq for text-only prompts.
723
- if (isGeminiRateLimitError(error) && settings.get("groq_api_key")) {
724
- if (parts.some((part) => part?.fileData)) {
725
- await status.finish(
726
- "جيميني عليه ضغط حالياً، ومش قادر أعالج الوسائط دلوقتي. حاول بعد شوية 🙏"
727
- );
728
- return;
729
- }
730
- try {
731
- const groqResponse = await getGroqFallbackResponse(parts);
732
- if (groqResponse) {
733
- await status.finish(groqResponse);
734
- return;
735
- }
736
- } catch (groqError) {
737
- logger.error({ err: groqError }, "Groq fallback failed");
738
- }
739
- }
740
-
741
693
  await status.fail(error, "حصلت مشكلة وأنا بكلم الذكاء الاصطناعي");
742
694
  }
743
695
  },
@@ -13,13 +13,18 @@ const settings = require("../config/settings.cjs");
13
13
  //
14
14
  // One @google/genai client covers both halves: `ai.files` replaced the separate
15
15
  // GoogleAIFileManager, and the model is named per request.
16
- let cache = { key: null, genAI: null };
16
+ let cache = { key: null, baseUrl: null, genAI: null };
17
17
 
18
18
  function geminiStt() {
19
19
  const key = settings.get("gemini_api_key");
20
20
  if (!key) return null;
21
- if (cache.key !== key) {
22
- cache = { key, genAI: new GoogleGenAI({ apiKey: key }) };
21
+ const baseUrl = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
22
+ if (cache.key !== key || cache.baseUrl !== baseUrl) {
23
+ cache = {
24
+ key,
25
+ baseUrl,
26
+ genAI: new GoogleGenAI({ apiKey: key, httpOptions: { baseUrl } }),
27
+ };
23
28
  }
24
29
  return cache;
25
30
  }
@@ -48,13 +48,40 @@ const SETTINGS = [
48
48
  },
49
49
 
50
50
  // --- AI ----------------------------------------------------------------
51
+ //
52
+ // One provider answers chats, chosen here and configured right below: the
53
+ // dashboard renders every one of these fields (the provider as a dropdown,
54
+ // keys as password fields), so a provider switch is a settings change, not a
55
+ // code change or an env file.
56
+ {
57
+ key: "ai_provider",
58
+ type: "string",
59
+ default: "gemini",
60
+ choices: ["gemini", "openai", "anthropic"],
61
+ choiceLabels: {
62
+ gemini: "Google Gemini",
63
+ openai: "OpenAI / compatible",
64
+ anthropic: "Anthropic Claude",
65
+ },
66
+ group: "ai",
67
+ label: "AI provider",
68
+ hint: "Which API answers chats. gemini = Google Gemini (the only one that reads media and has built-in Google Search). openai = any OpenAI-compatible chat-completions server. anthropic = Claude.",
69
+ },
51
70
  {
52
71
  key: "gemini_api_key",
53
72
  type: "secret",
54
73
  default: "",
55
74
  group: "ai",
56
75
  label: "Gemini API key",
57
- hint: "Free key from aistudio.google.com/apikey — without it the AI commands stay off.",
76
+ hint: "Free key from aistudio.google.com/apikey. Needed when the provider is gemini, and by !stt / !generate in every case.",
77
+ },
78
+ {
79
+ key: "gemini_base_url",
80
+ type: "string",
81
+ default: "https://generativelanguage.googleapis.com",
82
+ group: "ai",
83
+ label: "Google Gemini base URL",
84
+ hint: "Google Generative Language API root. Change it for a proxy or compatible gateway; do not append /v1beta. Applies live to chat, media uploads, !stt and !generate.",
58
85
  },
59
86
  {
60
87
  key: "gemini_model",
@@ -131,22 +158,54 @@ const SETTINGS = [
131
158
  default: true,
132
159
  group: "ai",
133
160
  label: "Gemini Google Search",
134
- hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the Groq fallback is unaffected.",
161
+ hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the openai and anthropic providers never see it.",
162
+ },
163
+ {
164
+ key: "openai_api_key",
165
+ type: "secret",
166
+ default: "",
167
+ group: "ai",
168
+ label: "OpenAI-compatible API key",
169
+ hint: "Used when the provider is openai. Works with OpenAI itself or any server that speaks the same format: OpenRouter, Together, LM Studio, ...",
135
170
  },
136
171
  {
137
- key: "groq_api_key",
172
+ key: "openai_base_url",
173
+ type: "string",
174
+ default: "https://api.openai.com/v1",
175
+ group: "ai",
176
+ label: "OpenAI-compatible base URL",
177
+ hint: "The /v1 root of a chat-completions server. Examples: https://openrouter.ai/api/v1, https://api.together.xyz/v1, http://127.0.0.1:11434/v1 (Ollama).",
178
+ },
179
+ {
180
+ key: "openai_model",
181
+ type: "string",
182
+ default: "gpt-4o-mini",
183
+ group: "ai",
184
+ label: "OpenAI-compatible model",
185
+ hint: "Must exist on the server the base URL points at (e.g. gpt-4o-mini on OpenAI, a llama model on a self-hosted server).",
186
+ },
187
+ {
188
+ key: "anthropic_api_key",
138
189
  type: "secret",
139
190
  default: "",
140
191
  group: "ai",
141
- label: "Groq API key (fallback)",
142
- hint: "Used only when Gemini is out of quota.",
192
+ label: "Anthropic API key",
193
+ hint: "Used when the provider is anthropic. Create one at console.anthropic.com.",
194
+ },
195
+ {
196
+ key: "anthropic_base_url",
197
+ type: "string",
198
+ default: "https://api.anthropic.com",
199
+ group: "ai",
200
+ label: "Anthropic base URL",
201
+ hint: "The host root — /v1/messages is appended. Change it only for a proxy or a compatible gateway.",
143
202
  },
144
203
  {
145
- key: "groq_model",
204
+ key: "anthropic_model",
146
205
  type: "string",
147
- default: "llama-3.3-70b-versatile",
206
+ default: "claude-sonnet-4-5",
148
207
  group: "ai",
149
- label: "Groq model",
208
+ label: "Anthropic model",
150
209
  },
151
210
  {
152
211
  key: "google_search_api_key",
@@ -466,6 +525,7 @@ function describe() {
466
525
  min: definition.min ?? null,
467
526
  max: definition.max ?? null,
468
527
  choices: definition.choices ?? null,
528
+ choiceLabels: definition.choiceLabels ?? null,
469
529
  restart: definition.restart === true,
470
530
  source: sourceOf(definition.key),
471
531
  };
package/src/core/proxy.js CHANGED
@@ -25,9 +25,9 @@
25
25
  // WHAT IS NOT PROXIED
26
26
  // -------------------
27
27
  // Nothing global is patched. The agents are handed to one `makeWASocket()`
28
- // call by the session manager, so the dashboard's own HTTP server, Gemini,
29
- // Groq and every other outbound request keep using the direct connection they
30
- // use today.
28
+ // call by the session manager, so the dashboard's own HTTP server, the AI
29
+ // providers and every other outbound request keep using the direct connection
30
+ // they use today.
31
31
  //
32
32
  // SECRETS
33
33
  // -------