@saadnvd1/agent-os 0.3.471 → 0.3.472

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.
Files changed (75) hide show
  1. package/README.md +100 -0
  2. package/app/api/models/route.ts +34 -0
  3. package/app/api/models/test/route.ts +17 -0
  4. package/app/api/sessions/[id]/summarize/route.ts +4 -1
  5. package/app/api/tasks/[id]/model/route.ts +23 -0
  6. package/app/api/tasks/route.ts +5 -1
  7. package/app/api/usage/compare/route.ts +21 -0
  8. package/app/api/usage/route.ts +7 -4
  9. package/app/api/workspaces/[id]/models/route.ts +27 -0
  10. package/components/CommandPalette/useAppCommands.ts +19 -1
  11. package/components/Draft/DraftChips.tsx +28 -0
  12. package/components/Draft/DraftPanel.tsx +1 -1
  13. package/components/Models/ModelSelect.tsx +67 -0
  14. package/components/Models/ModelsSection.tsx +199 -0
  15. package/components/Models/ProviderCard.tsx +271 -0
  16. package/components/SessionList/SidebarHeader.tsx +5 -1
  17. package/components/Settings/SettingsDialog.test.ts +3 -0
  18. package/components/Settings/SettingsDialog.tsx +9 -0
  19. package/components/Settings/commands.ts +19 -0
  20. package/components/Settings/entry-points.test.ts +1 -0
  21. package/components/Tasks/ResumeOnModel.tsx +72 -0
  22. package/components/Tasks/TaskRow.tsx +4 -0
  23. package/components/Usage/CompareView.tsx +211 -0
  24. package/components/Usage/ModelSpend.tsx +133 -0
  25. package/components/Usage/UsageDialog.tsx +38 -3
  26. package/components/Usage/format.ts +9 -0
  27. package/components/Workspaces/WorkspaceModelsForm.tsx +71 -0
  28. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  29. package/data/models/index.ts +125 -0
  30. package/data/usage/index.ts +3 -0
  31. package/lib/agents/launch.ts +11 -2
  32. package/lib/chat/context.ts +1 -1
  33. package/lib/chat/runner.ts +5 -0
  34. package/lib/chat/worker/extras.ts +12 -0
  35. package/lib/chat/worker/host.ts +13 -2
  36. package/lib/chat/worker/main.ts +5 -2
  37. package/lib/db/migrations.ts +28 -0
  38. package/lib/db/types.ts +6 -0
  39. package/lib/drafts.ts +3 -0
  40. package/lib/models/config.ts +218 -0
  41. package/lib/models/launch.ts +93 -0
  42. package/lib/models/plan-step.ts +94 -0
  43. package/lib/models/providers.ts +163 -0
  44. package/lib/models/resolve.ts +246 -0
  45. package/lib/models/resume.ts +99 -0
  46. package/lib/models/roles.ts +98 -0
  47. package/lib/models/secrets.ts +86 -0
  48. package/lib/models/session-model.ts +40 -0
  49. package/lib/models/test-provider.ts +116 -0
  50. package/lib/orchestrator/act.ts +13 -1
  51. package/lib/orchestrator/brief.ts +2 -1
  52. package/lib/orchestrator/claude-cli.ts +44 -15
  53. package/lib/orchestrator/home.ts +2 -2
  54. package/lib/orchestrator/review.ts +2 -0
  55. package/lib/orchestrator/scope.ts +2 -0
  56. package/lib/orchestrator/serve.ts +13 -0
  57. package/lib/orchestrator/tool-names.ts +1 -0
  58. package/lib/orchestrator/tool-schemas.ts +38 -0
  59. package/lib/orchestrator/tools.ts +3 -1
  60. package/lib/pr-generation.ts +5 -2
  61. package/lib/providers/registry.ts +1 -1
  62. package/lib/session-titles.ts +1 -1
  63. package/lib/sessions/launch.ts +19 -4
  64. package/lib/settings-url.ts +1 -0
  65. package/lib/tasks/index.ts +59 -8
  66. package/lib/tasks/queue.ts +14 -5
  67. package/lib/tasks/start.ts +7 -2
  68. package/lib/usage/by-model.ts +105 -0
  69. package/lib/usage/check-runs.ts +5 -2
  70. package/lib/usage/compare.ts +280 -0
  71. package/lib/usage/cost.ts +71 -0
  72. package/lib/usage/report.ts +26 -0
  73. package/lib/usage/turns.ts +79 -18
  74. package/package.json +1 -1
  75. package/stores/usageUi.ts +11 -2
@@ -0,0 +1,71 @@
1
+ /**
2
+ * What a turn would cost at a provider's list prices (the price table in
3
+ * Settings > Models). For Anthropic that's the API's price, not what the
4
+ * plan charges: the plan's own measure is its share of the 5-hour window.
5
+ */
6
+
7
+ import { ANTHROPIC_ALIASES, type ModelPrice } from "../models/providers";
8
+ import { ANTHROPIC } from "../models/roles";
9
+
10
+ export interface PricedRow {
11
+ provider: string | null;
12
+ model: string | null;
13
+ cost_usd: number;
14
+ input_tokens: number;
15
+ output_tokens: number;
16
+ cache_read_tokens: number;
17
+ cache_write_tokens: number;
18
+ }
19
+
20
+ export const UNRECORDED = "unrecorded";
21
+
22
+ // The row's provider and model as the table names them: turns from before
23
+ // routing have neither, and ran on Anthropic.
24
+ export function rowModel(r: Pick<PricedRow, "provider" | "model">): {
25
+ provider: string;
26
+ model: string;
27
+ } {
28
+ const provider = r.provider ?? ANTHROPIC;
29
+ const raw = r.model ?? UNRECORDED;
30
+ const model = provider === ANTHROPIC ? (ANTHROPIC_ALIASES[raw] ?? raw) : raw;
31
+ return { provider, model };
32
+ }
33
+
34
+ // Dollars, and whether it came from the table (false: the agent's own
35
+ // figure, for a model the table doesn't price).
36
+ export function estimateCost(
37
+ r: PricedRow,
38
+ prices: Record<string, ModelPrice>
39
+ ): { usd: number; priced: boolean } {
40
+ const { provider, model } = rowModel(r);
41
+ const p = prices[`${provider}/${model}`];
42
+ if (!p) return { usd: r.cost_usd, priced: false };
43
+ const usd =
44
+ (r.input_tokens * p.input +
45
+ r.cache_write_tokens * p.cacheWrite +
46
+ r.cache_read_tokens * p.cacheRead +
47
+ r.output_tokens * p.output) /
48
+ 1_000_000;
49
+ return { usd, priced: true };
50
+ }
51
+
52
+ // Each Anthropic turn's share of the plan's 5-hour window, in points: how
53
+ // far the window moved since the turn before it (any session's) ended.
54
+ // Turns overlap, so one turn's share is rough; summed over many it isn't.
55
+ // A drop means the window reset, and all of the new reading is this turn's;
56
+ // the first reading has nothing to measure from.
57
+ export function planShares(
58
+ rows: { at: number; provider: string | null; window_pct: number | null }[]
59
+ ): number[] {
60
+ const order = rows.map((r, i) => ({ r, i })).sort((a, b) => a.r.at - b.r.at);
61
+ const out = new Array<number>(rows.length).fill(0);
62
+ let prev: number | null = null;
63
+ for (const { r, i } of order) {
64
+ if ((r.provider ?? ANTHROPIC) !== ANTHROPIC || r.window_pct == null)
65
+ continue;
66
+ const pct = r.window_pct;
67
+ out[i] = prev === null ? 0 : pct < prev ? pct : pct - prev;
68
+ prev = pct;
69
+ }
70
+ return out;
71
+ }
@@ -1,4 +1,7 @@
1
1
  import { db } from "../db";
2
+ import { effectiveModels } from "../models/config";
3
+ import { LEDGER_SQL } from "./compare";
4
+ import { spendByModel, type ModelTurnRow, type SpendByModel } from "./by-model";
2
5
  import { listWorkspaces } from "../workspaces";
3
6
  import {
4
7
  aggregateUsage,
@@ -43,3 +46,26 @@ export function checkRunReport(range: UsageRange, now = Date.now()) {
43
46
  const names = Object.fromEntries(listWorkspaces().map((w) => [w.id, w.name]));
44
47
  return aggregateUsage(named, range, names, now);
45
48
  }
49
+
50
+ const HOUR = 3600 * 1000;
51
+
52
+ // Spend by model and role in the plan's current 5-hour window (its start
53
+ // from when it resets, or the last five hours) and the last 24 hours.
54
+ export function modelUsage(
55
+ fiveHourResetsIn: number | null | undefined,
56
+ now = Date.now()
57
+ ): { fiveHour: SpendByModel; day: SpendByModel } {
58
+ const day = now - 24 * HOUR;
59
+ const fiveHour =
60
+ fiveHourResetsIn != null
61
+ ? now + fiveHourResetsIn * 1000 - 5 * HOUR
62
+ : now - 5 * HOUR;
63
+ const rows = db
64
+ .prepare(LEDGER_SQL)
65
+ .all({ from: Math.min(day, fiveHour), to: now }) as ModelTurnRow[];
66
+ const { prices } = effectiveModels();
67
+ return {
68
+ fiveHour: spendByModel(rows, fiveHour, prices),
69
+ day: spendByModel(rows, day, prices),
70
+ };
71
+ }
@@ -7,6 +7,8 @@
7
7
  import { db, type Session } from "../db";
8
8
  import type { UsageTotals } from "../chat/events";
9
9
  import { totalTokens, turnDelta } from "../chat/context";
10
+ import { readUsage } from "../orchestrator/usage";
11
+ import { ANTHROPIC, sessionRole } from "../models/roles";
10
12
 
11
13
  // Where a worker's totals start: a resumed conversation continues from the
12
14
  // totals its transcript saved, which the last worker stored; a new one
@@ -30,12 +32,22 @@ function savedTotals(sessionId: string): UsageTotals | null {
30
32
  }
31
33
  }
32
34
 
33
- // Records the turn and returns the totals to measure the next one from.
35
+ // The plan's 5-hour window when a turn on it ended, for its share of the
36
+ // plan (lib/usage/compare): unknown when the statusline isn't sampling.
37
+ export function planWindowPct(): number | null {
38
+ const u = readUsage();
39
+ return "window" in u && u.window ? u.window.pct : null;
40
+ }
41
+
42
+ // Records the turn, with the model, provider and role it ran as, and
43
+ // returns the totals to measure the next one from.
34
44
  export function recordTurn(
35
45
  session: Pick<Session, "id" | "name" | "workspace_id">,
36
46
  prev: UsageTotals | null,
37
47
  totals: UsageTotals,
38
- at = Date.now()
48
+ at = Date.now(),
49
+ durationMs: number | null = null,
50
+ windowPct: () => number | null = planWindowPct
39
51
  ): UsageTotals {
40
52
  const turn = turnDelta(prev, totals);
41
53
  db.transaction(() => {
@@ -46,25 +58,35 @@ export function recordTurn(
46
58
  // A local command (/context, /mcp) spends nothing.
47
59
  if (turn.costUsd <= 0 && totalTokens(turn) <= 0) return;
48
60
  const current = db
49
- .prepare(`SELECT name, workspace_id FROM sessions WHERE id = ?`)
61
+ .prepare(
62
+ `SELECT name, workspace_id, model, model_provider, role, task_status,
63
+ plan_phase FROM sessions WHERE id = ?`
64
+ )
50
65
  .get(session.id) as
51
- | { name: string; workspace_id: string | null }
66
+ | Pick<
67
+ Session,
68
+ | "name"
69
+ | "workspace_id"
70
+ | "model"
71
+ | "model_provider"
72
+ | "role"
73
+ | "task_status"
74
+ | "plan_phase"
75
+ >
52
76
  | undefined;
53
- db.prepare(
54
- `INSERT INTO chat_turns (session_id, session_name, workspace_id, at, cost_usd,
55
- input_tokens, output_tokens, cache_read_tokens, cache_write_tokens)
56
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
57
- ).run(
58
- session.id,
59
- current?.name ?? session.name,
60
- current?.workspace_id ?? session.workspace_id,
77
+ const provider = current?.model_provider ?? ANTHROPIC;
78
+ insertTurn({
79
+ sessionId: session.id,
80
+ sessionName: current?.name ?? session.name,
81
+ workspaceId: current?.workspace_id ?? session.workspace_id,
61
82
  at,
62
- turn.costUsd,
63
- turn.inputTokens,
64
- turn.outputTokens,
65
- turn.cacheReadTokens,
66
- turn.cacheWriteTokens
67
- );
83
+ turn,
84
+ model: current?.model ?? null,
85
+ provider,
86
+ role: current ? sessionRole(current) : null,
87
+ durationMs,
88
+ windowPct: provider === ANTHROPIC ? windowPct() : null,
89
+ });
68
90
  })();
69
91
  return totals;
70
92
  }
@@ -96,3 +118,42 @@ export function recordCompaction(
96
118
  c.after ?? null
97
119
  );
98
120
  }
121
+
122
+ export interface TurnRecord {
123
+ sessionId: string;
124
+ sessionName: string;
125
+ workspaceId: string | null;
126
+ at: number;
127
+ turn: UsageTotals;
128
+ model: string | null;
129
+ provider: string;
130
+ role: string | null;
131
+ durationMs: number | null;
132
+ windowPct: number | null;
133
+ }
134
+
135
+ // One row of the usage ledger: a chat turn, or a one-shot check's run.
136
+ export function insertTurn(r: TurnRecord): void {
137
+ const { turn } = r;
138
+ db.prepare(
139
+ `INSERT INTO chat_turns (session_id, session_name, workspace_id, at, cost_usd,
140
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
141
+ model, provider, role, duration_ms, window_pct)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
143
+ ).run(
144
+ r.sessionId,
145
+ r.sessionName,
146
+ r.workspaceId,
147
+ r.at,
148
+ turn.costUsd,
149
+ turn.inputTokens,
150
+ turn.outputTokens,
151
+ turn.cacheReadTokens,
152
+ turn.cacheWriteTokens,
153
+ r.model,
154
+ r.provider,
155
+ r.role,
156
+ r.durationMs,
157
+ r.windowPct
158
+ );
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.471",
3
+ "version": "0.3.472",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
package/stores/usageUi.ts CHANGED
@@ -1,12 +1,21 @@
1
1
  import { proxy } from "valtio";
2
2
 
3
- export const usageUi = proxy({ open: false });
3
+ export type UsageTab = "spend" | "models" | "compare";
4
+
5
+ export const usageUi = proxy<{ open: boolean; tab: UsageTab }>({
6
+ open: false,
7
+ tab: "spend",
8
+ });
4
9
 
5
10
  export const usageUiActions = {
6
- open: () => {
11
+ open: (tab: UsageTab = "spend") => {
12
+ usageUi.tab = tab;
7
13
  usageUi.open = true;
8
14
  },
9
15
  setOpen: (open: boolean) => {
10
16
  usageUi.open = open;
11
17
  },
18
+ setTab: (tab: UsageTab) => {
19
+ usageUi.tab = tab;
20
+ },
12
21
  };