Skip to content

Commit c6d5a36

Browse files
committed
release: merge develop into main for v0.20.1
2 parents 759478c + 12b1e35 commit c6d5a36

6 files changed

Lines changed: 65 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.20.1] - 2026-04-13
9+
10+
### Added
11+
12+
- **Image generation cost estimates** — each image in the Costs page now shows an estimated USD cost based on model pricing (Gemini Flash $0.039/img, FLUX.2 $0.03/img, GPT-5 Image $0.04/img, etc.). Total image cost shown in section header and included in the "Total (All)" KPI card
13+
814
## [0.20.0] - 2026-04-13
915

1016
### Added

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@evoapi/evo-nexus",
3-
"version": "0.20.0",
3+
"version": "0.20.1",
44
"description": "Unofficial open source toolkit for Claude Code — AI-powered business operating system",
55
"keywords": [
66
"claude-code",

dashboard/backend/routes/routines.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,45 @@ def list_adws():
8484
return jsonify(scripts)
8585

8686

87+
# Pricing per model for image generation (USD)
88+
# Format: { model_substring: { per_image: float, input_per_1m: float, output_per_1m: float } }
89+
IMAGE_MODEL_PRICING = {
90+
"gemini-3.1-flash-image-preview": {"per_image": 0.039, "input_per_1m": 0.075, "output_per_1m": 0.30},
91+
"gemini-2.0-flash": {"per_image": 0.039, "input_per_1m": 0.10, "output_per_1m": 0.40},
92+
"flux-2": {"per_image": 0.03, "input_per_1m": 0, "output_per_1m": 0},
93+
"flux.2": {"per_image": 0.03, "input_per_1m": 0, "output_per_1m": 0},
94+
"gpt-5-image": {"per_image": 0.04, "input_per_1m": 0.005, "output_per_1m": 0.015},
95+
"seedream": {"per_image": 0.02, "input_per_1m": 0, "output_per_1m": 0},
96+
"riverflow": {"per_image": 0.02, "input_per_1m": 0, "output_per_1m": 0},
97+
}
98+
99+
100+
def _estimate_image_cost(entry: dict) -> float:
101+
"""Estimate USD cost for a single image generation entry."""
102+
model = entry.get("model", "").lower()
103+
tokens = entry.get("token_usage", {})
104+
total_tokens = tokens.get("total_tokens", 0)
105+
106+
# Find matching pricing by model substring
107+
pricing = None
108+
for key, p in IMAGE_MODEL_PRICING.items():
109+
if key in model:
110+
pricing = p
111+
break
112+
113+
if not pricing:
114+
# Fallback: assume $0.03/image for unknown models
115+
return 0.03
116+
117+
cost = pricing["per_image"]
118+
if total_tokens > 0 and pricing["input_per_1m"] > 0:
119+
cost += (total_tokens / 1_000_000) * pricing["input_per_1m"]
120+
return round(cost, 6)
121+
122+
87123
@bp.route("/api/routines/image-costs")
88124
def get_image_costs():
89-
"""Return AI image generation cost entries."""
125+
"""Return AI image generation cost entries with estimated costs."""
90126
costs_path = LOGS_DIR / "ai-image-creator-costs.json"
91127
content = safe_read(costs_path)
92128
if not content:
@@ -99,11 +135,15 @@ def get_image_costs():
99135
total_tokens = 0
100136
total_seconds = 0.0
101137
total_bytes = 0
138+
total_cost = 0.0
102139
for e in entries:
103140
tokens = e.get("token_usage", {})
104141
total_tokens += tokens.get("total_tokens", 0)
105142
total_seconds += e.get("elapsed_seconds", 0)
106143
total_bytes += e.get("size_bytes", 0)
144+
est = _estimate_image_cost(e)
145+
e["estimated_cost_usd"] = est
146+
total_cost += est
107147

108148
return jsonify({
109149
"entries": entries,
@@ -112,5 +152,6 @@ def get_image_costs():
112152
"total_tokens": total_tokens,
113153
"total_seconds": round(total_seconds, 1),
114154
"total_bytes": total_bytes,
155+
"total_cost_usd": round(total_cost, 4),
115156
},
116157
})

dashboard/frontend/src/pages/Costs.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ interface ImageCostEntry {
100100
size_bytes: number
101101
elapsed_seconds: number
102102
token_usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }
103+
estimated_cost_usd?: number
103104
}
104105

105106
interface ImageCosts {
106107
entries: ImageCostEntry[]
107-
totals: { count: number; total_tokens: number; total_seconds: number; total_bytes: number }
108+
totals: { count: number; total_tokens: number; total_seconds: number; total_bytes: number; total_cost_usd?: number }
108109
}
109110

110111
function relativeTime(ts: string): string {
@@ -174,6 +175,8 @@ export default function Costs() {
174175
}
175176

176177
const totalRuns = (data.by_routine || []).reduce((sum, r) => sum + Number(r.runs || 0), 0)
178+
const imageTotalCost = imageCosts?.totals?.total_cost_usd || 0
179+
const grandTotal = Number(data.total_cost || 0) + imageTotalCost
177180
const avgCostPerRun = totalRuns > 0 ? data.total_cost / totalRuns : 0
178181

179182
return (
@@ -199,9 +202,9 @@ export default function Costs() {
199202
icon={Activity}
200203
/>
201204
<StatCard
202-
label="Month Estimate"
203-
value={`$${Number(data.month_estimate || data.total_cost || 0).toFixed(2)}`}
204-
subtitle="Projected total"
205+
label="Total (All)"
206+
value={`$${grandTotal.toFixed(2)}`}
207+
subtitle={imageTotalCost > 0 ? `Routines + ${imageCosts?.totals?.count || 0} images` : 'Routines total'}
205208
icon={Zap}
206209
/>
207210
<StatCard
@@ -324,6 +327,9 @@ export default function Costs() {
324327
<span>{imageCosts.totals.total_tokens.toLocaleString()} tokens</span>
325328
<span>{formatBytes(imageCosts.totals.total_bytes)}</span>
326329
<span>{imageCosts.totals.total_seconds}s total</span>
330+
{imageCosts.totals.total_cost_usd !== undefined && (
331+
<span className="text-[#00FFA7] font-medium">${imageCosts.totals.total_cost_usd.toFixed(2)}</span>
332+
)}
327333
</div>
328334
</div>
329335
<div className="overflow-x-auto">
@@ -336,6 +342,7 @@ export default function Costs() {
336342
<th className="text-right p-4 pb-3">Tokens</th>
337343
<th className="text-right p-4 pb-3">Size</th>
338344
<th className="text-right p-4 pb-3">Time</th>
345+
<th className="text-right p-4 pb-3">Est. Cost</th>
339346
<th className="text-right p-4 pb-3">When</th>
340347
</tr>
341348
</thead>
@@ -354,6 +361,9 @@ export default function Costs() {
354361
<td className="p-4 text-right text-[#8b949e] tabular-nums text-[13px]">{e.token_usage.total_tokens.toLocaleString()}</td>
355362
<td className="p-4 text-right text-[#8b949e] tabular-nums text-[13px]">{formatBytes(e.size_bytes)}</td>
356363
<td className="p-4 text-right text-[#667085] tabular-nums text-[13px]">{e.elapsed_seconds.toFixed(1)}s</td>
364+
<td className="p-4 text-right tabular-nums text-[13px] text-[#00FFA7]">
365+
{e.estimated_cost_usd !== undefined ? `$${e.estimated_cost_usd.toFixed(4)}` : '—'}
366+
</td>
357367
<td className="p-4 text-right text-[#667085] text-[13px] whitespace-nowrap">{relativeTime(e.timestamp)}</td>
358368
</tr>
359369
))}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "evo-nexus"
3-
version = "0.20.0"
3+
version = "0.20.1"
44
description = "Unofficial open source toolkit for Claude Code — AI-powered business operating system"
55
requires-python = ">=3.10"
66
dependencies = [

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)