morpheus-cli 0.3.8 → 0.4.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/dist/channels/telegram.js +21 -1
- package/dist/http/api.js +91 -50
- package/dist/runtime/memory/embedding.service.js +15 -1
- package/dist/runtime/memory/sati/index.js +2 -2
- package/dist/runtime/memory/sati/repository.js +48 -34
- package/dist/runtime/memory/sati/service.js +3 -2
- package/dist/runtime/memory/sqlite.js +160 -27
- package/dist/runtime/oracle.js +9 -6
- package/dist/runtime/telephonist.js +19 -1
- package/dist/ui/assets/index-CovGlIO5.js +109 -0
- package/dist/ui/assets/index-LrqT6MpO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CwvCMGLo.css +0 -1
- package/dist/ui/assets/index-D9REy_tK.js +0 -109
|
@@ -13,6 +13,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
13
13
|
dbSati; // Optional separate DB for Sati memory, if needed in the future
|
|
14
14
|
sessionId;
|
|
15
15
|
limit;
|
|
16
|
+
titleSet = false; // cache: skip setSessionTitleIfNeeded after title is set
|
|
17
|
+
get currentSessionId() {
|
|
18
|
+
return this.sessionId;
|
|
19
|
+
}
|
|
16
20
|
constructor(fields) {
|
|
17
21
|
super();
|
|
18
22
|
this.sessionId = fields.sessionId && fields.sessionId !== '' ? fields.sessionId : '';
|
|
@@ -107,12 +111,16 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
107
111
|
total_tokens INTEGER,
|
|
108
112
|
cache_read_tokens INTEGER,
|
|
109
113
|
provider TEXT,
|
|
110
|
-
model TEXT
|
|
114
|
+
model TEXT,
|
|
115
|
+
audio_duration_seconds REAL
|
|
111
116
|
);
|
|
112
|
-
|
|
113
|
-
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
114
119
|
ON messages(session_id);
|
|
115
120
|
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session_id_id
|
|
122
|
+
ON messages(session_id, id DESC);
|
|
123
|
+
|
|
116
124
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
117
125
|
id TEXT PRIMARY KEY,
|
|
118
126
|
title TEXT,
|
|
@@ -126,6 +134,33 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
126
134
|
embedding_status TEXT CHECK (embedding_status IN ('none', 'pending', 'embedded', 'failed')) NOT NULL DEFAULT 'none'
|
|
127
135
|
);
|
|
128
136
|
|
|
137
|
+
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
138
|
+
provider TEXT NOT NULL,
|
|
139
|
+
model TEXT NOT NULL,
|
|
140
|
+
input_price_per_1m REAL NOT NULL DEFAULT 0,
|
|
141
|
+
output_price_per_1m REAL NOT NULL DEFAULT 0,
|
|
142
|
+
PRIMARY KEY (provider, model)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
INSERT OR IGNORE INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES
|
|
146
|
+
('anthropic', 'claude-opus-4-6', 15.0, 75.0),
|
|
147
|
+
('anthropic', 'claude-sonnet-4-5-20250929', 3.0, 15.0),
|
|
148
|
+
('anthropic', 'claude-haiku-4-5-20251001', 0.8, 4.0),
|
|
149
|
+
('anthropic', 'claude-3-5-sonnet-20241022', 3.0, 15.0),
|
|
150
|
+
('anthropic', 'claude-3-5-haiku-20241022', 0.8, 4.0),
|
|
151
|
+
('anthropic', 'claude-3-opus-20240229', 15.0, 75.0),
|
|
152
|
+
('openai', 'gpt-4o', 2.5, 10.0),
|
|
153
|
+
('openai', 'gpt-4o-mini', 0.15, 0.6),
|
|
154
|
+
('openai', 'gpt-4-turbo', 10.0, 30.0),
|
|
155
|
+
('openai', 'gpt-3.5-turbo', 0.5, 1.5),
|
|
156
|
+
('openai', 'o1', 15.0, 60.0),
|
|
157
|
+
('openai', 'o1-mini', 3.0, 12.0),
|
|
158
|
+
('google', 'gemini-2.5-flash', 0.15, 0.6),
|
|
159
|
+
('google', 'gemini-2.5-flash-lite', 0.075, 0.3),
|
|
160
|
+
('google', 'gemini-2.0-flash', 0.1, 0.4),
|
|
161
|
+
('google', 'gemini-1.5-pro', 1.25, 5.0),
|
|
162
|
+
('google', 'gemini-1.5-flash', 0.075, 0.3);
|
|
163
|
+
|
|
129
164
|
`);
|
|
130
165
|
this.migrateTable();
|
|
131
166
|
}
|
|
@@ -147,13 +182,15 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
147
182
|
'total_tokens',
|
|
148
183
|
'cache_read_tokens',
|
|
149
184
|
'provider',
|
|
150
|
-
'model'
|
|
185
|
+
'model',
|
|
186
|
+
'audio_duration_seconds'
|
|
151
187
|
];
|
|
152
188
|
const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
|
|
189
|
+
const realColumns = new Set(['audio_duration_seconds']);
|
|
153
190
|
for (const col of newColumns) {
|
|
154
191
|
if (!columns.has(col)) {
|
|
155
192
|
try {
|
|
156
|
-
const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
|
|
193
|
+
const type = integerColumns.has(col) ? 'INTEGER' : realColumns.has(col) ? 'REAL' : 'TEXT';
|
|
157
194
|
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
158
195
|
}
|
|
159
196
|
catch (e) {
|
|
@@ -292,6 +329,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
292
329
|
// Extract provider metadata
|
|
293
330
|
const provider = anyMsg.provider_metadata?.provider ?? null;
|
|
294
331
|
const model = anyMsg.provider_metadata?.model ?? null;
|
|
332
|
+
const audioDurationSeconds = usage?.audio_duration_seconds ?? null;
|
|
295
333
|
// Handle special content serialization for Tools
|
|
296
334
|
let finalContent = "";
|
|
297
335
|
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
@@ -314,8 +352,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
314
352
|
? message.content
|
|
315
353
|
: JSON.stringify(message.content);
|
|
316
354
|
}
|
|
317
|
-
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
318
|
-
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model);
|
|
355
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, audio_duration_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
356
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model, audioDurationSeconds);
|
|
319
357
|
// Verificar se a sessão tem título e definir automaticamente se necessário
|
|
320
358
|
await this.setSessionTitleIfNeeded();
|
|
321
359
|
}
|
|
@@ -335,11 +373,67 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
335
373
|
throw new Error(`Failed to add message: ${error}`);
|
|
336
374
|
}
|
|
337
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Adds multiple messages in a single SQLite transaction for better performance.
|
|
378
|
+
* Replaces calling addMessage() in a loop when inserting agent-generated messages.
|
|
379
|
+
*/
|
|
380
|
+
async addMessages(messages) {
|
|
381
|
+
if (messages.length === 0)
|
|
382
|
+
return;
|
|
383
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, audio_duration_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
384
|
+
const insertAll = this.db.transaction((msgs) => {
|
|
385
|
+
for (const message of msgs) {
|
|
386
|
+
let type;
|
|
387
|
+
if (message instanceof HumanMessage)
|
|
388
|
+
type = "human";
|
|
389
|
+
else if (message instanceof AIMessage)
|
|
390
|
+
type = "ai";
|
|
391
|
+
else if (message instanceof SystemMessage)
|
|
392
|
+
type = "system";
|
|
393
|
+
else if (message instanceof ToolMessage)
|
|
394
|
+
type = "tool";
|
|
395
|
+
else
|
|
396
|
+
throw new Error(`Unsupported message type: ${message.constructor.name}`);
|
|
397
|
+
const anyMsg = message;
|
|
398
|
+
const usage = anyMsg.usage_metadata || anyMsg.response_metadata?.usage || anyMsg.response_metadata?.tokenUsage || anyMsg.usage;
|
|
399
|
+
let finalContent;
|
|
400
|
+
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
401
|
+
finalContent = JSON.stringify({ text: message.content, tool_calls: message.tool_calls });
|
|
402
|
+
}
|
|
403
|
+
else if (type === 'tool') {
|
|
404
|
+
const tm = message;
|
|
405
|
+
finalContent = JSON.stringify({ content: tm.content, tool_call_id: tm.tool_call_id, name: tm.name });
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
finalContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
409
|
+
}
|
|
410
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), usage?.input_tokens ?? null, usage?.output_tokens ?? null, usage?.total_tokens ?? null, usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null, anyMsg.provider_metadata?.provider ?? null, anyMsg.provider_metadata?.model ?? null, usage?.audio_duration_seconds ?? null);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
try {
|
|
414
|
+
insertAll(messages);
|
|
415
|
+
await this.setSessionTitleIfNeeded();
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (error instanceof Error) {
|
|
419
|
+
if (error.message.includes('SQLITE_BUSY'))
|
|
420
|
+
throw new Error(`Database is locked. Please try again. Original error: ${error.message}`);
|
|
421
|
+
if (error.message.includes('SQLITE_READONLY'))
|
|
422
|
+
throw new Error(`Database is read-only. Check file permissions. Original error: ${error.message}`);
|
|
423
|
+
if (error.message.includes('SQLITE_FULL'))
|
|
424
|
+
throw new Error(`Database is full or disk space is exhausted. Original error: ${error.message}`);
|
|
425
|
+
}
|
|
426
|
+
throw new Error(`Failed to add messages in batch: ${error}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
338
429
|
/**
|
|
339
430
|
* Verifies if the session has a title, and if not, sets it automatically
|
|
340
431
|
* using the first 50 characters of the oldest human message.
|
|
341
432
|
*/
|
|
342
433
|
async setSessionTitleIfNeeded() {
|
|
434
|
+
// Fast path: skip DB query if we already set the title this session
|
|
435
|
+
if (this.titleSet)
|
|
436
|
+
return;
|
|
343
437
|
// Verificar se a sessão já tem título
|
|
344
438
|
const session = this.db.prepare(`
|
|
345
439
|
SELECT title FROM sessions
|
|
@@ -347,6 +441,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
347
441
|
`).get(this.sessionId);
|
|
348
442
|
if (session && session.title) {
|
|
349
443
|
// A sessão já tem título, não precisa fazer nada
|
|
444
|
+
this.titleSet = true;
|
|
350
445
|
return;
|
|
351
446
|
}
|
|
352
447
|
// Obter a mensagem mais antiga do tipo "human" da sessão
|
|
@@ -369,6 +464,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
369
464
|
}
|
|
370
465
|
// Chamar a função renameSession para definir o título automaticamente
|
|
371
466
|
await this.renameSession(this.sessionId, title);
|
|
467
|
+
this.titleSet = true;
|
|
372
468
|
}
|
|
373
469
|
}
|
|
374
470
|
/**
|
|
@@ -378,9 +474,18 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
378
474
|
try {
|
|
379
475
|
const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
|
|
380
476
|
const row = stmt.get();
|
|
477
|
+
// Calculate total estimated cost by summing per-model costs
|
|
478
|
+
const costStmt = this.db.prepare(`SELECT
|
|
479
|
+
SUM((COALESCE(m.input_tokens, 0) / 1000000.0) * p.input_price_per_1m
|
|
480
|
+
+ (COALESCE(m.output_tokens, 0) / 1000000.0) * p.output_price_per_1m) as totalCost
|
|
481
|
+
FROM messages m
|
|
482
|
+
INNER JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
|
|
483
|
+
WHERE m.provider IS NOT NULL`);
|
|
484
|
+
const costRow = costStmt.get();
|
|
381
485
|
return {
|
|
382
486
|
totalInputTokens: row.totalInput || 0,
|
|
383
|
-
totalOutputTokens: row.totalOutput || 0
|
|
487
|
+
totalOutputTokens: row.totalOutput || 0,
|
|
488
|
+
totalEstimatedCostUsd: costRow.totalCost ?? null
|
|
384
489
|
};
|
|
385
490
|
}
|
|
386
491
|
catch (error) {
|
|
@@ -412,31 +517,58 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
412
517
|
*/
|
|
413
518
|
async getUsageStatsByProviderAndModel() {
|
|
414
519
|
try {
|
|
415
|
-
const stmt = this.db.prepare(`SELECT
|
|
416
|
-
provider,
|
|
417
|
-
COALESCE(model, 'unknown') as model,
|
|
418
|
-
SUM(input_tokens) as totalInputTokens,
|
|
419
|
-
SUM(output_tokens) as totalOutputTokens,
|
|
420
|
-
SUM(total_tokens) as totalTokens,
|
|
421
|
-
COUNT(*) as messageCount
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
520
|
+
const stmt = this.db.prepare(`SELECT
|
|
521
|
+
m.provider,
|
|
522
|
+
COALESCE(m.model, 'unknown') as model,
|
|
523
|
+
SUM(m.input_tokens) as totalInputTokens,
|
|
524
|
+
SUM(m.output_tokens) as totalOutputTokens,
|
|
525
|
+
SUM(m.total_tokens) as totalTokens,
|
|
526
|
+
COUNT(*) as messageCount,
|
|
527
|
+
COALESCE(SUM(m.audio_duration_seconds), 0) as totalAudioSeconds,
|
|
528
|
+
p.input_price_per_1m,
|
|
529
|
+
p.output_price_per_1m
|
|
530
|
+
FROM messages m
|
|
531
|
+
LEFT JOIN model_pricing p ON p.provider = m.provider AND p.model = COALESCE(m.model, 'unknown')
|
|
532
|
+
WHERE m.provider IS NOT NULL
|
|
533
|
+
GROUP BY m.provider, COALESCE(m.model, 'unknown')
|
|
534
|
+
ORDER BY m.provider, m.model`);
|
|
426
535
|
const rows = stmt.all();
|
|
427
|
-
return rows.map((row) =>
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
536
|
+
return rows.map((row) => {
|
|
537
|
+
const inputTokens = row.totalInputTokens || 0;
|
|
538
|
+
const outputTokens = row.totalOutputTokens || 0;
|
|
539
|
+
let estimatedCostUsd = null;
|
|
540
|
+
if (row.input_price_per_1m != null && row.output_price_per_1m != null) {
|
|
541
|
+
estimatedCostUsd = (inputTokens / 1_000_000) * row.input_price_per_1m
|
|
542
|
+
+ (outputTokens / 1_000_000) * row.output_price_per_1m;
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
provider: row.provider,
|
|
546
|
+
model: row.model,
|
|
547
|
+
totalInputTokens: inputTokens,
|
|
548
|
+
totalOutputTokens: outputTokens,
|
|
549
|
+
totalTokens: row.totalTokens || 0,
|
|
550
|
+
messageCount: row.messageCount || 0,
|
|
551
|
+
totalAudioSeconds: row.totalAudioSeconds || 0,
|
|
552
|
+
estimatedCostUsd
|
|
553
|
+
};
|
|
554
|
+
});
|
|
435
555
|
}
|
|
436
556
|
catch (error) {
|
|
437
557
|
throw new Error(`Failed to get grouped usage stats: ${error}`);
|
|
438
558
|
}
|
|
439
559
|
}
|
|
560
|
+
// --- Model Pricing CRUD ---
|
|
561
|
+
listModelPricing() {
|
|
562
|
+
const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m FROM model_pricing ORDER BY provider, model').all();
|
|
563
|
+
return rows;
|
|
564
|
+
}
|
|
565
|
+
upsertModelPricing(entry) {
|
|
566
|
+
this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES (?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m);
|
|
567
|
+
}
|
|
568
|
+
deleteModelPricing(provider, model) {
|
|
569
|
+
const result = this.db.prepare('DELETE FROM model_pricing WHERE provider = ? AND model = ?').run(provider, model);
|
|
570
|
+
return result.changes;
|
|
571
|
+
}
|
|
440
572
|
/**
|
|
441
573
|
* Clears all messages for the current session from the database.
|
|
442
574
|
*/
|
|
@@ -508,6 +640,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
508
640
|
`).run(newId, now);
|
|
509
641
|
// Atualizar o ID da sessão atual desta instância
|
|
510
642
|
this.sessionId = newId;
|
|
643
|
+
this.titleSet = false; // reset cache for new session
|
|
511
644
|
});
|
|
512
645
|
tx(); // Executar a transação
|
|
513
646
|
this.display.log('✅ Nova sessão iniciada e sessão anterior pausada', { source: 'Sati' });
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -216,22 +216,23 @@ You maintain intent until resolution.
|
|
|
216
216
|
const startNewMessagesIndex = messages.length;
|
|
217
217
|
const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
|
|
218
218
|
// console.log('New generated messages', newGeneratedMessages);
|
|
219
|
-
//
|
|
220
|
-
await this.history.addMessage(userMessage);
|
|
221
|
-
// Persist all new intermediate tool calls and responses
|
|
219
|
+
// Inject provider/model metadata into all new messages
|
|
222
220
|
for (const msg of newGeneratedMessages) {
|
|
223
|
-
// Inject provider/model metadata search interactors
|
|
224
221
|
msg.provider_metadata = {
|
|
225
222
|
provider: this.config.llm.provider,
|
|
226
223
|
model: this.config.llm.model
|
|
227
224
|
};
|
|
228
|
-
await this.history.addMessage(msg);
|
|
229
225
|
}
|
|
226
|
+
// Persist user message + all generated messages in a single transaction
|
|
227
|
+
await this.history.addMessages([userMessage, ...newGeneratedMessages]);
|
|
230
228
|
this.display.log('Response generated.', { source: 'Oracle' });
|
|
231
229
|
const lastMessage = response.messages[response.messages.length - 1];
|
|
232
230
|
const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
233
231
|
// Sati Middleware: Evaluation (Fire and forget)
|
|
234
|
-
this.
|
|
232
|
+
const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
|
|
233
|
+
? this.history.currentSessionId
|
|
234
|
+
: undefined;
|
|
235
|
+
this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
|
|
235
236
|
.catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
|
|
236
237
|
return responseContent;
|
|
237
238
|
}
|
|
@@ -302,6 +303,8 @@ You maintain intent until resolution.
|
|
|
302
303
|
//
|
|
303
304
|
// This is safe and clean.
|
|
304
305
|
await this.history.switchSession(sessionId);
|
|
306
|
+
// Close previous connection before re-instantiating to avoid file handle leaks
|
|
307
|
+
this.history.close();
|
|
305
308
|
// Re-instantiate to point to new session
|
|
306
309
|
this.history = new SQLiteChatMessageHistory({
|
|
307
310
|
sessionId: sessionId,
|
|
@@ -2,6 +2,21 @@ import { GoogleGenAI } from '@google/genai';
|
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import { OpenRouter } from '@openrouter/sdk';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
+
/**
|
|
6
|
+
* Estimates audio duration in seconds based on file size and a typical bitrate.
|
|
7
|
+
* Uses 32 kbps (4000 bytes/sec) as a conservative baseline for compressed audio (OGG, MP3, etc.).
|
|
8
|
+
* This is an approximation — actual duration depends on encoding settings.
|
|
9
|
+
*/
|
|
10
|
+
function estimateAudioDurationSeconds(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const stats = fs.statSync(filePath);
|
|
13
|
+
const bytesPerSecond = 4000; // ~32 kbps
|
|
14
|
+
return Math.round(stats.size / bytesPerSecond);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
class GeminiTelephonist {
|
|
6
21
|
model;
|
|
7
22
|
constructor(model) {
|
|
@@ -41,7 +56,8 @@ class GeminiTelephonist {
|
|
|
41
56
|
total_tokens: usage?.totalTokenCount ?? 0,
|
|
42
57
|
input_token_details: {
|
|
43
58
|
cache_read: usage?.cachedContentTokenCount ?? 0
|
|
44
|
-
}
|
|
59
|
+
},
|
|
60
|
+
audio_duration_seconds: estimateAudioDurationSeconds(filePath)
|
|
45
61
|
};
|
|
46
62
|
return { text, usage: usageMetadata };
|
|
47
63
|
}
|
|
@@ -75,6 +91,7 @@ class WhisperTelephonist {
|
|
|
75
91
|
input_tokens: 0,
|
|
76
92
|
output_tokens: 0,
|
|
77
93
|
total_tokens: 0,
|
|
94
|
+
audio_duration_seconds: estimateAudioDurationSeconds(filePath)
|
|
78
95
|
};
|
|
79
96
|
return { text, usage: usageMetadata };
|
|
80
97
|
}
|
|
@@ -131,6 +148,7 @@ class OpenRouterTelephonist {
|
|
|
131
148
|
input_tokens: usage?.prompt_tokens ?? 0,
|
|
132
149
|
output_tokens: usage?.completion_tokens ?? 0,
|
|
133
150
|
total_tokens: usage?.total_tokens ?? 0,
|
|
151
|
+
audio_duration_seconds: estimateAudioDurationSeconds(filePath)
|
|
134
152
|
};
|
|
135
153
|
return { text, usage: usageMetadata };
|
|
136
154
|
}
|