@tokentop/ttop 0.2.2 → 0.3.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.
Files changed (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,4 +1,4 @@
1
- import type { BasePlugin, PluginHttpClient, PluginLogger } from './base.ts';
1
+ import type { BasePlugin, PluginHttpClient, PluginLogger } from "./base.ts";
2
2
 
3
3
  export interface ProviderCapabilities {
4
4
  usageLimits: boolean;
@@ -15,7 +15,7 @@ export interface Credentials {
15
15
  apiKey?: string;
16
16
  oauth?: OAuthCredentials;
17
17
  groupId?: string;
18
- source: 'env' | 'opencode' | 'external' | 'config';
18
+ source: "env" | "opencode" | "external" | "config";
19
19
  }
20
20
 
21
21
  export interface OAuthCredentials {
@@ -40,7 +40,7 @@ export interface RefreshedCredentials {
40
40
  export interface CredentialResult {
41
41
  ok: boolean;
42
42
  credentials?: Credentials;
43
- reason?: 'missing' | 'expired' | 'invalid' | 'error';
43
+ reason?: "missing" | "expired" | "invalid" | "error";
44
44
  message?: string;
45
45
  }
46
46
 
@@ -49,7 +49,7 @@ export interface CredentialResult {
49
49
  * Used by AuthSources.opencode.getProviderEntry().
50
50
  */
51
51
  export interface OpenCodeAuthEntry {
52
- type: 'api' | 'oauth' | 'codex' | 'github' | 'wellknown';
52
+ type: "api" | "oauth" | "codex" | "github" | "wellknown";
53
53
  key?: string;
54
54
  access?: string;
55
55
  refresh?: string;
@@ -89,7 +89,7 @@ export interface AuthSources {
89
89
 
90
90
  /** Platform information for cross-platform credential path resolution. */
91
91
  platform: {
92
- os: 'darwin' | 'linux' | 'win32';
92
+ os: "darwin" | "linux" | "win32";
93
93
  homedir: string;
94
94
  arch: string;
95
95
  };
@@ -197,7 +197,11 @@ export interface ProviderUsageData {
197
197
  cost?: {
198
198
  actual?: CostBreakdown;
199
199
  estimated?: CostBreakdown;
200
- source: 'api' | 'estimated';
200
+ /** Session-derived daily cost (today only). Set by enrichment hook. */
201
+ estimatedDaily?: CostBreakdown;
202
+ /** Session-derived monthly cost (MTD). Set by enrichment hook. */
203
+ estimatedMonthly?: CostBreakdown;
204
+ source: "api" | "estimated";
201
205
  };
202
206
  fetchedAt: number;
203
207
  error?: string;
@@ -214,7 +218,7 @@ export interface ProviderFetchContext {
214
218
  readonly config: Record<string, unknown>;
215
219
  readonly signal: AbortSignal;
216
220
  readonly options?: {
217
- timePeriod?: 'session' | 'daily' | 'weekly' | 'monthly';
221
+ timePeriod?: "session" | "daily" | "weekly" | "monthly";
218
222
  sessionId?: string;
219
223
  };
220
224
  }
@@ -224,7 +228,7 @@ export interface ProviderFetchContext {
224
228
  // ---------------------------------------------------------------------------
225
229
 
226
230
  export interface ProviderPlugin extends BasePlugin {
227
- readonly type: 'provider';
231
+ readonly type: "provider";
228
232
  readonly capabilities: ProviderCapabilities;
229
233
  readonly auth: ProviderAuth;
230
234
  readonly pricing?: ProviderPricing;
@@ -1,5 +1,5 @@
1
- import { z } from 'zod';
2
- import type { BasePlugin } from './base.ts';
1
+ import { z } from "zod";
2
+ import type { BasePlugin } from "./base.ts";
3
3
 
4
4
  export const ThemeColorsSchema = z.object({
5
5
  background: z.string(),
@@ -27,34 +27,42 @@ export const ThemeColorsSchema = z.object({
27
27
  export type ThemeColors = z.infer<typeof ThemeColorsSchema>;
28
28
 
29
29
  export const ThemeComponentsSchema = z.object({
30
- header: z.object({
31
- background: z.string().optional(),
32
- foreground: z.string().optional(),
33
- titleColor: z.string().optional(),
34
- titleAccentColor: z.string().optional(),
35
- }).optional(),
36
- statusBar: z.object({
37
- background: z.string().optional(),
38
- foreground: z.string().optional(),
39
- }).optional(),
40
- commandPalette: z.object({
41
- background: z.string().optional(),
42
- border: z.string().optional(),
43
- }).optional(),
44
- gauge: z.object({
45
- height: z.number().optional(),
46
- borderRadius: z.number().optional(),
47
- }).optional(),
30
+ header: z
31
+ .object({
32
+ background: z.string().optional(),
33
+ foreground: z.string().optional(),
34
+ titleColor: z.string().optional(),
35
+ titleAccentColor: z.string().optional(),
36
+ })
37
+ .optional(),
38
+ statusBar: z
39
+ .object({
40
+ background: z.string().optional(),
41
+ foreground: z.string().optional(),
42
+ })
43
+ .optional(),
44
+ commandPalette: z
45
+ .object({
46
+ background: z.string().optional(),
47
+ border: z.string().optional(),
48
+ })
49
+ .optional(),
50
+ gauge: z
51
+ .object({
52
+ height: z.number().optional(),
53
+ borderRadius: z.number().optional(),
54
+ })
55
+ .optional(),
48
56
  });
49
57
 
50
58
  export type ThemeComponents = z.infer<typeof ThemeComponentsSchema>;
51
59
 
52
- export type ColorScheme = 'light' | 'dark';
60
+ export type ColorScheme = "light" | "dark";
53
61
 
54
- export type ColorSchemePreference = 'auto' | 'light' | 'dark';
62
+ export type ColorSchemePreference = "auto" | "light" | "dark";
55
63
 
56
64
  export interface ThemePlugin extends BasePlugin {
57
- readonly type: 'theme';
65
+ readonly type: "theme";
58
66
  readonly family: string;
59
67
  readonly colorScheme: ColorScheme;
60
68
  readonly colors: ThemeColors;
@@ -9,7 +9,7 @@
9
9
  * skipped because they have no registry entry to compare against.
10
10
  */
11
11
 
12
- import type { PluginType } from './types/base.ts';
12
+ import type { PluginType } from "./types/base.ts";
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // Types
@@ -33,7 +33,7 @@ export interface PluginUpdateInfo {
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
35
  function parseVersion(v: string): [number, number, number] | null {
36
- const clean = v.replace(/^v/, '');
36
+ const clean = v.replace(/^v/, "");
37
37
  const match = clean.match(/^(\d+)\.(\d+)\.(\d+)/);
38
38
  if (!match) return null;
39
39
  return [Number(match[1]), Number(match[2]), Number(match[3])];
@@ -60,9 +60,9 @@ function isNewer(current: string, latest: string): boolean {
60
60
  * Uses the abbreviated metadata endpoint for minimal payload.
61
61
  */
62
62
  async function fetchLatestVersion(packageName: string): Promise<string> {
63
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`;
63
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace("%40", "@")}`;
64
64
  const res = await fetch(url, {
65
- headers: { Accept: 'application/vnd.npm.install-v1+json' },
65
+ headers: { Accept: "application/vnd.npm.install-v1+json" },
66
66
  signal: AbortSignal.timeout(8_000),
67
67
  });
68
68
 
@@ -70,8 +70,8 @@ async function fetchLatestVersion(packageName: string): Promise<string> {
70
70
  throw new Error(`npm registry returned ${res.status} for ${packageName}`);
71
71
  }
72
72
 
73
- const data = (await res.json()) as { 'dist-tags'?: { latest?: string } };
74
- const latest = data?.['dist-tags']?.latest;
73
+ const data = (await res.json()) as { "dist-tags"?: { latest?: string } };
74
+ const latest = data?.["dist-tags"]?.latest;
75
75
  if (!latest) {
76
76
  throw new Error(`No dist-tags.latest found for ${packageName}`);
77
77
  }
@@ -1,14 +1,9 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import type { ModelPricing } from '@/plugins/types/provider.ts';
3
- import {
4
- estimateCost,
5
- estimateSessionCost,
6
- formatCost,
7
- formatTokenCount,
8
- } from './estimator.ts';
9
-
10
- describe('estimateCost()', () => {
11
- test('calculates input and output costs for a typical coding session', () => {
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModelPricing } from "@/plugins/types/provider.ts";
3
+ import { estimateCost, estimateSessionCost, formatCost, formatTokenCount } from "./estimator.ts";
4
+
5
+ describe("estimateCost()", () => {
6
+ test("calculates input and output costs for a typical coding session", () => {
12
7
  const usage = { input: 150_000, output: 50_000 };
13
8
  const pricing: ModelPricing = { input: 3, output: 15 };
14
9
 
@@ -16,11 +11,11 @@ describe('estimateCost()', () => {
16
11
  total: 1.2,
17
12
  input: 0.45,
18
13
  output: 0.75,
19
- currency: 'USD',
14
+ currency: "USD",
20
15
  });
21
16
  });
22
17
 
23
- test('includes cache read and write costs when both usage and pricing are present', () => {
18
+ test("includes cache read and write costs when both usage and pricing are present", () => {
24
19
  const usage = { input: 100_000, output: 25_000, cacheRead: 80_000, cacheWrite: 40_000 };
25
20
  const pricing: ModelPricing = {
26
21
  input: 3,
@@ -35,11 +30,11 @@ describe('estimateCost()', () => {
35
30
  output: 0.375,
36
31
  cacheRead: 0.024,
37
32
  cacheWrite: 0.15,
38
- currency: 'USD',
33
+ currency: "USD",
39
34
  });
40
35
  });
41
36
 
42
- test('does not include cache fields when cache pricing is unavailable', () => {
37
+ test("does not include cache fields when cache pricing is unavailable", () => {
43
38
  const usage = { input: 10_000, output: 20_000, cacheRead: 5_000, cacheWrite: 7_000 };
44
39
  const pricing: ModelPricing = { input: 2, output: 8 };
45
40
 
@@ -47,11 +42,11 @@ describe('estimateCost()', () => {
47
42
  total: 0.18,
48
43
  input: 0.02,
49
44
  output: 0.16,
50
- currency: 'USD',
45
+ currency: "USD",
51
46
  });
52
47
  });
53
48
 
54
- test('returns zero cost for zero token usage', () => {
49
+ test("returns zero cost for zero token usage", () => {
55
50
  const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
56
51
  const pricing: ModelPricing = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
57
52
 
@@ -59,11 +54,11 @@ describe('estimateCost()', () => {
59
54
  total: 0,
60
55
  input: 0,
61
56
  output: 0,
62
- currency: 'USD',
57
+ currency: "USD",
63
58
  });
64
59
  });
65
60
 
66
- test('handles very large token counts correctly', () => {
61
+ test("handles very large token counts correctly", () => {
67
62
  const usage = {
68
63
  input: 123_456_789,
69
64
  output: 987_654_321,
@@ -83,11 +78,11 @@ describe('estimateCost()', () => {
83
78
  output: 9876.54321,
84
79
  cacheRead: 5.555556,
85
80
  cacheWrite: 44.444444,
86
- currency: 'USD',
81
+ currency: "USD",
87
82
  });
88
83
  });
89
84
 
90
- test('rounds costs to six decimal places', () => {
85
+ test("rounds costs to six decimal places", () => {
91
86
  const usage = { input: 1, output: 1 };
92
87
  const pricing: ModelPricing = { input: 1.234567, output: 2.345678 };
93
88
 
@@ -95,13 +90,13 @@ describe('estimateCost()', () => {
95
90
  total: 0.000004,
96
91
  input: 0.000001,
97
92
  output: 0.000002,
98
- currency: 'USD',
93
+ currency: "USD",
99
94
  });
100
95
  });
101
96
  });
102
97
 
103
- describe('estimateSessionCost()', () => {
104
- test('aggregates multiple sessions before estimating cost', () => {
98
+ describe("estimateSessionCost()", () => {
99
+ test("aggregates multiple sessions before estimating cost", () => {
105
100
  const sessions = [
106
101
  { input: 100_000, output: 20_000, cacheRead: 10_000, cacheWrite: 5_000 },
107
102
  { input: 50_000, output: 10_000, cacheRead: 5_000, cacheWrite: 10_000 },
@@ -120,22 +115,22 @@ describe('estimateSessionCost()', () => {
120
115
  output: 0.525,
121
116
  cacheRead: 0.0045,
122
117
  cacheWrite: 0.05625,
123
- currency: 'USD',
118
+ currency: "USD",
124
119
  });
125
120
  });
126
121
 
127
- test('returns zero cost for an empty session list', () => {
122
+ test("returns zero cost for an empty session list", () => {
128
123
  const pricing: ModelPricing = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
129
124
 
130
125
  expect(estimateSessionCost([], pricing)).toEqual({
131
126
  total: 0,
132
127
  input: 0,
133
128
  output: 0,
134
- currency: 'USD',
129
+ currency: "USD",
135
130
  });
136
131
  });
137
132
 
138
- test('matches estimateCost for a single session', () => {
133
+ test("matches estimateCost for a single session", () => {
139
134
  const session = { input: 150_000, output: 50_000, cacheRead: 80_000, cacheWrite: 40_000 };
140
135
  const pricing: ModelPricing = {
141
136
  input: 3,
@@ -148,34 +143,34 @@ describe('estimateSessionCost()', () => {
148
143
  });
149
144
  });
150
145
 
151
- describe('formatCost()', () => {
152
- test('formats USD costs below $0.01 with 4 decimal places', () => {
153
- expect(formatCost(0.009876)).toBe('$0.0099');
146
+ describe("formatCost()", () => {
147
+ test("formats USD costs below $0.01 with 4 decimal places", () => {
148
+ expect(formatCost(0.009876)).toBe("$0.0099");
154
149
  });
155
150
 
156
- test('formats USD costs below $1 with 3 decimal places', () => {
157
- expect(formatCost(0.45678)).toBe('$0.457');
151
+ test("formats USD costs below $1 with 3 decimal places", () => {
152
+ expect(formatCost(0.45678)).toBe("$0.457");
158
153
  });
159
154
 
160
- test('formats USD costs at or above $1 with 2 decimal places', () => {
161
- expect(formatCost(12.3456)).toBe('$12.35');
155
+ test("formats USD costs at or above $1 with 2 decimal places", () => {
156
+ expect(formatCost(12.3456)).toBe("$12.35");
162
157
  });
163
158
 
164
- test('formats non-USD currency with 4 decimals and currency suffix', () => {
165
- expect(formatCost(12.3456, 'EUR')).toBe('12.3456 EUR');
159
+ test("formats non-USD currency with 4 decimals and currency suffix", () => {
160
+ expect(formatCost(12.3456, "EUR")).toBe("12.3456 EUR");
166
161
  });
167
162
  });
168
163
 
169
- describe('formatTokenCount()', () => {
170
- test('returns raw token count for values below 1K', () => {
171
- expect(formatTokenCount(999)).toBe('999');
164
+ describe("formatTokenCount()", () => {
165
+ test("returns raw token count for values below 1K", () => {
166
+ expect(formatTokenCount(999)).toBe("999");
172
167
  });
173
168
 
174
- test('formats token count in K for values below 1M', () => {
175
- expect(formatTokenCount(12_500)).toBe('12.5K');
169
+ test("formats token count in K for values below 1M", () => {
170
+ expect(formatTokenCount(12_500)).toBe("12.5K");
176
171
  });
177
172
 
178
- test('formats token count in M for values at or above 1M', () => {
179
- expect(formatTokenCount(2_345_678)).toBe('2.35M');
173
+ test("formats token count in M for values at or above 1M", () => {
174
+ expect(formatTokenCount(2_345_678)).toBe("2.35M");
180
175
  });
181
176
  });
@@ -1,4 +1,4 @@
1
- import type { ModelPricing, CostBreakdown } from '@/plugins/types/provider.ts';
1
+ import type { CostBreakdown, ModelPricing } from "@/plugins/types/provider.ts";
2
2
 
3
3
  export interface TokenUsage {
4
4
  input: number;
@@ -7,10 +7,7 @@ export interface TokenUsage {
7
7
  cacheWrite?: number;
8
8
  }
9
9
 
10
- export function estimateCost(
11
- usage: TokenUsage,
12
- pricing: ModelPricing
13
- ): CostBreakdown {
10
+ export function estimateCost(usage: TokenUsage, pricing: ModelPricing): CostBreakdown {
14
11
  const inputCost = (usage.input / 1_000_000) * pricing.input;
15
12
  const outputCost = (usage.output / 1_000_000) * pricing.output;
16
13
 
@@ -30,7 +27,7 @@ export function estimateCost(
30
27
  total: roundCost(total),
31
28
  input: roundCost(inputCost),
32
29
  output: roundCost(outputCost),
33
- currency: 'USD',
30
+ currency: "USD",
34
31
  };
35
32
 
36
33
  if (cacheReadCost > 0) breakdown.cacheRead = roundCost(cacheReadCost);
@@ -39,10 +36,7 @@ export function estimateCost(
39
36
  return breakdown;
40
37
  }
41
38
 
42
- export function estimateSessionCost(
43
- sessions: TokenUsage[],
44
- pricing: ModelPricing
45
- ): CostBreakdown {
39
+ export function estimateSessionCost(sessions: TokenUsage[], pricing: ModelPricing): CostBreakdown {
46
40
  const totals = sessions.reduce<TokenUsage>(
47
41
  (acc, session) => ({
48
42
  input: acc.input + session.input,
@@ -50,14 +44,14 @@ export function estimateSessionCost(
50
44
  cacheRead: (acc.cacheRead ?? 0) + (session.cacheRead ?? 0),
51
45
  cacheWrite: (acc.cacheWrite ?? 0) + (session.cacheWrite ?? 0),
52
46
  }),
53
- { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
47
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
54
48
  );
55
49
 
56
50
  return estimateCost(totals, pricing);
57
51
  }
58
52
 
59
- export function formatCost(cost: number, currency = 'USD'): string {
60
- if (currency === 'USD') {
53
+ export function formatCost(cost: number, currency = "USD"): string {
54
+ if (currency === "USD") {
61
55
  if (cost < 0.01) {
62
56
  return `$${cost.toFixed(4)}`;
63
57
  }
@@ -1,71 +1,67 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import {
3
- FALLBACK_PRICING,
4
- getFallbackPricing,
5
- getFallbackProviderPricing,
6
- } from './fallback.ts';
1
+ import { describe, expect, test } from "bun:test";
2
+ import { FALLBACK_PRICING, getFallbackPricing, getFallbackProviderPricing } from "./fallback.ts";
7
3
 
8
- describe('getFallbackPricing()', () => {
9
- test('returns pricing for an exact model match', () => {
10
- expect(getFallbackPricing('openai', 'gpt-4.1')).toEqual({
4
+ describe("getFallbackPricing()", () => {
5
+ test("returns pricing for an exact model match", () => {
6
+ expect(getFallbackPricing("openai", "gpt-4.1")).toEqual({
11
7
  input: 2,
12
8
  output: 8,
13
9
  cacheRead: 0.5,
14
- source: 'fallback',
10
+ source: "fallback",
15
11
  });
16
12
  });
17
13
 
18
- test('returns pricing when model id includes a known key', () => {
19
- expect(getFallbackPricing('anthropic', 'claude-3-7-sonnet-20250219-thinking')).toEqual({
14
+ test("returns pricing when model id includes a known key", () => {
15
+ expect(getFallbackPricing("anthropic", "claude-3-7-sonnet-20250219-thinking")).toEqual({
20
16
  input: 3,
21
17
  output: 15,
22
18
  cacheRead: 0.3,
23
19
  cacheWrite: 3.75,
24
- source: 'fallback',
20
+ source: "fallback",
25
21
  });
26
22
  });
27
23
 
28
- test('returns pricing when a known key includes the model id', () => {
29
- expect(getFallbackPricing('google', 'gemini-2.5')).toEqual({
24
+ test("returns pricing when a known key includes the model id", () => {
25
+ expect(getFallbackPricing("google", "gemini-2.5")).toEqual({
30
26
  input: 1.25,
31
27
  output: 10,
32
- source: 'fallback',
28
+ source: "fallback",
33
29
  });
34
30
  });
35
31
 
36
- test('returns null for unknown provider', () => {
37
- expect(getFallbackPricing('unknown-provider', 'gpt-4.1')).toBeNull();
32
+ test("returns null for unknown provider", () => {
33
+ expect(getFallbackPricing("unknown-provider", "gpt-4.1")).toBeNull();
38
34
  });
39
35
 
40
- test('returns null for unknown model in a known provider', () => {
41
- expect(getFallbackPricing('openai', 'totally-unknown-model')).toBeNull();
36
+ test("returns null for unknown model in a known provider", () => {
37
+ expect(getFallbackPricing("openai", "totally-unknown-model")).toBeNull();
42
38
  });
43
39
  });
44
40
 
45
- describe('getFallbackProviderPricing()', () => {
46
- test('returns the full provider pricing map', () => {
47
- const anthropicPricing = FALLBACK_PRICING['anthropic'];
41
+ describe("getFallbackProviderPricing()", () => {
42
+ test("returns the full provider pricing map", () => {
43
+ const anthropicPricing = FALLBACK_PRICING["anthropic"];
48
44
  if (!anthropicPricing) {
49
- throw new Error('Expected anthropic fallback pricing to exist');
45
+ throw new Error("Expected anthropic fallback pricing to exist");
50
46
  }
51
- expect(getFallbackProviderPricing('anthropic')).toEqual(anthropicPricing);
47
+ expect(getFallbackProviderPricing("anthropic")).toEqual(anthropicPricing);
52
48
  });
53
49
 
54
- test('returns null for unknown provider', () => {
55
- expect(getFallbackProviderPricing('does-not-exist')).toBeNull();
50
+ test("returns null for unknown provider", () => {
51
+ expect(getFallbackProviderPricing("does-not-exist")).toBeNull();
56
52
  });
57
53
  });
58
54
 
59
- describe('FALLBACK_PRICING', () => {
60
- test('includes anthropic provider pricing data', () => {
61
- expect(FALLBACK_PRICING['anthropic']).toBeDefined();
55
+ describe("FALLBACK_PRICING", () => {
56
+ test("includes anthropic provider pricing data", () => {
57
+ expect(FALLBACK_PRICING["anthropic"]).toBeDefined();
62
58
  });
63
59
 
64
- test('includes openai provider pricing data', () => {
65
- expect(FALLBACK_PRICING['openai']).toBeDefined();
60
+ test("includes openai provider pricing data", () => {
61
+ expect(FALLBACK_PRICING["openai"]).toBeDefined();
66
62
  });
67
63
 
68
- test('includes google provider pricing data', () => {
69
- expect(FALLBACK_PRICING['google']).toBeDefined();
64
+ test("includes google provider pricing data", () => {
65
+ expect(FALLBACK_PRICING["google"]).toBeDefined();
70
66
  });
71
67
  });
@@ -1,45 +1,72 @@
1
- import type { ModelPricing } from '@/plugins/types/provider.ts';
1
+ import type { ModelPricing } from "@/plugins/types/provider.ts";
2
2
 
3
3
  //TODO: get more up to date fallback pricing
4
4
  export const FALLBACK_PRICING: Record<string, Record<string, ModelPricing>> = {
5
5
  anthropic: {
6
- 'claude-sonnet-4-20250514': { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, source: 'fallback' },
7
- 'claude-3-7-sonnet-20250219': { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, source: 'fallback' },
8
- 'claude-3-5-sonnet-20241022': { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75, source: 'fallback' },
9
- 'claude-3-5-haiku-20241022': { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, source: 'fallback' },
10
- 'claude-3-opus-20240229': { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75, source: 'fallback' },
6
+ "claude-sonnet-4-20250514": {
7
+ input: 3,
8
+ output: 15,
9
+ cacheRead: 0.3,
10
+ cacheWrite: 3.75,
11
+ source: "fallback",
12
+ },
13
+ "claude-3-7-sonnet-20250219": {
14
+ input: 3,
15
+ output: 15,
16
+ cacheRead: 0.3,
17
+ cacheWrite: 3.75,
18
+ source: "fallback",
19
+ },
20
+ "claude-3-5-sonnet-20241022": {
21
+ input: 3,
22
+ output: 15,
23
+ cacheRead: 0.3,
24
+ cacheWrite: 3.75,
25
+ source: "fallback",
26
+ },
27
+ "claude-3-5-haiku-20241022": {
28
+ input: 0.8,
29
+ output: 4,
30
+ cacheRead: 0.08,
31
+ cacheWrite: 1,
32
+ source: "fallback",
33
+ },
34
+ "claude-3-opus-20240229": {
35
+ input: 15,
36
+ output: 75,
37
+ cacheRead: 1.5,
38
+ cacheWrite: 18.75,
39
+ source: "fallback",
40
+ },
11
41
  },
12
42
 
13
43
  openai: {
14
- 'gpt-4.1': { input: 2, output: 8, cacheRead: 0.5, source: 'fallback' },
15
- 'gpt-4.1-mini': { input: 0.4, output: 1.6, cacheRead: 0.1, source: 'fallback' },
16
- 'gpt-4o': { input: 2.5, output: 10, cacheRead: 1.25, source: 'fallback' },
17
- 'gpt-4o-mini': { input: 0.15, output: 0.6, cacheRead: 0.075, source: 'fallback' },
18
- 'o1': { input: 15, output: 60, source: 'fallback' },
19
- 'o1-mini': { input: 1.1, output: 4.4, source: 'fallback' },
20
- 'o3': { input: 10, output: 40, source: 'fallback' },
21
- 'o3-mini': { input: 1.1, output: 4.4, source: 'fallback' },
22
- 'o4-mini': { input: 1.1, output: 4.4, source: 'fallback' },
44
+ "gpt-4.1": { input: 2, output: 8, cacheRead: 0.5, source: "fallback" },
45
+ "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1, source: "fallback" },
46
+ "gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25, source: "fallback" },
47
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075, source: "fallback" },
48
+ o1: { input: 15, output: 60, source: "fallback" },
49
+ "o1-mini": { input: 1.1, output: 4.4, source: "fallback" },
50
+ o3: { input: 10, output: 40, source: "fallback" },
51
+ "o3-mini": { input: 1.1, output: 4.4, source: "fallback" },
52
+ "o4-mini": { input: 1.1, output: 4.4, source: "fallback" },
23
53
  },
24
54
 
25
55
  google: {
26
- 'gemini-2.0-flash': { input: 0.1, output: 0.4, source: 'fallback' },
27
- 'gemini-2.0-flash-lite': { input: 0.075, output: 0.3, source: 'fallback' },
28
- 'gemini-2.5-pro': { input: 1.25, output: 10, source: 'fallback' },
29
- 'gemini-2.5-flash': { input: 0.15, output: 0.6, source: 'fallback' },
56
+ "gemini-2.0-flash": { input: 0.1, output: 0.4, source: "fallback" },
57
+ "gemini-2.0-flash-lite": { input: 0.075, output: 0.3, source: "fallback" },
58
+ "gemini-2.5-pro": { input: 1.25, output: 10, source: "fallback" },
59
+ "gemini-2.5-flash": { input: 0.15, output: 0.6, source: "fallback" },
30
60
  },
31
61
 
32
62
  openrouter: {
33
- 'anthropic/claude-sonnet-4': { input: 3, output: 15, source: 'fallback' },
34
- 'openai/gpt-4.1': { input: 2, output: 8, source: 'fallback' },
35
- 'google/gemini-2.5-pro': { input: 1.25, output: 10, source: 'fallback' },
63
+ "anthropic/claude-sonnet-4": { input: 3, output: 15, source: "fallback" },
64
+ "openai/gpt-4.1": { input: 2, output: 8, source: "fallback" },
65
+ "google/gemini-2.5-pro": { input: 1.25, output: 10, source: "fallback" },
36
66
  },
37
67
  };
38
68
 
39
- export function getFallbackPricing(
40
- providerId: string,
41
- modelId: string
42
- ): ModelPricing | null {
69
+ export function getFallbackPricing(providerId: string, modelId: string): ModelPricing | null {
43
70
  const providerPricing = FALLBACK_PRICING[providerId];
44
71
  if (!providerPricing) return null;
45
72
 
@@ -56,7 +83,7 @@ export function getFallbackPricing(
56
83
  }
57
84
 
58
85
  export function getFallbackProviderPricing(
59
- providerId: string
86
+ providerId: string,
60
87
  ): Record<string, ModelPricing> | null {
61
88
  return FALLBACK_PRICING[providerId] ?? null;
62
89
  }