arkaos 3.36.0 → 3.38.0
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/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.38.0
|
|
@@ -53,7 +53,8 @@ interface TrendResponse {
|
|
|
53
53
|
|
|
54
54
|
type Period = 'today' | 'week' | 'month' | 'all'
|
|
55
55
|
|
|
56
|
-
const { fetchApi } = useApi()
|
|
56
|
+
const { fetchApi, apiBase } = useApi()
|
|
57
|
+
const toast = useToast()
|
|
57
58
|
const period = ref<Period>('today')
|
|
58
59
|
|
|
59
60
|
const periodOptions: { label: string; value: Period }[] = [
|
|
@@ -73,6 +74,36 @@ const {
|
|
|
73
74
|
{ query: computed(() => ({ period: period.value })) },
|
|
74
75
|
)
|
|
75
76
|
|
|
77
|
+
// PR91d v3.38.0 — CSV export of the telemetry rows for the current period.
|
|
78
|
+
async function exportCsv() {
|
|
79
|
+
try {
|
|
80
|
+
const blob = await $fetch<Blob>(
|
|
81
|
+
`${apiBase}/api/llm-costs/export.csv`,
|
|
82
|
+
{ query: { period: period.value }, responseType: 'blob' },
|
|
83
|
+
)
|
|
84
|
+
const url = URL.createObjectURL(blob)
|
|
85
|
+
const a = document.createElement('a')
|
|
86
|
+
a.href = url
|
|
87
|
+
a.download = `arkaos-costs-${period.value}.csv`
|
|
88
|
+
document.body.appendChild(a)
|
|
89
|
+
a.click()
|
|
90
|
+
a.remove()
|
|
91
|
+
URL.revokeObjectURL(url)
|
|
92
|
+
toast.add({
|
|
93
|
+
title: 'CSV downloaded',
|
|
94
|
+
description: `arkaos-costs-${period.value}.csv`,
|
|
95
|
+
color: 'success',
|
|
96
|
+
icon: 'i-lucide-download',
|
|
97
|
+
})
|
|
98
|
+
} catch (err) {
|
|
99
|
+
toast.add({
|
|
100
|
+
title: 'Export failed',
|
|
101
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
102
|
+
color: 'error',
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
76
107
|
// PR90c v3.33.0 — let the operator pick 7d / 14d / 30d.
|
|
77
108
|
const trendDays = ref<7 | 14 | 30>(7)
|
|
78
109
|
const trendDaysOptions = [
|
|
@@ -204,6 +235,14 @@ async function refreshAll() {
|
|
|
204
235
|
size="sm"
|
|
205
236
|
class="w-32"
|
|
206
237
|
/>
|
|
238
|
+
<UButton
|
|
239
|
+
label="Export CSV"
|
|
240
|
+
icon="i-lucide-download"
|
|
241
|
+
variant="soft"
|
|
242
|
+
size="sm"
|
|
243
|
+
class="ml-2"
|
|
244
|
+
@click="exportCsv"
|
|
245
|
+
/>
|
|
207
246
|
<UButton
|
|
208
247
|
label="Refresh"
|
|
209
248
|
variant="ghost"
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TableColumn } from '@nuxt/ui'
|
|
9
9
|
|
|
10
|
+
interface WorkflowPhase {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
gate_type: string
|
|
15
|
+
agent_count: number
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
interface Workflow {
|
|
11
19
|
id: string
|
|
12
20
|
name: string
|
|
@@ -15,6 +23,7 @@ interface Workflow {
|
|
|
15
23
|
tier: string
|
|
16
24
|
command: string
|
|
17
25
|
phases_count: number
|
|
26
|
+
phases: WorkflowPhase[]
|
|
18
27
|
file: string
|
|
19
28
|
content: string
|
|
20
29
|
}
|
|
@@ -35,7 +44,16 @@ interface WorkflowRun {
|
|
|
35
44
|
}
|
|
36
45
|
const runs = ref<WorkflowRun[]>([])
|
|
37
46
|
const runsLoading = ref(false)
|
|
38
|
-
const sidePanelTab = ref<'yaml' | 'runs'>('
|
|
47
|
+
const sidePanelTab = ref<'flow' | 'yaml' | 'runs'>('flow')
|
|
48
|
+
|
|
49
|
+
function gateColor(gateType: string): 'primary' | 'warning' | 'error' | 'neutral' {
|
|
50
|
+
const m: Record<string, 'primary' | 'warning' | 'error' | 'neutral'> = {
|
|
51
|
+
user_approval: 'warning',
|
|
52
|
+
quality_gate: 'error',
|
|
53
|
+
automatic: 'primary',
|
|
54
|
+
}
|
|
55
|
+
return m[gateType] ?? 'neutral'
|
|
56
|
+
}
|
|
39
57
|
|
|
40
58
|
async function loadRuns(id: string) {
|
|
41
59
|
runsLoading.value = true
|
|
@@ -152,7 +170,7 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
152
170
|
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
153
171
|
td: 'border-b border-default',
|
|
154
172
|
}"
|
|
155
|
-
@select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = '
|
|
173
|
+
@select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'flow'; runs = []; loadRuns(row.original.id) }"
|
|
156
174
|
>
|
|
157
175
|
<template #name-cell="{ row }">
|
|
158
176
|
<div class="min-w-0">
|
|
@@ -205,6 +223,14 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
205
223
|
{{ selected.description }}
|
|
206
224
|
</p>
|
|
207
225
|
<div class="flex items-center gap-1 mt-3 text-xs">
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class="px-2 py-1 rounded-md transition-colors"
|
|
229
|
+
:class="sidePanelTab === 'flow' ? 'bg-elevated/60 text-default font-semibold' : 'text-muted hover:text-default'"
|
|
230
|
+
@click="sidePanelTab = 'flow'"
|
|
231
|
+
>
|
|
232
|
+
Flow
|
|
233
|
+
</button>
|
|
208
234
|
<button
|
|
209
235
|
type="button"
|
|
210
236
|
class="px-2 py-1 rounded-md transition-colors"
|
|
@@ -223,7 +249,43 @@ const columns: TableColumn<Workflow>[] = [
|
|
|
223
249
|
</button>
|
|
224
250
|
</div>
|
|
225
251
|
</div>
|
|
226
|
-
<div v-if="sidePanelTab === '
|
|
252
|
+
<div v-if="sidePanelTab === 'flow'" class="p-4">
|
|
253
|
+
<ol v-if="selected.phases.length > 0" class="relative border-l border-default ml-2 space-y-3">
|
|
254
|
+
<li
|
|
255
|
+
v-for="(ph, idx) in selected.phases"
|
|
256
|
+
:key="ph.id || idx"
|
|
257
|
+
class="ml-4"
|
|
258
|
+
>
|
|
259
|
+
<span class="absolute -left-1.5 size-3 rounded-full bg-primary border border-primary/60" />
|
|
260
|
+
<div class="rounded-lg border border-default p-3 bg-elevated/20">
|
|
261
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
262
|
+
<span class="font-mono text-xs text-muted">{{ idx + 1 }}.</span>
|
|
263
|
+
<span class="font-semibold">{{ ph.name || ph.id }}</span>
|
|
264
|
+
<UBadge
|
|
265
|
+
v-if="ph.gate_type"
|
|
266
|
+
:label="ph.gate_type"
|
|
267
|
+
:color="gateColor(ph.gate_type)"
|
|
268
|
+
variant="subtle"
|
|
269
|
+
size="xs"
|
|
270
|
+
/>
|
|
271
|
+
<UBadge
|
|
272
|
+
v-if="ph.agent_count > 0"
|
|
273
|
+
:label="`${ph.agent_count} agent${ph.agent_count === 1 ? '' : 's'}`"
|
|
274
|
+
variant="outline"
|
|
275
|
+
size="xs"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<p v-if="ph.description" class="text-xs text-muted mt-1">
|
|
279
|
+
{{ ph.description }}
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
</li>
|
|
283
|
+
</ol>
|
|
284
|
+
<div v-else class="py-6 text-center text-sm text-muted">
|
|
285
|
+
No phases defined.
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<div v-else-if="sidePanelTab === 'yaml'" class="overflow-x-auto">
|
|
227
289
|
<pre class="p-4 text-xs font-mono whitespace-pre">{{ selected.content }}</pre>
|
|
228
290
|
</div>
|
|
229
291
|
<div v-else class="p-4">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1848,12 +1848,33 @@ def workflows_list():
|
|
|
1848
1848
|
"tier": str(raw.get("tier") or ""),
|
|
1849
1849
|
"command": str(raw.get("command") or ""),
|
|
1850
1850
|
"phases_count": len(phases),
|
|
1851
|
+
"phases": _summarise_phases(phases), # PR91c v3.37.0
|
|
1851
1852
|
"file": rel,
|
|
1852
1853
|
"content": content,
|
|
1853
1854
|
})
|
|
1854
1855
|
return {"workflows": out}
|
|
1855
1856
|
|
|
1856
1857
|
|
|
1858
|
+
def _summarise_phases(phases: list) -> list[dict]:
|
|
1859
|
+
"""PR91c v3.37.0 — distil each phase down to what the flow stepper needs."""
|
|
1860
|
+
out: list[dict] = []
|
|
1861
|
+
for p in phases:
|
|
1862
|
+
if not isinstance(p, dict):
|
|
1863
|
+
continue
|
|
1864
|
+
gate = p.get("gate") if isinstance(p.get("gate"), dict) else {}
|
|
1865
|
+
agents = p.get("agents") if isinstance(p.get("agents"), list) else []
|
|
1866
|
+
out.append({
|
|
1867
|
+
"id": str(p.get("id") or ""),
|
|
1868
|
+
"name": str(p.get("name") or ""),
|
|
1869
|
+
"description": str(p.get("description") or ""),
|
|
1870
|
+
"gate_type": str(gate.get("type") or ""),
|
|
1871
|
+
"agent_count": sum(
|
|
1872
|
+
1 for a in agents if isinstance(a, dict) and a.get("agent_id")
|
|
1873
|
+
),
|
|
1874
|
+
})
|
|
1875
|
+
return out
|
|
1876
|
+
|
|
1877
|
+
|
|
1857
1878
|
# --- Sidebar stats widget (PR87d v3.22.0) ---
|
|
1858
1879
|
|
|
1859
1880
|
@app.get("/api/sidebar-stats")
|
|
@@ -2489,6 +2510,56 @@ def llm_costs(period: str = "today"):
|
|
|
2489
2510
|
}
|
|
2490
2511
|
|
|
2491
2512
|
|
|
2513
|
+
@app.get("/api/llm-costs/export.csv")
|
|
2514
|
+
def llm_costs_export(period: str = "month"):
|
|
2515
|
+
"""PR91d v3.38.0 — stream the telemetry rows for the period as CSV.
|
|
2516
|
+
|
|
2517
|
+
Returns ``text/csv`` with a header row + every row in the selected
|
|
2518
|
+
period. Period values match `summarise()`: today / week / month / all.
|
|
2519
|
+
"""
|
|
2520
|
+
try:
|
|
2521
|
+
from core.runtime.llm_cost_telemetry import (
|
|
2522
|
+
VALID_PERIODS,
|
|
2523
|
+
_load_slice,
|
|
2524
|
+
_period_cutoff,
|
|
2525
|
+
)
|
|
2526
|
+
except Exception:
|
|
2527
|
+
return {"error": "telemetry unavailable"}
|
|
2528
|
+
if period not in VALID_PERIODS:
|
|
2529
|
+
period = "month"
|
|
2530
|
+
|
|
2531
|
+
entries, _ = _load_slice(None, _period_cutoff(period, now=None))
|
|
2532
|
+
import csv
|
|
2533
|
+
import io
|
|
2534
|
+
buffer = io.StringIO()
|
|
2535
|
+
writer = csv.writer(buffer)
|
|
2536
|
+
writer.writerow([
|
|
2537
|
+
"ts", "session_id", "provider", "model", "category",
|
|
2538
|
+
"tokens_in", "tokens_out", "cached_tokens", "estimated_cost_usd",
|
|
2539
|
+
])
|
|
2540
|
+
for entry in entries:
|
|
2541
|
+
writer.writerow([
|
|
2542
|
+
entry.get("ts", ""),
|
|
2543
|
+
entry.get("session_id", ""),
|
|
2544
|
+
entry.get("provider", ""),
|
|
2545
|
+
entry.get("model", ""),
|
|
2546
|
+
entry.get("category", ""),
|
|
2547
|
+
entry.get("tokens_in", ""),
|
|
2548
|
+
entry.get("tokens_out", ""),
|
|
2549
|
+
entry.get("cached_tokens", ""),
|
|
2550
|
+
entry.get("estimated_cost_usd", ""),
|
|
2551
|
+
])
|
|
2552
|
+
filename = f"arkaos-costs-{period}.csv"
|
|
2553
|
+
from fastapi import Response
|
|
2554
|
+
return Response(
|
|
2555
|
+
content=buffer.getvalue(),
|
|
2556
|
+
media_type="text/csv",
|
|
2557
|
+
headers={
|
|
2558
|
+
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
2559
|
+
},
|
|
2560
|
+
)
|
|
2561
|
+
|
|
2562
|
+
|
|
2492
2563
|
@app.get("/api/llm-costs/trend")
|
|
2493
2564
|
def llm_costs_trend(days: int = 7):
|
|
2494
2565
|
"""Day-by-day rolling totals from the cost telemetry.
|