morpheus-cli 0.4.0 → 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/http/api.js +76 -0
- package/dist/runtime/memory/sati/index.js +2 -2
- package/dist/runtime/memory/sati/service.js +3 -2
- package/dist/runtime/memory/sqlite.js +98 -28
- package/dist/runtime/oracle.js +4 -1
- 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
package/dist/http/api.js
CHANGED
|
@@ -213,6 +213,82 @@ export function createApiRouter(oracle) {
|
|
|
213
213
|
res.status(500).json({ error: error.message });
|
|
214
214
|
}
|
|
215
215
|
});
|
|
216
|
+
// --- Model Pricing ---
|
|
217
|
+
const ModelPricingSchema = z.object({
|
|
218
|
+
provider: z.string().min(1),
|
|
219
|
+
model: z.string().min(1),
|
|
220
|
+
input_price_per_1m: z.number().nonnegative(),
|
|
221
|
+
output_price_per_1m: z.number().nonnegative()
|
|
222
|
+
});
|
|
223
|
+
router.get('/model-pricing', (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
226
|
+
const entries = h.listModelPricing();
|
|
227
|
+
h.close();
|
|
228
|
+
res.json(entries);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
res.status(500).json({ error: error.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
router.post('/model-pricing', (req, res) => {
|
|
235
|
+
const parsed = ModelPricingSchema.safeParse(req.body);
|
|
236
|
+
if (!parsed.success) {
|
|
237
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
241
|
+
h.upsertModelPricing(parsed.data);
|
|
242
|
+
h.close();
|
|
243
|
+
res.json({ success: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
router.put('/model-pricing/:provider/:model', (req, res) => {
|
|
250
|
+
const { provider, model } = req.params;
|
|
251
|
+
const partial = z.object({
|
|
252
|
+
input_price_per_1m: z.number().nonnegative().optional(),
|
|
253
|
+
output_price_per_1m: z.number().nonnegative().optional()
|
|
254
|
+
}).safeParse(req.body);
|
|
255
|
+
if (!partial.success) {
|
|
256
|
+
return res.status(400).json({ error: 'Invalid payload', details: partial.error.issues });
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
260
|
+
const existing = h.listModelPricing().find(e => e.provider === provider && e.model === model);
|
|
261
|
+
if (!existing) {
|
|
262
|
+
h.close();
|
|
263
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
264
|
+
}
|
|
265
|
+
h.upsertModelPricing({
|
|
266
|
+
provider,
|
|
267
|
+
model,
|
|
268
|
+
input_price_per_1m: partial.data.input_price_per_1m ?? existing.input_price_per_1m,
|
|
269
|
+
output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m
|
|
270
|
+
});
|
|
271
|
+
h.close();
|
|
272
|
+
res.json({ success: true });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
res.status(500).json({ error: error.message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
router.delete('/model-pricing/:provider/:model', (req, res) => {
|
|
279
|
+
const { provider, model } = req.params;
|
|
280
|
+
try {
|
|
281
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
282
|
+
const changes = h.deleteModelPricing(provider, model);
|
|
283
|
+
h.close();
|
|
284
|
+
if (changes === 0)
|
|
285
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
286
|
+
res.json({ success: true });
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
res.status(500).json({ error: error.message });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
216
292
|
// Calculate diff between two objects
|
|
217
293
|
const getDiff = (obj1, obj2, prefix = '') => {
|
|
218
294
|
const changes = [];
|
|
@@ -41,7 +41,7 @@ export class SatiMemoryMiddleware {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
async afterAgent(generatedResponse, history) {
|
|
44
|
+
async afterAgent(generatedResponse, history, userSessionId) {
|
|
45
45
|
try {
|
|
46
46
|
await this.service.evaluateAndPersist([
|
|
47
47
|
...history.slice(-5).map(m => ({
|
|
@@ -49,7 +49,7 @@ export class SatiMemoryMiddleware {
|
|
|
49
49
|
content: m.content.toString()
|
|
50
50
|
})),
|
|
51
51
|
{ role: 'assistant', content: generatedResponse }
|
|
52
|
-
]);
|
|
52
|
+
], userSessionId);
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
55
|
display.log(`Error in afterAgent: ${error}`, { source: 'Sati' });
|
|
@@ -50,7 +50,7 @@ export class SatiService {
|
|
|
50
50
|
}))
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
async evaluateAndPersist(conversation) {
|
|
53
|
+
async evaluateAndPersist(conversation, userSessionId) {
|
|
54
54
|
try {
|
|
55
55
|
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
56
56
|
if (!satiConfig)
|
|
@@ -74,7 +74,8 @@ export class SatiService {
|
|
|
74
74
|
new SystemMessage(SATI_EVALUATION_PROMPT),
|
|
75
75
|
new HumanMessage(JSON.stringify(inputPayload, null, 2))
|
|
76
76
|
];
|
|
77
|
-
const
|
|
77
|
+
const satiSessionId = userSessionId ? `sati-evaluation-${userSessionId}` : 'sati-evaluation';
|
|
78
|
+
const history = new SQLiteChatMessageHistory({ sessionId: satiSessionId });
|
|
78
79
|
try {
|
|
79
80
|
const inputMsg = new ToolMessage({
|
|
80
81
|
content: JSON.stringify(inputPayload, null, 2),
|
|
@@ -14,6 +14,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
14
14
|
sessionId;
|
|
15
15
|
limit;
|
|
16
16
|
titleSet = false; // cache: skip setSessionTitleIfNeeded after title is set
|
|
17
|
+
get currentSessionId() {
|
|
18
|
+
return this.sessionId;
|
|
19
|
+
}
|
|
17
20
|
constructor(fields) {
|
|
18
21
|
super();
|
|
19
22
|
this.sessionId = fields.sessionId && fields.sessionId !== '' ? fields.sessionId : '';
|
|
@@ -108,9 +111,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
108
111
|
total_tokens INTEGER,
|
|
109
112
|
cache_read_tokens INTEGER,
|
|
110
113
|
provider TEXT,
|
|
111
|
-
model TEXT
|
|
114
|
+
model TEXT,
|
|
115
|
+
audio_duration_seconds REAL
|
|
112
116
|
);
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
115
119
|
ON messages(session_id);
|
|
116
120
|
|
|
@@ -130,6 +134,33 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
130
134
|
embedding_status TEXT CHECK (embedding_status IN ('none', 'pending', 'embedded', 'failed')) NOT NULL DEFAULT 'none'
|
|
131
135
|
);
|
|
132
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
|
+
|
|
133
164
|
`);
|
|
134
165
|
this.migrateTable();
|
|
135
166
|
}
|
|
@@ -151,13 +182,15 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
151
182
|
'total_tokens',
|
|
152
183
|
'cache_read_tokens',
|
|
153
184
|
'provider',
|
|
154
|
-
'model'
|
|
185
|
+
'model',
|
|
186
|
+
'audio_duration_seconds'
|
|
155
187
|
];
|
|
156
188
|
const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
|
|
189
|
+
const realColumns = new Set(['audio_duration_seconds']);
|
|
157
190
|
for (const col of newColumns) {
|
|
158
191
|
if (!columns.has(col)) {
|
|
159
192
|
try {
|
|
160
|
-
const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
|
|
193
|
+
const type = integerColumns.has(col) ? 'INTEGER' : realColumns.has(col) ? 'REAL' : 'TEXT';
|
|
161
194
|
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
162
195
|
}
|
|
163
196
|
catch (e) {
|
|
@@ -296,6 +329,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
296
329
|
// Extract provider metadata
|
|
297
330
|
const provider = anyMsg.provider_metadata?.provider ?? null;
|
|
298
331
|
const model = anyMsg.provider_metadata?.model ?? null;
|
|
332
|
+
const audioDurationSeconds = usage?.audio_duration_seconds ?? null;
|
|
299
333
|
// Handle special content serialization for Tools
|
|
300
334
|
let finalContent = "";
|
|
301
335
|
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
@@ -318,8 +352,8 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
318
352
|
? message.content
|
|
319
353
|
: JSON.stringify(message.content);
|
|
320
354
|
}
|
|
321
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
322
|
-
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);
|
|
323
357
|
// Verificar se a sessão tem título e definir automaticamente se necessário
|
|
324
358
|
await this.setSessionTitleIfNeeded();
|
|
325
359
|
}
|
|
@@ -346,7 +380,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
346
380
|
async addMessages(messages) {
|
|
347
381
|
if (messages.length === 0)
|
|
348
382
|
return;
|
|
349
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
350
384
|
const insertAll = this.db.transaction((msgs) => {
|
|
351
385
|
for (const message of msgs) {
|
|
352
386
|
let type;
|
|
@@ -373,7 +407,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
373
407
|
else {
|
|
374
408
|
finalContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
375
409
|
}
|
|
376
|
-
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);
|
|
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);
|
|
377
411
|
}
|
|
378
412
|
});
|
|
379
413
|
try {
|
|
@@ -440,9 +474,18 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
440
474
|
try {
|
|
441
475
|
const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
|
|
442
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();
|
|
443
485
|
return {
|
|
444
486
|
totalInputTokens: row.totalInput || 0,
|
|
445
|
-
totalOutputTokens: row.totalOutput || 0
|
|
487
|
+
totalOutputTokens: row.totalOutput || 0,
|
|
488
|
+
totalEstimatedCostUsd: costRow.totalCost ?? null
|
|
446
489
|
};
|
|
447
490
|
}
|
|
448
491
|
catch (error) {
|
|
@@ -474,31 +517,58 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
474
517
|
*/
|
|
475
518
|
async getUsageStatsByProviderAndModel() {
|
|
476
519
|
try {
|
|
477
|
-
const stmt = this.db.prepare(`SELECT
|
|
478
|
-
provider,
|
|
479
|
-
COALESCE(model, 'unknown') as model,
|
|
480
|
-
SUM(input_tokens) as totalInputTokens,
|
|
481
|
-
SUM(output_tokens) as totalOutputTokens,
|
|
482
|
-
SUM(total_tokens) as totalTokens,
|
|
483
|
-
COUNT(*) as messageCount
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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`);
|
|
488
535
|
const rows = stmt.all();
|
|
489
|
-
return rows.map((row) =>
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
});
|
|
497
555
|
}
|
|
498
556
|
catch (error) {
|
|
499
557
|
throw new Error(`Failed to get grouped usage stats: ${error}`);
|
|
500
558
|
}
|
|
501
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
|
+
}
|
|
502
572
|
/**
|
|
503
573
|
* Clears all messages for the current session from the database.
|
|
504
574
|
*/
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -229,7 +229,10 @@ You maintain intent until resolution.
|
|
|
229
229
|
const lastMessage = response.messages[response.messages.length - 1];
|
|
230
230
|
const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
231
231
|
// Sati Middleware: Evaluation (Fire and forget)
|
|
232
|
-
this.
|
|
232
|
+
const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
|
|
233
|
+
? this.history.currentSessionId
|
|
234
|
+
: undefined;
|
|
235
|
+
this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
|
|
233
236
|
.catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
|
|
234
237
|
return responseContent;
|
|
235
238
|
}
|
|
@@ -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
|
}
|