llm-cli-gateway 1.15.3 → 1.16.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/CHANGELOG.md +49 -0
- package/README.md +50 -10
- package/dist/cli-updater.js +16 -8
- package/dist/config.d.ts +2 -17
- package/dist/config.js +6 -24
- package/dist/db.d.ts +3 -13
- package/dist/db.js +6 -78
- package/dist/doctor.d.ts +4 -4
- package/dist/doctor.js +23 -13
- package/dist/health.d.ts +1 -8
- package/dist/health.js +2 -18
- package/dist/index.d.ts +5 -1
- package/dist/index.js +19 -11
- package/dist/migrate-sessions.js +3 -5
- package/dist/provider-login-guidance.js +10 -5
- package/dist/provider-status.js +1 -1
- package/dist/request-helpers.d.ts +10 -3
- package/dist/request-helpers.js +16 -4
- package/dist/session-manager-pg.d.ts +13 -42
- package/dist/session-manager-pg.js +25 -277
- package/dist/session-manager.js +3 -3
- package/dist/upstream-contracts.js +24 -2
- package/package.json +1 -6
|
@@ -7,112 +7,16 @@ const DEFAULT_SESSION_DESCRIPTIONS = {
|
|
|
7
7
|
mistral: "Mistral Session",
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* PostgreSQL-backed session manager
|
|
10
|
+
* PostgreSQL-backed session manager. PostgreSQL is the source of truth and
|
|
11
|
+
* the only required service for this backend.
|
|
11
12
|
*/
|
|
12
13
|
export class PostgreSQLSessionManager {
|
|
13
14
|
pool;
|
|
14
|
-
|
|
15
|
-
cacheTtl;
|
|
16
|
-
logger;
|
|
17
|
-
constructor(pool, redis, cacheTtl, logger) {
|
|
15
|
+
constructor(pool) {
|
|
18
16
|
this.pool = pool;
|
|
19
|
-
this.redis = redis;
|
|
20
|
-
this.cacheTtl = cacheTtl;
|
|
21
|
-
this.logger = logger;
|
|
22
17
|
}
|
|
23
18
|
/**
|
|
24
|
-
*
|
|
25
|
-
* Returns [success, lockValue] tuple
|
|
26
|
-
*/
|
|
27
|
-
async acquireLock(key, ttlSeconds) {
|
|
28
|
-
const lockKey = `lock:${key}`;
|
|
29
|
-
const lockValue = randomUUID();
|
|
30
|
-
// SET NX EX atomic operation
|
|
31
|
-
const result = await this.redis.set(lockKey, lockValue, "EX", ttlSeconds, "NX");
|
|
32
|
-
return [result === "OK", lockValue];
|
|
33
|
-
}
|
|
34
|
-
async sleep(ms) {
|
|
35
|
-
await new Promise(resolve => setTimeout(resolve, ms));
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Acquire a distributed lock with bounded retries to smooth contention spikes.
|
|
39
|
-
*/
|
|
40
|
-
async acquireLockWithRetry(key, ttlSeconds, errorLabel, maxWaitMs = 6000) {
|
|
41
|
-
const deadline = Date.now() + maxWaitMs;
|
|
42
|
-
while (true) {
|
|
43
|
-
const [lockAcquired, lockValue] = await this.acquireLock(key, ttlSeconds);
|
|
44
|
-
if (lockAcquired) {
|
|
45
|
-
return lockValue;
|
|
46
|
-
}
|
|
47
|
-
if (Date.now() >= deadline) {
|
|
48
|
-
throw new Error(`Failed to acquire lock for ${errorLabel}`);
|
|
49
|
-
}
|
|
50
|
-
// Small jitter avoids lock-step retries from concurrent callers.
|
|
51
|
-
await this.sleep(25 + Math.floor(Math.random() * 25));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Release distributed lock with optimistic Redis transaction semantics.
|
|
56
|
-
* Only releases if lockValue matches, which prevents releasing another
|
|
57
|
-
* process's lock after expiry/reacquire.
|
|
58
|
-
*/
|
|
59
|
-
async releaseLock(key, lockValue) {
|
|
60
|
-
const lockKey = `lock:${key}`;
|
|
61
|
-
await this.redis.watch(lockKey);
|
|
62
|
-
try {
|
|
63
|
-
const currentValue = await this.redis.get(lockKey);
|
|
64
|
-
if (currentValue !== lockValue) {
|
|
65
|
-
await this.redis.unwatch();
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
await this.redis.multi().del(lockKey).exec();
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
await this.redis.unwatch().catch(() => undefined);
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Invalidate session cache
|
|
77
|
-
*/
|
|
78
|
-
async invalidateCache(sessionId) {
|
|
79
|
-
try {
|
|
80
|
-
await this.redis.del(`session:${sessionId}`);
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
// Graceful degradation - log but don't fail
|
|
84
|
-
this.logger.error(`Cache invalidation failed for session ${sessionId}`, { error, sessionId });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Invalidate session list cache using SCAN (non-blocking)
|
|
89
|
-
*/
|
|
90
|
-
async invalidateListCache(cli) {
|
|
91
|
-
try {
|
|
92
|
-
if (cli) {
|
|
93
|
-
await this.redis.del(`session_list:${cli}`);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
// Use SCAN instead of KEYS to avoid blocking Redis
|
|
97
|
-
const keys = [];
|
|
98
|
-
let cursor = "0";
|
|
99
|
-
do {
|
|
100
|
-
const [nextCursor, matchedKeys] = await this.redis.scan(cursor, "MATCH", "session_list:*", "COUNT", 100);
|
|
101
|
-
cursor = nextCursor;
|
|
102
|
-
keys.push(...matchedKeys);
|
|
103
|
-
} while (cursor !== "0");
|
|
104
|
-
// Delete in batches to avoid overwhelming Redis
|
|
105
|
-
if (keys.length > 0) {
|
|
106
|
-
await this.redis.del(...keys);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
this.logger.error("List cache invalidation failed", { error });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Create a new session
|
|
19
|
+
* Create a new session.
|
|
116
20
|
*/
|
|
117
21
|
async createSession(cli, description, sessionId) {
|
|
118
22
|
const id = sessionId || randomUUID();
|
|
@@ -121,32 +25,19 @@ export class PostgreSQLSessionManager {
|
|
|
121
25
|
const client = await this.pool.connect();
|
|
122
26
|
try {
|
|
123
27
|
await client.query("BEGIN");
|
|
124
|
-
// Insert session
|
|
125
28
|
await client.query(`INSERT INTO sessions (id, cli, description, created_at, last_used_at)
|
|
126
29
|
VALUES ($1, $2, $3, $4, $5)`, [id, cli, sessionDescription, now, now]);
|
|
127
|
-
// Set as active if none exists
|
|
128
30
|
await client.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
|
|
129
31
|
VALUES ($1, $2, $3)
|
|
130
32
|
ON CONFLICT (cli) DO NOTHING`, [cli, id, now]);
|
|
131
33
|
await client.query("COMMIT");
|
|
132
|
-
|
|
34
|
+
return {
|
|
133
35
|
id,
|
|
134
36
|
cli,
|
|
135
37
|
createdAt: now,
|
|
136
38
|
lastUsedAt: now,
|
|
137
39
|
description: sessionDescription,
|
|
138
40
|
};
|
|
139
|
-
// Write-through to cache
|
|
140
|
-
try {
|
|
141
|
-
await this.redis.setex(`session:${id}`, this.cacheTtl.session, JSON.stringify(session));
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
// Graceful degradation
|
|
145
|
-
this.logger.error("Cache write failed", { error });
|
|
146
|
-
}
|
|
147
|
-
// Invalidate list cache
|
|
148
|
-
await this.invalidateListCache(cli);
|
|
149
|
-
return session;
|
|
150
41
|
}
|
|
151
42
|
catch (error) {
|
|
152
43
|
await client.query("ROLLBACK");
|
|
@@ -157,55 +48,18 @@ export class PostgreSQLSessionManager {
|
|
|
157
48
|
}
|
|
158
49
|
}
|
|
159
50
|
/**
|
|
160
|
-
* Get session by ID
|
|
51
|
+
* Get session by ID.
|
|
161
52
|
*/
|
|
162
53
|
async getSession(sessionId) {
|
|
163
|
-
// Try cache first
|
|
164
|
-
try {
|
|
165
|
-
const cached = await this.redis.get(`session:${sessionId}`);
|
|
166
|
-
if (cached) {
|
|
167
|
-
return JSON.parse(cached);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
// Graceful degradation - fallback to DB
|
|
172
|
-
this.logger.error("Cache read failed", { error });
|
|
173
|
-
}
|
|
174
|
-
// Cache miss - query database
|
|
175
54
|
const result = await this.pool.query(`SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
176
55
|
FROM sessions
|
|
177
56
|
WHERE id = $1`, [sessionId]);
|
|
178
|
-
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
const session = result.rows[0];
|
|
182
|
-
// Populate cache
|
|
183
|
-
try {
|
|
184
|
-
await this.redis.setex(`session:${sessionId}`, this.cacheTtl.session, JSON.stringify(session));
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
this.logger.error("Cache write failed", { error });
|
|
188
|
-
}
|
|
189
|
-
return session;
|
|
57
|
+
return result.rows[0] ?? null;
|
|
190
58
|
}
|
|
191
59
|
/**
|
|
192
|
-
* List all sessions, optionally filtered by CLI
|
|
60
|
+
* List all sessions, optionally filtered by CLI.
|
|
193
61
|
*/
|
|
194
62
|
async listSessions(cli) {
|
|
195
|
-
// Try cache for CLI-specific lists
|
|
196
|
-
const cacheKey = cli ? `session_list:${cli}` : null;
|
|
197
|
-
if (cacheKey) {
|
|
198
|
-
try {
|
|
199
|
-
const cached = await this.redis.get(cacheKey);
|
|
200
|
-
if (cached) {
|
|
201
|
-
return JSON.parse(cached);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
this.logger.error("Cache read failed", { error });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Query database
|
|
209
63
|
const query = cli
|
|
210
64
|
? `SELECT id, cli, description, metadata, created_at AS "createdAt", last_used_at AS "lastUsedAt"
|
|
211
65
|
FROM sessions
|
|
@@ -217,176 +71,70 @@ export class PostgreSQLSessionManager {
|
|
|
217
71
|
const result = cli
|
|
218
72
|
? await this.pool.query(query, [cli])
|
|
219
73
|
: await this.pool.query(query);
|
|
220
|
-
|
|
221
|
-
// Cache CLI-specific lists
|
|
222
|
-
if (cacheKey) {
|
|
223
|
-
try {
|
|
224
|
-
await this.redis.setex(cacheKey, this.cacheTtl.sessionList, JSON.stringify(sessions));
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
this.logger.error("Cache write failed", { error });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return sessions;
|
|
74
|
+
return result.rows;
|
|
231
75
|
}
|
|
232
76
|
/**
|
|
233
|
-
* Delete a session
|
|
77
|
+
* Delete a session.
|
|
234
78
|
*/
|
|
235
79
|
async deleteSession(sessionId) {
|
|
236
|
-
// Get session to find CLI type
|
|
237
80
|
const session = await this.getSession(sessionId);
|
|
238
81
|
if (!session) {
|
|
239
82
|
return false;
|
|
240
83
|
}
|
|
241
|
-
// Delete from database (CASCADE will handle active_sessions)
|
|
242
84
|
const result = await this.pool.query("DELETE FROM sessions WHERE id = $1", [sessionId]);
|
|
243
|
-
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
// Invalidate caches (session, active session for this CLI, and list)
|
|
247
|
-
await this.invalidateCache(sessionId);
|
|
248
|
-
try {
|
|
249
|
-
await this.redis.del(`active_session:${session.cli}`);
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
this.logger.error(`Failed to invalidate active session cache for ${session.cli}`, { error });
|
|
253
|
-
}
|
|
254
|
-
await this.invalidateListCache(session.cli);
|
|
255
|
-
return true;
|
|
85
|
+
return result.rowCount !== 0;
|
|
256
86
|
}
|
|
257
87
|
/**
|
|
258
|
-
* Set active session for a CLI
|
|
88
|
+
* Set active session for a CLI. The row-level update is serialized by
|
|
89
|
+
* PostgreSQL and the session FK keeps stale IDs from being recorded.
|
|
259
90
|
*/
|
|
260
91
|
async setActiveSession(cli, sessionId) {
|
|
261
|
-
// Validate session exists if not null
|
|
262
92
|
if (sessionId !== null) {
|
|
263
93
|
const session = await this.getSession(sessionId);
|
|
264
94
|
if (!session || session.cli !== cli) {
|
|
265
95
|
return false;
|
|
266
96
|
}
|
|
267
97
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
await this.pool.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
|
|
274
|
-
VALUES ($1, $2, $3)
|
|
275
|
-
ON CONFLICT (cli) DO UPDATE SET session_id = $2, updated_at = $3`, [cli, sessionId, now]);
|
|
276
|
-
// Update cache
|
|
277
|
-
try {
|
|
278
|
-
if (sessionId) {
|
|
279
|
-
await this.redis.setex(`active_session:${cli}`, this.cacheTtl.activeSession, sessionId);
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
await this.redis.del(`active_session:${cli}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
this.logger.error("Cache update failed", { error });
|
|
287
|
-
}
|
|
288
|
-
return true;
|
|
289
|
-
}
|
|
290
|
-
finally {
|
|
291
|
-
// Release lock with ownership verification
|
|
292
|
-
try {
|
|
293
|
-
await this.releaseLock(`active_session:${cli}`, lockValue);
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
this.logger.error(`Failed to release lock for active session ${cli}`, { error, cli });
|
|
297
|
-
}
|
|
298
|
-
}
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
await this.pool.query(`INSERT INTO active_sessions (cli, session_id, updated_at)
|
|
100
|
+
VALUES ($1, $2, $3)
|
|
101
|
+
ON CONFLICT (cli) DO UPDATE SET session_id = $2, updated_at = $3`, [cli, sessionId, now]);
|
|
102
|
+
return true;
|
|
299
103
|
}
|
|
300
104
|
/**
|
|
301
|
-
* Get active session for a CLI
|
|
105
|
+
* Get active session for a CLI.
|
|
302
106
|
*/
|
|
303
107
|
async getActiveSession(cli) {
|
|
304
|
-
// Try cache first
|
|
305
|
-
try {
|
|
306
|
-
const cachedId = await this.redis.get(`active_session:${cli}`);
|
|
307
|
-
if (cachedId) {
|
|
308
|
-
return await this.getSession(cachedId);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
this.logger.error("Cache read failed", { error });
|
|
313
|
-
}
|
|
314
|
-
// Query database
|
|
315
108
|
const result = await this.pool.query("SELECT session_id FROM active_sessions WHERE cli = $1", [cli]);
|
|
316
|
-
|
|
109
|
+
const sessionId = result.rows[0]?.session_id;
|
|
110
|
+
if (!sessionId) {
|
|
317
111
|
return null;
|
|
318
112
|
}
|
|
319
|
-
const sessionId = result.rows[0].session_id;
|
|
320
|
-
// Populate cache
|
|
321
|
-
try {
|
|
322
|
-
await this.redis.setex(`active_session:${cli}`, this.cacheTtl.activeSession, sessionId);
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
this.logger.error("Cache write failed", { error });
|
|
326
|
-
}
|
|
327
113
|
return await this.getSession(sessionId);
|
|
328
114
|
}
|
|
329
115
|
/**
|
|
330
|
-
* Update session usage timestamp
|
|
116
|
+
* Update session usage timestamp.
|
|
331
117
|
*/
|
|
332
118
|
async updateSessionUsage(sessionId) {
|
|
333
119
|
const now = new Date().toISOString();
|
|
334
120
|
await this.pool.query("UPDATE sessions SET last_used_at = $1 WHERE id = $2", [now, sessionId]);
|
|
335
|
-
// Invalidate cache to force refresh
|
|
336
|
-
await this.invalidateCache(sessionId);
|
|
337
121
|
}
|
|
338
122
|
/**
|
|
339
|
-
* Update session metadata
|
|
123
|
+
* Update session metadata using PostgreSQL's atomic JSONB merge.
|
|
340
124
|
*/
|
|
341
125
|
async updateSessionMetadata(sessionId, metadata) {
|
|
342
|
-
// Use PostgreSQL JSONB || operator for atomic merge (prevents race conditions)
|
|
343
126
|
const result = await this.pool.query(`UPDATE sessions
|
|
344
127
|
SET metadata = COALESCE(metadata, '{}'::jsonb) || $1::jsonb
|
|
345
128
|
WHERE id = $2
|
|
346
129
|
RETURNING id`, [JSON.stringify(metadata), sessionId]);
|
|
347
|
-
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
// Invalidate cache
|
|
351
|
-
await this.invalidateCache(sessionId);
|
|
352
|
-
return true;
|
|
130
|
+
return result.rowCount !== 0;
|
|
353
131
|
}
|
|
354
132
|
/**
|
|
355
|
-
* Clear all sessions, optionally filtered by CLI
|
|
356
|
-
* Invalidates all related caches (session, active, list)
|
|
133
|
+
* Clear all sessions, optionally filtered by CLI.
|
|
357
134
|
*/
|
|
358
135
|
async clearAllSessions(cli) {
|
|
359
|
-
// First get all sessions to invalidate their caches
|
|
360
|
-
const sessions = await this.listSessions(cli);
|
|
361
|
-
// Delete from database
|
|
362
136
|
const query = cli ? "DELETE FROM sessions WHERE cli = $1" : "DELETE FROM sessions";
|
|
363
137
|
const result = cli ? await this.pool.query(query, [cli]) : await this.pool.query(query);
|
|
364
|
-
// Invalidate individual session caches (concurrent — each has its own try/catch)
|
|
365
|
-
await Promise.all(sessions.map(session => this.invalidateCache(session.id)));
|
|
366
|
-
// Invalidate active session caches
|
|
367
|
-
if (cli) {
|
|
368
|
-
try {
|
|
369
|
-
await this.redis.del(`active_session:${cli}`);
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
this.logger.error(`Failed to invalidate active session cache for ${cli}`, { error, cli });
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
// Invalidate all active session caches
|
|
377
|
-
try {
|
|
378
|
-
await Promise.all([
|
|
379
|
-
this.redis.del("active_session:claude"),
|
|
380
|
-
this.redis.del("active_session:codex"),
|
|
381
|
-
this.redis.del("active_session:gemini"),
|
|
382
|
-
]);
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
this.logger.error("Failed to invalidate active session caches", { error });
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// Invalidate list caches
|
|
389
|
-
await this.invalidateListCache(cli);
|
|
390
138
|
return result.rowCount || 0;
|
|
391
139
|
}
|
|
392
140
|
}
|
package/dist/session-manager.js
CHANGED
|
@@ -230,15 +230,15 @@ export const SessionManager = FileSessionManager;
|
|
|
230
230
|
* @param logger - Logger instance for structured logging
|
|
231
231
|
*/
|
|
232
232
|
export async function createSessionManager(config, db, logger, opts) {
|
|
233
|
-
if (config?.database
|
|
234
|
-
// Import dynamically to avoid loading pg
|
|
233
|
+
if (config?.database) {
|
|
234
|
+
// Import dynamically to avoid loading pg if not needed.
|
|
235
235
|
const { PostgreSQLSessionManager } = await import("./session-manager-pg.js");
|
|
236
236
|
// Use provided db connection or create new one
|
|
237
237
|
if (!db) {
|
|
238
238
|
const { createDatabaseConnection } = await import("./db.js");
|
|
239
239
|
db = await createDatabaseConnection(config, logger);
|
|
240
240
|
}
|
|
241
|
-
return new PostgreSQLSessionManager(db.getPool()
|
|
241
|
+
return new PostgreSQLSessionManager(db.getPool());
|
|
242
242
|
}
|
|
243
243
|
else {
|
|
244
244
|
// Use file-based storage with TTL from config
|
|
@@ -604,15 +604,16 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
604
604
|
// Phase 4 slice δ
|
|
605
605
|
"maxTurns",
|
|
606
606
|
"maxPrice",
|
|
607
|
+
"maxTokens",
|
|
607
608
|
// Phase 4 slice ζ
|
|
608
609
|
"workingDir",
|
|
609
610
|
"addDir",
|
|
610
611
|
],
|
|
611
612
|
flags: {
|
|
612
613
|
"-p": { arity: "one", description: "Prompt text" },
|
|
613
|
-
"--output
|
|
614
|
+
"--output": {
|
|
614
615
|
arity: "one",
|
|
615
|
-
values: ["
|
|
616
|
+
values: ["text", "json", "streaming"],
|
|
616
617
|
description: "Output format",
|
|
617
618
|
},
|
|
618
619
|
"--agent": {
|
|
@@ -641,6 +642,11 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
641
642
|
pattern: /^(0|[1-9][0-9]*)(\.[0-9]+)?$/,
|
|
642
643
|
description: "Cumulative cost cap in USD (Phase 4 slice δ, programmatic mode only)",
|
|
643
644
|
},
|
|
645
|
+
"--max-tokens": {
|
|
646
|
+
arity: "one",
|
|
647
|
+
pattern: /^[1-9][0-9]*$/,
|
|
648
|
+
description: "Cumulative prompt + completion token cap (Vibe 2.x programmatic mode)",
|
|
649
|
+
},
|
|
644
650
|
"--workdir": {
|
|
645
651
|
arity: "one",
|
|
646
652
|
description: "Working directory for the invocation (Phase 4 slice ζ)",
|
|
@@ -686,6 +692,22 @@ export const UPSTREAM_CLI_CONTRACTS = {
|
|
|
686
692
|
env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
|
|
687
693
|
expect: "pass",
|
|
688
694
|
},
|
|
695
|
+
{
|
|
696
|
+
id: "mistral-output-streaming-and-max-tokens",
|
|
697
|
+
description: "Vibe 2.x: --output streaming and --max-tokens are accepted",
|
|
698
|
+
args: [
|
|
699
|
+
"-p",
|
|
700
|
+
"hello",
|
|
701
|
+
"--agent",
|
|
702
|
+
"auto-approve",
|
|
703
|
+
"--output",
|
|
704
|
+
"streaming",
|
|
705
|
+
"--max-tokens",
|
|
706
|
+
"1000",
|
|
707
|
+
],
|
|
708
|
+
env: { VIBE_ACTIVE_MODEL: "mistral-medium-3.5" },
|
|
709
|
+
expect: "pass",
|
|
710
|
+
},
|
|
689
711
|
{
|
|
690
712
|
id: "mistral-max-price-scientific-notation",
|
|
691
713
|
description: "Phase 4 slice δ: scientific-notation --max-price is rejected by contract pattern (matches MAX_PRICE_SCHEMA bounds)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-cli-gateway",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.1",
|
|
4
4
|
"mcpName": "io.github.verivus-oss/llm-cli-gateway",
|
|
5
5
|
"description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -90,13 +90,9 @@
|
|
|
90
90
|
"zod": "^3.23.0"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"ioredis": "^5.4.1",
|
|
94
93
|
"pg": "^8.12.0"
|
|
95
94
|
},
|
|
96
95
|
"peerDependenciesMeta": {
|
|
97
|
-
"ioredis": {
|
|
98
|
-
"optional": true
|
|
99
|
-
},
|
|
100
96
|
"pg": {
|
|
101
97
|
"optional": true
|
|
102
98
|
}
|
|
@@ -111,7 +107,6 @@
|
|
|
111
107
|
"eslint": "^8.57.1",
|
|
112
108
|
"eslint-config-prettier": "^9.0.0",
|
|
113
109
|
"eslint-plugin-security": "^3.0.1",
|
|
114
|
-
"ioredis": "5.9.2",
|
|
115
110
|
"pg": "^8.12.0",
|
|
116
111
|
"prettier": "^3.0.0",
|
|
117
112
|
"typescript": "^5.0.0",
|