Skip to content

Commit e0576aa

Browse files
author
frontend-fable
committed
feat(console): build contract-bound cost surface
1 parent 352a238 commit e0576aa

7 files changed

Lines changed: 375 additions & 0 deletions

File tree

apps/console/src/lib/api/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import type {
2323
DeliveryItem,
2424
ExecutorItem,
2525
HeartbeatItem,
26+
GovernanceItem,
27+
GovernancePool,
2628
LeaseItem,
2729
EdgeRegistryItem,
2830
EdgeSessionItem,
@@ -134,6 +136,15 @@ export async function readHeartbeats(
134136
});
135137
return json<ReadEnvelope<HeartbeatItem>>(res);
136138
}
139+
export async function readGovernance(
140+
fetchFn: typeof fetch = fetch,
141+
): Promise<ReadEnvelope<GovernanceItem> & { pool?: GovernancePool }> {
142+
const res = await fetchFn(`${base()}/governance?limit=1000`, {
143+
headers: { accept: "application/json" },
144+
credentials: "include",
145+
});
146+
return json<ReadEnvelope<GovernanceItem> & { pool?: GovernancePool }>(res);
147+
}
137148
export async function readTasks(fetchFn: typeof fetch = fetch): Promise<ReadEnvelope<TaskItem>> {
138149
let cursor: string | null = null;
139150
let first: ReadEnvelope<TaskItem> | null = null;

apps/console/src/lib/api/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,23 @@ export type HeartbeatState =
398398
| "stopped";
399399
export type Autonomy = "auto" | "ask" | "readonly" | "paused";
400400
export type BudgetLightColor = "green" | "yellow" | "red";
401+
export interface GovernanceItem extends Extra {
402+
agent: string;
403+
light: BudgetLightColor;
404+
tokens_spent: number;
405+
granted_tokens?: number | null;
406+
grant_expires_epoch?: number | null;
407+
tier?: "haiku" | "sonnet" | "opus" | null;
408+
rate_limit_hits: number;
409+
last_rate_limited_epoch?: number | null;
410+
rate_limit_reset_epoch?: number | null;
411+
}
412+
export interface GovernancePool extends Extra {
413+
pool_tokens: number;
414+
pool_spent: number;
415+
fleet_mode: "parallel" | "sequential";
416+
cascade_active: boolean;
417+
}
401418
export interface RosterItem extends Extra {
402419
handle: string;
403420
host?: string | null;

apps/console/src/lib/components/icons.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ChartLine from "@lucide/svelte/icons/chart-line";
22
import CircleCheck from "@lucide/svelte/icons/circle-check";
33
import CircleHelp from "@lucide/svelte/icons/circle-help";
4+
import Coins from "@lucide/svelte/icons/coins";
45
import DoorOpen from "@lucide/svelte/icons/door-open";
56
import Ellipsis from "@lucide/svelte/icons/ellipsis";
67
import Kanban from "@lucide/svelte/icons/kanban";
@@ -11,13 +12,15 @@ import Mails from "@lucide/svelte/icons/mails";
1112
import MousePointer2 from "@lucide/svelte/icons/mouse-pointer-2";
1213
import RadioTower from "@lucide/svelte/icons/radio-tower";
1314
import ReceiptText from "@lucide/svelte/icons/receipt-text";
15+
import Search from "@lucide/svelte/icons/search";
1416
import Send from "@lucide/svelte/icons/send";
1517
import Server from "@lucide/svelte/icons/server";
1618
import ShieldCheck from "@lucide/svelte/icons/shield-check";
1719
import Sparkles from "@lucide/svelte/icons/sparkles";
1820
import SquareTerminal from "@lucide/svelte/icons/square-terminal";
1921
import TriangleAlert from "@lucide/svelte/icons/triangle-alert";
2022
import UsersRound from "@lucide/svelte/icons/users-round";
23+
import X from "@lucide/svelte/icons/x";
2124
/**
2225
* Lucide icon registry. Every icon the shell uses is imported explicitly (keeps tree-shaking
2326
* honest) and mapped by its Lucide id so string-keyed callers (nav, fix_ops, signage) resolve
@@ -33,6 +36,7 @@ export const ICONS: Record<string, LucideIcon> = {
3336
"chart-line": ChartLine,
3437
"circle-check": CircleCheck,
3538
"circle-help": CircleHelp,
39+
coins: Coins,
3640
"door-open": DoorOpen,
3741
ellipsis: Ellipsis,
3842
kanban: Kanban,
@@ -43,11 +47,13 @@ export const ICONS: Record<string, LucideIcon> = {
4347
"mouse-pointer-2": MousePointer2,
4448
"radio-tower": RadioTower,
4549
"receipt-text": ReceiptText,
50+
search: Search,
4651
send: Send,
4752
server: Server,
4853
"shield-check": ShieldCheck,
4954
sparkles: Sparkles,
5055
"square-terminal": SquareTerminal,
5156
"triangle-alert": TriangleAlert,
5257
"users-round": UsersRound,
58+
x: X,
5359
};

apps/console/src/lib/data/cost.ts

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import type { GovernanceItem, GovernancePool } from "$lib/api/types";
2+
3+
export interface DailyCost {
4+
day: string;
5+
spend: number;
6+
prior: number;
7+
forecast?: number;
8+
}
9+
export interface CostBreakdown {
10+
id: string;
11+
label: string;
12+
spend: number | null;
13+
share: number;
14+
rate?: string;
15+
unpricedTokens?: number;
16+
}
17+
export interface AgentCost {
18+
handle: string;
19+
host: string;
20+
spend: number;
21+
tokens: string;
22+
mix: { label: string; share: number }[];
23+
spark: number[];
24+
governance: GovernanceItem;
25+
}
26+
export interface CostSession {
27+
id: string;
28+
started: string;
29+
agent: string;
30+
model: string;
31+
taskId?: number;
32+
tokens: string;
33+
spend: number;
34+
math: { kind: string; tokens: number; rate: number; dollars: number }[];
35+
source: "computed" | "reported" | "mixed";
36+
}
37+
export interface PriceRow {
38+
model: string;
39+
input: number;
40+
output: number;
41+
write: number | null;
42+
read: number;
43+
unpricedTokens?: number;
44+
}
45+
46+
const expires = (hours: number) => Math.floor(Date.now() / 1000) + hours * 3600;
47+
export const mockGovernance: GovernanceItem[] = [
48+
{
49+
agent: "carson-2",
50+
light: "yellow",
51+
tokens_spent: 640_000,
52+
granted_tokens: 800_000,
53+
grant_expires_epoch: expires(22),
54+
tier: "opus",
55+
rate_limit_hits: 0,
56+
},
57+
{
58+
agent: "janet",
59+
light: "green",
60+
tokens_spent: 210_000,
61+
granted_tokens: 400_000,
62+
grant_expires_epoch: expires(72),
63+
tier: "opus",
64+
rate_limit_hits: 0,
65+
},
66+
{
67+
agent: "point-fable",
68+
light: "yellow",
69+
tokens_spent: 285_000,
70+
granted_tokens: 400_000,
71+
grant_expires_epoch: expires(41),
72+
tier: "opus",
73+
rate_limit_hits: 0,
74+
},
75+
{
76+
agent: "scout",
77+
light: "green",
78+
tokens_spent: 88_000,
79+
granted_tokens: 200_000,
80+
grant_expires_epoch: expires(72),
81+
tier: "sonnet",
82+
rate_limit_hits: 0,
83+
},
84+
{
85+
agent: "derek",
86+
light: "green",
87+
tokens_spent: 27_000,
88+
granted_tokens: 200_000,
89+
grant_expires_epoch: expires(120),
90+
tier: "haiku",
91+
rate_limit_hits: 0,
92+
},
93+
{
94+
agent: "codex",
95+
light: "yellow",
96+
tokens_spent: 170_000,
97+
granted_tokens: 200_000,
98+
grant_expires_epoch: expires(48),
99+
tier: "opus",
100+
rate_limit_hits: 0,
101+
},
102+
];
103+
export const mockPool: GovernancePool = {
104+
pool_tokens: 2_200_000,
105+
pool_spent: 1_420_000,
106+
fleet_mode: "parallel",
107+
cascade_active: false,
108+
};
109+
export const daily: DailyCost[] = [
110+
{ day: "Mon 6", spend: 5.7, prior: 5.1 },
111+
{ day: "Tue 7", spend: 9.84, prior: 5.5 },
112+
{ day: "Wed 8", spend: 7.1, prior: 5.8 },
113+
{ day: "Thu 9", spend: 6.6, prior: 6.4 },
114+
{ day: "Fri 10", spend: 7.8, prior: 6.1 },
115+
{ day: "Sat 11", spend: 4.16, prior: 5.7 },
116+
{ day: "Sun 12", spend: 0, prior: 5.2, forecast: 5.9 },
117+
];
118+
export const breakdowns: Record<"agent" | "model" | "project", CostBreakdown[]> = {
119+
agent: [
120+
{ id: "carson-2", label: "carson-2", spend: 14.02, share: 34 },
121+
{ id: "janet", label: "janet", spend: 9.77, share: 24 },
122+
{ id: "point-fable", label: "point-fable", spend: 8.91, share: 22 },
123+
{ id: "codex", label: "codex", spend: 4.1, share: 10 },
124+
{ id: "other", label: "2 more · $4.40", spend: 4.4, share: 10 },
125+
],
126+
model: [
127+
{ id: "opus", label: "claude-opus-4-8", spend: 27.3, share: 66, rate: "5.00 · 25.00 /Mtok" },
128+
{ id: "fable", label: "claude-fable-5", spend: 7.6, share: 18, rate: "10.00 · 50.00 /Mtok" },
129+
{ id: "gpt", label: "gpt-5.5", spend: 2.6, share: 6, rate: "5.00 · 20.00 /Mtok" },
130+
{ id: "haiku", label: "claude-haiku-4-5", spend: 2.1, share: 5, rate: "1.00 · 5.00 /Mtok" },
131+
{ id: "sonnet", label: "claude-sonnet-5", spend: 1.6, share: 4, rate: "2.00 · 10.00 /Mtok" },
132+
],
133+
project: [
134+
{ id: "console", label: "Lab Console", spend: 19.8, share: 48 },
135+
{ id: "library", label: "Library backfill", spend: 12.3, share: 30 },
136+
{ id: "infra", label: "Neighborhood infra", spend: 6.1, share: 15 },
137+
{ id: "other", label: "3 more · $3.00", spend: 3, share: 7 },
138+
],
139+
};
140+
export const agents: AgentCost[] = [
141+
{
142+
handle: "carson-2",
143+
host: ".14 · builder",
144+
spend: 14.02,
145+
tokens: "in 0.42M · out 0.24M · cache 6.1M",
146+
mix: [
147+
{ label: "opus-4-8", share: 79 },
148+
{ label: "fable-5", share: 13 },
149+
{ label: "other", share: 8 },
150+
],
151+
spark: [4, 13, 6, 5, 8, 4, 4],
152+
governance: mockGovernance[0]!,
153+
},
154+
{
155+
handle: "janet",
156+
host: ".202 · helm",
157+
spend: 9.77,
158+
tokens: "in 0.31M · out 0.17M · cache 4.8M",
159+
mix: [
160+
{ label: "opus-4-8", share: 88 },
161+
{ label: "sonnet-5", share: 9 },
162+
{ label: "other", share: 3 },
163+
],
164+
spark: [5, 6, 7, 6, 9, 7, 8],
165+
governance: mockGovernance[1]!,
166+
},
167+
{
168+
handle: "point-fable",
169+
host: ".14 · builder",
170+
spend: 8.91,
171+
tokens: "in 0.26M · out 0.14M · cache 3.9M",
172+
mix: [
173+
{ label: "fable-5", share: 64 },
174+
{ label: "opus-4-8", share: 29 },
175+
{ label: "other", share: 7 },
176+
],
177+
spark: [3, 5, 4, 10, 7, 9, 7],
178+
governance: mockGovernance[2]!,
179+
},
180+
];
181+
export const sessions: CostSession[] = [
182+
{
183+
id: "a1f4c2e8",
184+
started: "Tue 14:02",
185+
agent: "carson-2",
186+
model: "claude-opus-4-8",
187+
taskId: 512,
188+
tokens: "in 240k · out 140k · cache 2.8M",
189+
spend: 7.1,
190+
source: "computed",
191+
math: [
192+
{ kind: "input", tokens: 240000, rate: 5, dollars: 1.2 },
193+
{ kind: "output", tokens: 140000, rate: 25, dollars: 3.5 },
194+
{ kind: "cache write", tokens: 160000, rate: 6.25, dollars: 1 },
195+
{ kind: "cache read", tokens: 2800000, rate: 0.5, dollars: 1.4 },
196+
],
197+
},
198+
{
199+
id: "b209d",
200+
started: "Sat 20:14",
201+
agent: "janet",
202+
model: "claude-opus-4-8",
203+
taskId: 718,
204+
tokens: "in 96k · out 41k · cache 1.2M",
205+
spend: 3.84,
206+
source: "mixed",
207+
math: [],
208+
},
209+
{
210+
id: "c331e",
211+
started: "Thu 09:32",
212+
agent: "point-fable",
213+
model: "mixed · 3",
214+
taskId: 701,
215+
tokens: "in 84k · out 52k · cache 990k",
216+
spend: 2.92,
217+
source: "computed",
218+
math: [],
219+
},
220+
{
221+
id: "d402f",
222+
started: "Fri 16:08",
223+
agent: "codex",
224+
model: "gpt-5.5",
225+
tokens: "in 150k · out 55k · cache 1.0M",
226+
spend: 2.35,
227+
source: "reported",
228+
math: [],
229+
},
230+
{
231+
id: "e592a",
232+
started: "Fri 11:20",
233+
agent: "carson-2",
234+
model: "claude-fable-5",
235+
taskId: 512,
236+
tokens: "in 30k · out 18k · cache 550k",
237+
spend: 1.75,
238+
source: "computed",
239+
math: [],
240+
},
241+
];
242+
export const prices: PriceRow[] = [
243+
{ model: "claude-opus-4-8", input: 5, output: 25, write: 6.25, read: 0.5 },
244+
{ model: "claude-fable-5", input: 10, output: 50, write: 12.5, read: 1 },
245+
{ model: "claude-sonnet-5", input: 2, output: 10, write: 2.5, read: 0.2 },
246+
{ model: "claude-haiku-4-5", input: 1, output: 5, write: 1.25, read: 0.1 },
247+
{ model: "gpt-5.5", input: 5, output: 20, write: null, read: 0.5 },
248+
];

apps/console/src/lib/nav.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ const NAV: NavEntry[] = [
5353
badge: "none",
5454
sign: "Accounting",
5555
},
56+
{
57+
label: "Cost",
58+
icon: "coins",
59+
href: "/cost",
60+
key: "p",
61+
badge: "count",
62+
sign: "The Point System",
63+
},
5664
{
5765
label: "Signals",
5866
icon: "radio-tower",

0 commit comments

Comments
 (0)