Skip to content

Commit 102f775

Browse files
committed
Set the cost per models
1 parent 1275a9c commit 102f775

File tree

4 files changed

+130
-51
lines changed

4 files changed

+130
-51
lines changed

README.md

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,49 @@ curl "http://localhost:8000/admin/weekly-summary/next-run"
295295

296296
## Costs
297297

298-
Approximate API costs per operation:
299-
- **Whisper transcription**: $0.006 per minute of audio
300-
- **GPT-4o-mini summarization**: ~$0.001-0.01 per summary (avg 1,500 tokens)
301-
- **GPT-4o weekly summary**: ~$0.05-0.15 per summary (avg 10,000 tokens)
302-
- **Gemini**: Free tier available (15 requests/minute, 1 million tokens/day)
298+
### Current Model Pricing (Per 1M Tokens)
299+
300+
| Model | Input | Output | Notes |
301+
|-------|-------|--------|-------|
302+
| **OpenAI** ||||
303+
| gpt-5-nano | $0.05 | $0.40 | Ultra-lightweight for high-volume tasks |
304+
| gpt-4o-mini | $0.15 | $0.60 | Reliable workhorse (recommended) |
305+
| gpt-4o | $2.50 | $10.00 | Higher quality, stable pricing |
306+
| gpt-5.2 | $1.75 | $14.00 | Extended thinking capacity |
307+
| whisper-1 | - | - | $0.006 per minute of audio |
308+
| **Google Gemini** ||||
309+
| gemini-2.5-flash | $0.15 | $0.60 | Fast, comparable to gpt-4o-mini (recommended) |
310+
| gemini-3-flash-preview | $0.50 | $3.00 | Speed-optimized preview |
311+
| gemini-3-pro-preview | $2.00 | $12.00 | High quality (≤200k context) |
312+
313+
### Estimated Costs Per Operation
314+
315+
**Using default configuration (Whisper + Gemini 2.5 Flash):**
316+
- **Video transcription** (Whisper): $0.006 per minute of audio
317+
- **Video summarization** (Gemini 2.5 Flash): ~$0.0003-0.001 per summary
318+
- Typical: 2,000 input tokens (transcript) + 500 output tokens
319+
- Cost: (2,000 × $0.15 + 500 × $0.60) / 1,000,000 = **$0.0006**
320+
- **Weekly summary** (Gemini 2.5 Flash): ~$0.003-0.01 per summary
321+
- Typical: 10,000 input tokens + 2,000 output tokens
322+
- Cost: (10,000 × $0.15 + 2,000 × $0.60) / 1,000,000 = **$0.0027**
323+
- **Book suggestions** (Gemini 2.5 Flash): ~$0.0002-0.0005 per request
324+
- Typical: 1,000 input tokens + 100 output tokens
325+
- Cost: (1,000 × $0.15 + 100 × $0.60) / 1,000,000 = **$0.0002**
326+
327+
**Example monthly cost** (watching 30 hours/month):
328+
- Transcription: 30 hours × 60 min × $0.006 = **$10.80**
329+
- Summarization: 30 videos × $0.0006 = **$0.02**
330+
- Weekly summaries: 4 weeks × $0.0027 = **$0.01**
331+
- **Total: ~$10.83/month**
332+
333+
**Gemini Free Tier:**
334+
- 15 requests per minute
335+
- 1 million tokens per day
336+
- Summarization and weekly summaries are essentially free under these limits
337+
- Only transcription (Whisper) has costs
303338

304339
**Cost tracking:**
305-
Use the LLM usage statistics endpoints above to monitor your actual usage and calculate precise costs based on current provider pricing.
340+
Use the `/admin/llm-usage/stats` and `/admin/llm-usage/summary` endpoints to monitor your actual usage and calculate precise costs based on current provider pricing.
306341

307342
# Server configuration
308343

templates/stats.html

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,50 @@ <h2><i class="fas fa-table"></i> Detailed Breakdown</h2>
432432
let providerChart = null;
433433
let featureChart = null;
434434

435+
// Model pricing (per 1M tokens)
436+
const MODEL_PRICING = {
437+
// OpenAI models
438+
'whisper-1': { input: 0, output: 0 }, // Whisper is priced per minute, not tokens
439+
'gpt-5-nano': { input: 0.05, output: 0.40 },
440+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
441+
'gpt-4o': { input: 2.50, output: 10.00 },
442+
'gpt-5.2': { input: 1.75, output: 14.00 },
443+
// Gemini models
444+
'gemini-2.5-flash': { input: 0.15, output: 0.60 },
445+
'gemini-2.5-flash-preview-tts': { input: 0.15, output: 0.60 },
446+
'gemini-3-flash-preview': { input: 0.50, output: 3.00 },
447+
'gemini-3-pro-preview': { input: 2.00, output: 12.00 },
448+
'gemini-1.5-flash': { input: 0.15, output: 0.60 },
449+
'gemini-1.5-pro': { input: 2.00, output: 12.00 },
450+
};
451+
452+
function calculateCost(summary) {
453+
let totalCost = 0;
454+
455+
summary.by_provider_model_feature.forEach(item => {
456+
const model = item.model;
457+
const pricing = MODEL_PRICING[model];
458+
459+
if (pricing) {
460+
const inputTokens = item.total_prompt_tokens || 0;
461+
const outputTokens = item.total_response_tokens || 0;
462+
463+
// Calculate cost: (tokens / 1,000,000) * price_per_million
464+
const inputCost = (inputTokens / 1000000) * pricing.input;
465+
const outputCost = (outputTokens / 1000000) * pricing.output;
466+
467+
totalCost += inputCost + outputCost;
468+
} else {
469+
// Unknown model - use average rate as fallback
470+
console.warn(`Unknown model pricing: ${model}, using average rate`);
471+
const totalTokens = item.total_tokens || 0;
472+
totalCost += (totalTokens / 1000000) * 1.0; // $1 per 1M tokens fallback
473+
}
474+
});
475+
476+
return totalCost;
477+
}
478+
435479
async function loadStatistics() {
436480
try {
437481
document.getElementById('loading').style.display = 'block';
@@ -478,9 +522,18 @@ <h2><i class="fas fa-table"></i> Detailed Breakdown</h2>
478522
document.getElementById('token-breakdown').textContent =
479523
`${totals.total_prompt_tokens.toLocaleString()} input / ${totals.total_response_tokens.toLocaleString()} output`;
480524

481-
// Estimate cost (rough average: $0.000001 per token)
482-
const estimatedCost = (totals.total_tokens * 0.000001).toFixed(2);
483-
document.getElementById('estimated-cost').textContent = `$${estimatedCost}`;
525+
// Calculate accurate cost based on actual model pricing
526+
const estimatedCost = calculateCost(summary);
527+
document.getElementById('estimated-cost').textContent = `$${estimatedCost.toFixed(4)}`;
528+
529+
// Update sub-label to show it's actual pricing
530+
const costCard = document.getElementById('estimated-cost').parentElement;
531+
const subLabel = costCard.querySelector('.stat-sub');
532+
if (estimatedCost < 0.01) {
533+
subLabel.textContent = `≈ $${(estimatedCost * 100).toFixed(2)}¢`;
534+
} else {
535+
subLabel.textContent = 'Based on actual model pricing';
536+
}
484537

485538
// Find most used
486539
if (summary.by_provider_model_feature.length > 0) {

tests/conftest.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,42 @@ def mock_config():
2929
return config
3030

3131

32-
@pytest.fixture
33-
def temp_db():
34-
"""Temporary SQLite database for testing."""
32+
@pytest.fixture(autouse=True)
33+
def db_path(monkeypatch):
34+
"""Temporary SQLite database for testing.
35+
36+
This fixture runs automatically for ALL tests to ensure they never
37+
touch the development database.
38+
39+
Renamed from temp_db to db_path to match existing test expectations.
40+
"""
3541
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
36-
db_path = f.name
42+
temp_path = f.name
3743

38-
# Set environment variable for database
39-
original_db = os.environ.get("DATABASE_PATH")
40-
os.environ["DATABASE_PATH"] = db_path
44+
# Set environment variable for database using monkeypatch
45+
# This ensures proper cleanup even if tests fail
46+
monkeypatch.setenv("DATABASE_PATH", temp_path)
4147

42-
yield db_path
48+
# Force reload of database module to pick up new DATABASE_PATH
49+
import services.database
50+
import importlib
4351

44-
# Cleanup
45-
if original_db:
46-
os.environ["DATABASE_PATH"] = original_db
47-
else:
48-
os.environ.pop("DATABASE_PATH", None)
52+
importlib.reload(services.database)
4953

50-
if os.path.exists(db_path):
51-
os.unlink(db_path)
54+
# Initialize the database with all required tables
55+
from services.database import init_database
56+
57+
init_database()
58+
59+
yield temp_path
60+
61+
# Cleanup
62+
if os.path.exists(temp_path):
63+
try:
64+
os.unlink(temp_path)
65+
except Exception:
66+
# Ignore cleanup errors
67+
pass
5268

5369

5470
@pytest.fixture

tests/services/test_database.py

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
"""Tests for database service."""
22

3-
import os
4-
import tempfile
5-
import pytest
63
from services.database import (
74
init_database,
85
add_to_history,
@@ -17,30 +14,8 @@
1714
get_db_connection,
1815
)
1916

20-
21-
@pytest.fixture(autouse=True)
22-
def db_path(monkeypatch):
23-
"""Create temporary database for testing."""
24-
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
25-
path = f.name
26-
27-
# Set environment variable BEFORE importing services
28-
monkeypatch.setenv("DATABASE_PATH", path)
29-
30-
# Reload the database module to pick up new path
31-
import services.database
32-
import importlib
33-
34-
importlib.reload(services.database)
35-
36-
yield path
37-
38-
# Cleanup
39-
if os.path.exists(path):
40-
try:
41-
os.unlink(path)
42-
except Exception:
43-
pass
17+
# Note: The temp_db fixture from conftest.py is used automatically
18+
# for all tests (autouse=True), so no need to define it here
4419

4520

4621
class TestDatabaseInit:

0 commit comments

Comments
 (0)