tiger-agent 0.2.0 → 0.2.1
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 +95 -56
- package/package.json +1 -1
- package/src/agent/mainAgent.js +12 -11
- package/src/apiProviders.js +9 -4
- package/src/config.js +1 -9
- package/src/llmClient.js +47 -31
- package/src/telegram/bot.js +53 -5
- package/src/tokenManager.js +16 -0
package/README.md
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
# 🐯 Tiger Agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/tiger-agent)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
**Cognitive AI Agent** with persistent long-term memory, multi-provider LLM support, self-learning, and Telegram bot integration — designed for 24/7 autonomous operation on Linux.
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Made by **AI Research Group, Department of Civil Engineering, KMUTT**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🆕 What's New — v0.2.0
|
|
14
|
+
|
|
15
|
+
- **npm global install** — `npm install -g tiger-agent`, no git clone needed
|
|
16
|
+
- **Multi-provider LLM** — 5 providers (Kimi, Z.ai, MiniMax, Claude, Moonshot) with auto-fallback
|
|
17
|
+
- **Daily token limits** — per-provider limits with automatic switching at UTC midnight
|
|
18
|
+
- **`tiger` CLI** — unified command: `tiger onboard`, `tiger start`, `tiger telegram`, `tiger stop`
|
|
19
|
+
- **Telegram `/api` & `/tokens`** — switch providers and monitor usage from chat
|
|
20
|
+
- **Encrypted secrets** — optional at-rest encryption for API keys
|
|
6
21
|
|
|
7
22
|
---
|
|
8
23
|
|
|
9
24
|
## 🎯 Why Tiger?
|
|
10
25
|
|
|
11
|
-
| Feature | Tiger
|
|
12
|
-
|
|
26
|
+
| Feature | Tiger | Generic AI Assistants |
|
|
27
|
+
|---------|-------|-----------------------|
|
|
13
28
|
| **Memory** | Persistent lifetime memory (Vector DB) | Forgets when session ends |
|
|
14
29
|
| **Learning** | Self-training every 12 hours | Static, never improves |
|
|
15
30
|
| **Security** | Audit logs + Encryption + Hardened perms | No audit trail |
|
|
@@ -24,24 +39,22 @@ Tiger is a **Cognitive AI Agent** with persistent long-term memory, multi-provid
|
|
|
24
39
|
npm install -g tiger-agent
|
|
25
40
|
```
|
|
26
41
|
|
|
27
|
-
No git clone needed.
|
|
28
|
-
|
|
29
42
|
---
|
|
30
43
|
|
|
31
44
|
## 🚀 Quick Start
|
|
32
45
|
|
|
33
46
|
```bash
|
|
34
|
-
tiger onboard # First-time setup wizard
|
|
47
|
+
tiger onboard # First-time setup wizard (run once)
|
|
35
48
|
tiger start # Start CLI chat
|
|
36
49
|
```
|
|
37
50
|
|
|
38
51
|
CLI exit: `/exit` or `/quit`
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
**Telegram bot:**
|
|
41
54
|
|
|
42
55
|
```bash
|
|
43
|
-
tiger telegram #
|
|
44
|
-
tiger telegram --background #
|
|
56
|
+
tiger telegram # Start in foreground
|
|
57
|
+
tiger telegram --background # Start as background daemon
|
|
45
58
|
tiger stop # Stop daemon
|
|
46
59
|
tiger status # Check daemon status
|
|
47
60
|
```
|
|
@@ -58,13 +71,13 @@ tiger status # Check daemon status
|
|
|
58
71
|
|
|
59
72
|
## 🎮 Run Modes
|
|
60
73
|
|
|
61
|
-
| Mode |
|
|
62
|
-
|
|
63
|
-
| **CLI** | `tiger start` |
|
|
64
|
-
| **Telegram** | `tiger telegram` |
|
|
65
|
-
| **Background** | `tiger telegram --background` |
|
|
66
|
-
| **Stop** | `tiger stop` |
|
|
67
|
-
| **Status** | `tiger status` |
|
|
74
|
+
| Mode | Command | Use Case |
|
|
75
|
+
|------|---------|----------|
|
|
76
|
+
| **CLI** | `tiger start` | Interactive terminal chat |
|
|
77
|
+
| **Telegram** | `tiger telegram` | Telegram bot (foreground) |
|
|
78
|
+
| **Background** | `tiger telegram --background` | 24/7 daemon |
|
|
79
|
+
| **Stop** | `tiger stop` | Kill background daemon |
|
|
80
|
+
| **Status** | `tiger status` | Check if daemon is running |
|
|
68
81
|
|
|
69
82
|
Background logs: `~/.tiger/logs/telegram-supervisor.log`
|
|
70
83
|
|
|
@@ -72,12 +85,13 @@ Background logs: `~/.tiger/logs/telegram-supervisor.log`
|
|
|
72
85
|
|
|
73
86
|
## 🔧 Setup Wizard
|
|
74
87
|
|
|
75
|
-
`tiger onboard`
|
|
88
|
+
`tiger onboard` creates and configures:
|
|
76
89
|
|
|
77
|
-
- `~/.tiger/.env` —
|
|
78
|
-
- `~/.tiger/.env.secrets` — API keys
|
|
90
|
+
- `~/.tiger/.env` — settings and provider config
|
|
91
|
+
- `~/.tiger/.env.secrets` — API keys (mode 600, gitignored)
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
Setup options:
|
|
94
|
+
- Choose LLM provider and API keys
|
|
81
95
|
- Persistent vs temporary vector DB
|
|
82
96
|
- Optional `sqlite-vec` acceleration
|
|
83
97
|
- Optional encrypted secrets file
|
|
@@ -95,6 +109,13 @@ Options during setup:
|
|
|
95
109
|
| `ALLOW_SKILL_INSTALL` | `false` | Enable ClawHub skill install |
|
|
96
110
|
| `VECTOR_DB_PATH` | `~/.tiger/db/memory.sqlite` | SQLite vector DB path |
|
|
97
111
|
| `DATA_DIR` | `~/.tiger/data` | Context files directory |
|
|
112
|
+
| `OWN_SKILL_UPDATE_HOURS` | `24` | Hours between `ownskill.md` regenerations (min 1) |
|
|
113
|
+
| `SOUL_UPDATE_HOURS` | `24` | Hours between `soul.md` regenerations (min 1) |
|
|
114
|
+
| `REFLECTION_UPDATE_HOURS` | `12` | Hours between reflection cycles (min 1) |
|
|
115
|
+
| `MEMORY_INGEST_EVERY_TURNS` | `2` | Ingest durable memory every N conversation turns |
|
|
116
|
+
| `MEMORY_INGEST_MIN_CHARS` | `140` | Minimum combined chars in a turn to trigger memory ingest |
|
|
117
|
+
|
|
118
|
+
Config lives in `~/.tiger/.env` after running `tiger onboard`.
|
|
98
119
|
|
|
99
120
|
---
|
|
100
121
|
|
|
@@ -104,15 +125,15 @@ Tiger supports **5 providers** with automatic fallback and daily token limits.
|
|
|
104
125
|
|
|
105
126
|
### Supported Providers
|
|
106
127
|
|
|
107
|
-
| Provider | ID | Default Model | API Key |
|
|
108
|
-
|
|
128
|
+
| Provider | ID | Default Model | API Key Variable |
|
|
129
|
+
|----------|----|--------------|-----------------|
|
|
109
130
|
| Kimi Code | `kimi` | `k2p5` | `KIMI_CODE_API_KEY` |
|
|
110
131
|
| Kimi Moonshot | `moonshot` | `kimi-k1` | `MOONSHOT_API_KEY` |
|
|
111
|
-
| Z.ai (Zhipu) | `zai` | `glm-
|
|
132
|
+
| Z.ai (Zhipu) | `zai` | `glm-4.7` | `ZAI_API_KEY` (format: `id.secret`) |
|
|
112
133
|
| MiniMax | `minimax` | `abab6.5s-chat` | `MINIMAX_API_KEY` |
|
|
113
134
|
| Claude (Anthropic) | `claude` | `claude-sonnet-4-6` | `CLAUDE_API_KEY` |
|
|
114
135
|
|
|
115
|
-
### `.env`
|
|
136
|
+
### `.env` Example
|
|
116
137
|
|
|
117
138
|
```env
|
|
118
139
|
ACTIVE_PROVIDER=zai
|
|
@@ -137,8 +158,8 @@ MOONSHOT_TOKEN_LIMIT=100000
|
|
|
137
158
|
1. Uses `ACTIVE_PROVIDER` for all requests
|
|
138
159
|
2. On **429** (rate limit) or **403** (quota exceeded) — switches to next in `PROVIDER_ORDER`
|
|
139
160
|
3. When a provider's daily token limit is reached — skipped for the rest of the day
|
|
140
|
-
4. Providers with no API key are silently skipped
|
|
141
|
-
5. Token usage
|
|
161
|
+
4. Providers with no API key configured are silently skipped
|
|
162
|
+
5. Token usage resets at UTC midnight (`~/.tiger/db/token_usage.json`)
|
|
142
163
|
|
|
143
164
|
---
|
|
144
165
|
|
|
@@ -147,44 +168,60 @@ MOONSHOT_TOKEN_LIMIT=100000
|
|
|
147
168
|
| Command | Description |
|
|
148
169
|
|---------|-------------|
|
|
149
170
|
| `/api` | Show all providers with token usage |
|
|
150
|
-
| `/api <id>` | Switch provider (e.g. `/api claude`) |
|
|
171
|
+
| `/api <id>` | Switch active provider (e.g. `/api claude`) |
|
|
151
172
|
| `/tokens` | Show today's token usage per provider |
|
|
173
|
+
| `/limit` | Show daily token limits per provider |
|
|
174
|
+
| `/limit <provider> <n>` | Set daily token limit (0 = unlimited, e.g. `/limit zai 100000`) |
|
|
152
175
|
| `/help` | Show all commands |
|
|
153
176
|
|
|
154
177
|
---
|
|
155
178
|
|
|
156
179
|
## 🧠 Memory & Context
|
|
157
180
|
|
|
158
|
-
Context
|
|
181
|
+
### Context Files
|
|
182
|
+
|
|
183
|
+
Loaded on every turn from `~/.tiger/data/`:
|
|
159
184
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
| File | Purpose |
|
|
186
|
+
|------|---------|
|
|
187
|
+
| `soul.md` | Agent identity, principles, and stable preferences |
|
|
188
|
+
| `human.md` | User profile — goals, patterns, preferences |
|
|
189
|
+
| `human2.md` | Running update log written after every conversation turn |
|
|
190
|
+
| `ownskill.md` | Known skills, workflows, and lessons learned |
|
|
163
191
|
|
|
164
|
-
Auto-
|
|
192
|
+
### Auto-Refresh Cycles
|
|
165
193
|
|
|
166
|
-
|
|
167
|
-
|-------|----------|---------|
|
|
168
|
-
| Skill summary | `OWN_SKILL_UPDATE_HOURS` | 24h |
|
|
169
|
-
| Soul refresh | `SOUL_UPDATE_HOURS` | 24h |
|
|
170
|
-
| Reflection | `REFLECTION_UPDATE_HOURS` | 12h |
|
|
171
|
-
| Memory ingest | `MEMORY_INGEST_EVERY_TURNS` | every N turns |
|
|
194
|
+
Tiger periodically regenerates these files using the LLM. All durations are configurable in `.env` (minimum 1 hour).
|
|
172
195
|
|
|
173
|
-
|
|
196
|
+
| Cycle | `.env` Variable | Default | What It Does |
|
|
197
|
+
|-------|----------------|---------|--------------|
|
|
198
|
+
| **Skill summary** | `OWN_SKILL_UPDATE_HOURS` | `24` | Rewrites `ownskill.md` with updated skills, workflows, and lessons derived from recent conversations |
|
|
199
|
+
| **Soul refresh** | `SOUL_UPDATE_HOURS` | `24` | Rewrites `soul.md` to reflect any evolved identity, operating rules, or preferences |
|
|
200
|
+
| **Reflection** | `REFLECTION_UPDATE_HOURS` | `12` | Extracts long-term memory bullets from recent messages and appends them to `soul.md`, `human.md`, `ownskill.md`, and the vector DB |
|
|
201
|
+
| **Memory ingest** | `MEMORY_INGEST_EVERY_TURNS` | `2` | After every N conversation turns, distils durable preference or workflow facts into the vector DB |
|
|
202
|
+
|
|
203
|
+
> **Note:** Refresh timers for `soul.md` and `ownskill.md` are tracked in the DB (not file modification time), so reflection appends do not reset the 24-hour clock.
|
|
204
|
+
|
|
205
|
+
Example `.env` — tighten cycles for an active bot:
|
|
174
206
|
|
|
175
|
-
Optional `sqlite-vec` acceleration:
|
|
176
207
|
```env
|
|
177
|
-
|
|
208
|
+
OWN_SKILL_UPDATE_HOURS=12
|
|
209
|
+
SOUL_UPDATE_HOURS=12
|
|
210
|
+
REFLECTION_UPDATE_HOURS=6
|
|
211
|
+
MEMORY_INGEST_EVERY_TURNS=2
|
|
212
|
+
MEMORY_INGEST_MIN_CHARS=140
|
|
178
213
|
```
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
### Vector Memory
|
|
216
|
+
|
|
217
|
+
Stored in `~/.tiger/db/memory.sqlite`. Optional `sqlite-vec` extension enables fast ANN search:
|
|
218
|
+
|
|
219
|
+
```env
|
|
220
|
+
SQLITE_VEC_EXTENSION=/path/to/sqlite_vec
|
|
186
221
|
```
|
|
187
222
|
|
|
223
|
+
Without it, Tiger falls back to cosine similarity in Python — slower but fully functional.
|
|
224
|
+
|
|
188
225
|
---
|
|
189
226
|
|
|
190
227
|
## 🛠️ Built-in Tools
|
|
@@ -202,17 +239,19 @@ npm run memory:vec:check # Check sqlite-vec
|
|
|
202
239
|
|
|
203
240
|
| Feature | Detail |
|
|
204
241
|
|---------|--------|
|
|
205
|
-
| **Credential Storage** |
|
|
242
|
+
| **Credential Storage** | `~/.tiger/.env.secrets` with mode 600 |
|
|
206
243
|
| **Database Security** | `~/.tiger/db/` with hardened permissions |
|
|
207
244
|
| **Audit Logging** | Sanitized skill logs at `~/.tiger/logs/audit.log` |
|
|
208
245
|
| **Auto Backup** | Daily SQLite backups, 30-day retention |
|
|
209
246
|
| **Secret Rotation** | Built-in 90-day rotation reminders |
|
|
210
247
|
|
|
211
|
-
### Encrypted Secrets
|
|
248
|
+
### Optional: Encrypted Secrets
|
|
212
249
|
|
|
213
250
|
```bash
|
|
214
|
-
|
|
215
|
-
|
|
251
|
+
# Run from ~/.tiger after onboard
|
|
252
|
+
export SECRETS_PASSPHRASE='your-passphrase'
|
|
253
|
+
node $(npm root -g)/tiger-agent/scripts/encrypt-env.js \
|
|
254
|
+
--in .env.secrets --out .env.secrets.enc
|
|
216
255
|
rm .env.secrets
|
|
217
256
|
```
|
|
218
257
|
|
|
@@ -239,18 +278,18 @@ rm .env.secrets
|
|
|
239
278
|
| Bot stuck on one provider | `/api <name>` in Telegram to switch manually |
|
|
240
279
|
| Provider silently skipped | No API key set, or daily limit reached — check `/tokens` |
|
|
241
280
|
| `401` auth error | Wrong or missing API key |
|
|
242
|
-
| `403` quota error | Daily quota exhausted — auto-switches;
|
|
281
|
+
| `403` quota error | Daily quota exhausted — auto-switches; raise `*_TOKEN_LIMIT` |
|
|
243
282
|
| `429` rate limit | Auto-switches to next provider in `PROVIDER_ORDER` |
|
|
244
283
|
| Z.ai auth fails | Key must be `id.secret` format (from Zhipu/BigModel console) |
|
|
245
|
-
| Shell tool disabled | Set `ALLOW_SHELL=true` in
|
|
246
|
-
| Stuck processes | `pkill -f
|
|
284
|
+
| Shell tool disabled | Set `ALLOW_SHELL=true` in `~/.tiger/.env` |
|
|
285
|
+
| Stuck processes | `pkill -f tiger-agent` then restart |
|
|
247
286
|
| Reset token counters | Delete `~/.tiger/db/token_usage.json` and restart |
|
|
248
287
|
|
|
249
288
|
---
|
|
250
289
|
|
|
251
290
|
## 📁 Data Directory
|
|
252
291
|
|
|
253
|
-
|
|
292
|
+
All runtime data lives in `~/.tiger/`:
|
|
254
293
|
|
|
255
294
|
```
|
|
256
295
|
~/.tiger/
|
package/package.json
CHANGED
package/src/agent/mainAgent.js
CHANGED
|
@@ -124,22 +124,21 @@ function buildSystemPrompt(contextText, memoriesText) {
|
|
|
124
124
|
.join('\n');
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
127
|
+
const OWNSKILL_META_KEY = 'ownskill_last_updated_ts';
|
|
128
|
+
const SOUL_META_KEY = 'soul_last_updated_ts';
|
|
129
|
+
|
|
130
|
+
function shouldRefreshByMeta(metaKey, updateHours) {
|
|
131
|
+
const lastTs = Number(getMeta(metaKey, 0) || 0);
|
|
132
|
+
if (!lastTs) return true;
|
|
133
|
+
return Date.now() - lastTs >= updateHours * 60 * 60 * 1000;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
function shouldRefreshOwnSkill() {
|
|
138
|
-
return
|
|
137
|
+
return shouldRefreshByMeta(OWNSKILL_META_KEY, ownSkillUpdateHours);
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
async function maybeUpdateOwnSkillSummary(conversationIdValue) {
|
|
142
|
-
if (!
|
|
141
|
+
if (!shouldRefreshByMeta(OWNSKILL_META_KEY, ownSkillUpdateHours)) return;
|
|
143
142
|
|
|
144
143
|
const recent = getRecentMessages(conversationIdValue, 80);
|
|
145
144
|
const transcript = recent
|
|
@@ -175,10 +174,11 @@ async function maybeUpdateOwnSkillSummary(conversationIdValue) {
|
|
|
175
174
|
const next = String(message.content || '').trim();
|
|
176
175
|
if (!next) return;
|
|
177
176
|
fs.writeFileSync(path.resolve(ownSkillPath), `${next}\n`, 'utf8');
|
|
177
|
+
setMeta(OWNSKILL_META_KEY, Date.now());
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
async function maybeUpdateSoulSummary(conversationIdValue) {
|
|
181
|
-
if (!
|
|
181
|
+
if (!shouldRefreshByMeta(SOUL_META_KEY, soulUpdateHours)) return;
|
|
182
182
|
|
|
183
183
|
const recent = getRecentMessages(conversationIdValue, 80);
|
|
184
184
|
const transcript = recent
|
|
@@ -210,6 +210,7 @@ async function maybeUpdateSoulSummary(conversationIdValue) {
|
|
|
210
210
|
const next = String(message.content || '').trim();
|
|
211
211
|
if (!next) return;
|
|
212
212
|
fs.writeFileSync(path.resolve(soulPath), `${next}\n`, 'utf8');
|
|
213
|
+
setMeta(SOUL_META_KEY, Date.now());
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
async function maybeIngestTurnMemory(conversationIdValue, userText, assistantText) {
|
package/src/apiProviders.js
CHANGED
|
@@ -155,13 +155,18 @@ function buildProviders(env) {
|
|
|
155
155
|
|
|
156
156
|
zai: {
|
|
157
157
|
id: 'zai',
|
|
158
|
-
name: 'Z.ai
|
|
159
|
-
baseUrl: (env.ZAI_BASE_URL || 'https://
|
|
160
|
-
chatModel: env.ZAI_MODEL || 'glm-
|
|
158
|
+
name: 'Z.ai',
|
|
159
|
+
baseUrl: (env.ZAI_BASE_URL || 'https://api.z.ai/api/coding/paas/v4').replace(/\/$/, ''),
|
|
160
|
+
chatModel: env.ZAI_MODEL || 'glm-4.7',
|
|
161
161
|
embedModel: env.ZAI_EMBED_MODEL || '',
|
|
162
162
|
apiKey: env.ZAI_API_KEY || '',
|
|
163
163
|
userAgent: '',
|
|
164
|
-
|
|
164
|
+
// api.z.ai uses plain Bearer; old bigmodel.cn used Zhipu JWT
|
|
165
|
+
authHeaders: (key) => {
|
|
166
|
+
const baseUrl = (env.ZAI_BASE_URL || '').toLowerCase();
|
|
167
|
+
if (baseUrl.includes('bigmodel.cn')) return { Authorization: `Bearer ${zhipuJwt(key)}` };
|
|
168
|
+
return { Authorization: `Bearer ${key}` };
|
|
169
|
+
},
|
|
165
170
|
chatPath: '/chat/completions',
|
|
166
171
|
embedPath: '/embeddings',
|
|
167
172
|
formatRequest: standardFormat,
|
package/src/config.js
CHANGED
|
@@ -79,16 +79,8 @@ const rawApiKey =
|
|
|
79
79
|
? process.env.KIMI_CODE_API_KEY || process.env.KIMI_API_KEY || ''
|
|
80
80
|
: process.env.MOONSHOT_API_KEY || process.env.KIMI_API_KEY || '';
|
|
81
81
|
const kimiApiKey = cleanEnvValue(rawApiKey);
|
|
82
|
-
// Only hard-fail on missing kimi key when kimi/moonshot is the active provider.
|
|
83
|
-
// If ACTIVE_PROVIDER is set to another provider, the key is optional.
|
|
84
82
|
const activeProvider = cleanEnvValue(process.env.ACTIVE_PROVIDER || '').toLowerCase();
|
|
85
|
-
|
|
86
|
-
if (!kimiApiKey && kimiIsActive) {
|
|
87
|
-
if (kimiProvider === 'code') {
|
|
88
|
-
throw new Error('Missing required env: KIMI_CODE_API_KEY (or KIMI_API_KEY)');
|
|
89
|
-
}
|
|
90
|
-
throw new Error('Missing required env: MOONSHOT_API_KEY (or KIMI_API_KEY)');
|
|
91
|
-
}
|
|
83
|
+
// Missing API keys are allowed — providers with no key are silently skipped at request time.
|
|
92
84
|
|
|
93
85
|
const defaultBaseUrl =
|
|
94
86
|
kimiProvider === 'code' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1';
|
package/src/llmClient.js
CHANGED
|
@@ -21,39 +21,56 @@ const tokenManager = require('./tokenManager');
|
|
|
21
21
|
|
|
22
22
|
// ─── Low-level fetch wrapper ─────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fetchProvider(provider, endpoint, body, maxRetries = 3) {
|
|
25
29
|
const key = provider.apiKey;
|
|
26
30
|
const headers = { 'Content-Type': 'application/json', ...provider.authHeaders(key) };
|
|
27
31
|
if (provider.userAgent) headers['User-Agent'] = provider.userAgent;
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
let lastErr;
|
|
34
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
35
|
+
if (attempt > 0) {
|
|
36
|
+
const delay = Math.min(attempt * 2000, 10000); // 2s, 4s, 6s … capped at 10s
|
|
37
|
+
process.stderr.write(`[llm] 429 on ${provider.name}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})\n`);
|
|
38
|
+
await sleep(delay);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const timeout = provider.timeout || 30000;
|
|
42
|
+
const ctrl = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => ctrl.abort(), timeout);
|
|
44
|
+
|
|
45
|
+
let res;
|
|
46
|
+
try {
|
|
47
|
+
res = await fetch(`${provider.baseUrl}${endpoint}`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers,
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
signal: ctrl.signal
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (err && err.name === 'AbortError') {
|
|
56
|
+
throw Object.assign(new Error(`Timeout after ${timeout}ms (${provider.name})`), { status: 0 });
|
|
57
|
+
}
|
|
58
|
+
throw Object.assign(new Error(`Network error (${provider.name}): ${err.message}`), { status: 0 });
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const text = await res.text().catch(() => '');
|
|
65
|
+
lastErr = Object.assign(new Error(`HTTP ${res.status} from ${provider.name}: ${text}`), { status: res.status });
|
|
66
|
+
if (res.status === 429 && attempt < maxRetries) continue; // retry on rate limit
|
|
67
|
+
throw lastErr;
|
|
45
68
|
}
|
|
46
|
-
throw Object.assign(new Error(`Network error (${provider.name}): ${err.message}`), { status: 0 });
|
|
47
|
-
} finally {
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
}
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
const text = await res.text().catch(() => '');
|
|
53
|
-
throw Object.assign(new Error(`HTTP ${res.status} from ${provider.name}: ${text}`), { status: res.status });
|
|
70
|
+
return res.json();
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
|
|
73
|
+
throw lastErr;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
// ─── chatCompletion ──────────────────────────────────────────────────────────
|
|
@@ -63,7 +80,7 @@ async function chatCompletion(messages, options = {}) {
|
|
|
63
80
|
const activeId = tokenManager.getCurrentProvider();
|
|
64
81
|
const candidates = [activeId, ...tokenManager.getNextCandidates(activeId)];
|
|
65
82
|
|
|
66
|
-
let
|
|
83
|
+
let firstError = null;
|
|
67
84
|
|
|
68
85
|
for (const providerId of candidates) {
|
|
69
86
|
if (!providerId) continue;
|
|
@@ -81,7 +98,7 @@ async function chatCompletion(messages, options = {}) {
|
|
|
81
98
|
try {
|
|
82
99
|
data = await fetchProvider(provider, provider.chatPath, body);
|
|
83
100
|
} catch (err) {
|
|
84
|
-
|
|
101
|
+
if (!firstError) firstError = err; // keep the active provider's error
|
|
85
102
|
|
|
86
103
|
// 429 = rate limit, 403 = quota exhausted — both warrant a fallback
|
|
87
104
|
if (err.status === 429 || err.status === 403) {
|
|
@@ -93,9 +110,8 @@ async function chatCompletion(messages, options = {}) {
|
|
|
93
110
|
continue;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
// Any other error
|
|
97
|
-
|
|
98
|
-
continue;
|
|
113
|
+
// Any other error (auth, network, server error) — surface immediately
|
|
114
|
+
throw err;
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
const { message, tokens } = provider.parseResponse(data);
|
|
@@ -114,7 +130,7 @@ async function chatCompletion(messages, options = {}) {
|
|
|
114
130
|
return message;
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
throw
|
|
133
|
+
throw firstError || new Error('All providers failed or exhausted.');
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
// ─── embedText ───────────────────────────────────────────────────────────────
|
package/src/telegram/bot.js
CHANGED
|
@@ -76,6 +76,47 @@ function handleApiCommand(arg) {
|
|
|
76
76
|
return `✅ Switched to *${p.name}* (\`${target}\`)\nModel: \`${p.chatModel}\``;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// ─── /limit command ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function handleLimitCommand(arg) {
|
|
82
|
+
if (!arg) {
|
|
83
|
+
const status = tokenManager.getStatus();
|
|
84
|
+
const lines = ['⚙️ *Token Limits* (0 = unlimited)', ''];
|
|
85
|
+
for (const s of status) {
|
|
86
|
+
const limitStr = s.limit > 0 ? s.limit.toLocaleString() : '∞ unlimited';
|
|
87
|
+
const active = s.active ? ' ✅' : '';
|
|
88
|
+
lines.push(`\`${s.id}\`: ${limitStr}${active}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('Use `/limit <provider> <number>` to set a limit.');
|
|
92
|
+
lines.push('Use `/limit <provider> 0` for unlimited.');
|
|
93
|
+
lines.push('Providers: ' + KNOWN_PROVIDERS.map((n) => `\`${n}\``).join(', '));
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parts = arg.trim().split(/\s+/);
|
|
98
|
+
if (parts.length !== 2) {
|
|
99
|
+
return '❌ Usage: `/limit <provider> <number>`\nExample: `/limit claude 0`';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [providerArg, valueArg] = parts;
|
|
103
|
+
const id = providerArg.toLowerCase();
|
|
104
|
+
if (!KNOWN_PROVIDERS.includes(id)) {
|
|
105
|
+
return `❌ Unknown provider: \`${id}\`\nAvailable: ${KNOWN_PROVIDERS.map((n) => `\`${n}\``).join(', ')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const n = Number(valueArg);
|
|
109
|
+
if (isNaN(n) || n < 0 || !Number.isFinite(n)) {
|
|
110
|
+
return '❌ Limit must be a non-negative number. Use `0` for unlimited.';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = tokenManager.setLimit(id, n);
|
|
114
|
+
if (!result.ok) return `❌ ${result.error}`;
|
|
115
|
+
|
|
116
|
+
const limitStr = n === 0 ? '∞ unlimited' : n.toLocaleString() + ' tokens/day';
|
|
117
|
+
return `✅ *${id}* limit set to *${limitStr}*`;
|
|
118
|
+
}
|
|
119
|
+
|
|
79
120
|
// ─── /tokens command ──────────────────────────────────────────────────────────
|
|
80
121
|
|
|
81
122
|
function handleTokensCommand() {
|
|
@@ -102,6 +143,7 @@ function startTelegramBot() {
|
|
|
102
143
|
bot.setMyCommands([
|
|
103
144
|
{ command: 'api', description: 'Show or switch active API provider' },
|
|
104
145
|
{ command: 'tokens', description: 'Show token usage for today' },
|
|
146
|
+
{ command: 'limit', description: 'Show or set daily token limit per provider' },
|
|
105
147
|
{ command: 'help', description: 'Show all available commands' }
|
|
106
148
|
]).catch((err) => {
|
|
107
149
|
process.stderr.write(`[telegram] setMyCommands failed: ${err.message}\n`);
|
|
@@ -127,8 +169,8 @@ function startTelegramBot() {
|
|
|
127
169
|
const MD = { parse_mode: 'Markdown' };
|
|
128
170
|
|
|
129
171
|
// ── Slash commands ────────────────────────────────────────────────────
|
|
130
|
-
if (text.startsWith('/api')) {
|
|
131
|
-
const arg = text.slice(4).trim() || null;
|
|
172
|
+
if (text.startsWith('/api') || text.startsWith('/ap ') || text === '/ap') {
|
|
173
|
+
const arg = text.startsWith('/api') ? text.slice(4).trim() || null : text.slice(3).trim() || null;
|
|
132
174
|
await safeSend(bot, chatId, handleApiCommand(arg), MD);
|
|
133
175
|
return;
|
|
134
176
|
}
|
|
@@ -138,6 +180,12 @@ function startTelegramBot() {
|
|
|
138
180
|
return;
|
|
139
181
|
}
|
|
140
182
|
|
|
183
|
+
if (text.startsWith('/limit')) {
|
|
184
|
+
const arg = text.slice(6).trim() || null;
|
|
185
|
+
await safeSend(bot, chatId, handleLimitCommand(arg), MD);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
141
189
|
if (text === '/help' || text === '/start') {
|
|
142
190
|
const helpText = [
|
|
143
191
|
'🤖 *Tiger Bot Commands*',
|
|
@@ -145,11 +193,11 @@ function startTelegramBot() {
|
|
|
145
193
|
'/api \\- Show current provider & token stats',
|
|
146
194
|
'/api `<name>` \\- Switch active API provider',
|
|
147
195
|
'/tokens \\- Show token usage for today',
|
|
196
|
+
'/limit \\- Show daily token limits per provider',
|
|
197
|
+
'/limit `<name> <n>` \\- Set limit \\(0 = unlimited\\)',
|
|
148
198
|
'/help \\- Show this message',
|
|
149
199
|
'',
|
|
150
|
-
'*Available providers:* ' + KNOWN_PROVIDERS.join(', ')
|
|
151
|
-
'',
|
|
152
|
-
'_Token limits & auto\\-switch configured in .env_'
|
|
200
|
+
'*Available providers:* ' + KNOWN_PROVIDERS.join(', ')
|
|
153
201
|
].join('\n');
|
|
154
202
|
await safeSend(bot, chatId, helpText, { parse_mode: 'MarkdownV2' });
|
|
155
203
|
return;
|
package/src/tokenManager.js
CHANGED
|
@@ -107,6 +107,21 @@ function getCurrentProvider() {
|
|
|
107
107
|
return state.activeProvider;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Set the daily token limit for a provider at runtime.
|
|
112
|
+
* limit = 0 means unlimited. Returns { ok } or { ok: false, error }.
|
|
113
|
+
*/
|
|
114
|
+
function setLimit(id, limit) {
|
|
115
|
+
ensureInit();
|
|
116
|
+
const known = ['kimi', 'moonshot', 'zai', 'minimax', 'claude'];
|
|
117
|
+
if (!known.includes(id)) return { ok: false, error: `Unknown provider: ${id}` };
|
|
118
|
+
const n = Number(limit);
|
|
119
|
+
if (isNaN(n) || n < 0) return { ok: false, error: 'Limit must be a non-negative number (0 = unlimited)' };
|
|
120
|
+
state.limits[id] = Math.floor(n);
|
|
121
|
+
saveUsageFile();
|
|
122
|
+
return { ok: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
110
125
|
/**
|
|
111
126
|
* Manually switch to a named provider.
|
|
112
127
|
* Returns { ok, provider } or { ok: false, error }
|
|
@@ -214,6 +229,7 @@ module.exports = {
|
|
|
214
229
|
init,
|
|
215
230
|
getCurrentProvider,
|
|
216
231
|
setProvider,
|
|
232
|
+
setLimit,
|
|
217
233
|
recordTokens,
|
|
218
234
|
isOverLimit,
|
|
219
235
|
getNextCandidates,
|