@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BasePlugin, PluginHttpClient, PluginLogger } from
|
|
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:
|
|
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?:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
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
|
|
2
|
-
import type { BasePlugin } from
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
60
|
+
export type ColorScheme = "light" | "dark";
|
|
53
61
|
|
|
54
|
-
export type ColorSchemePreference =
|
|
62
|
+
export type ColorSchemePreference = "auto" | "light" | "dark";
|
|
55
63
|
|
|
56
64
|
export interface ThemePlugin extends BasePlugin {
|
|
57
|
-
readonly type:
|
|
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
|
|
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(
|
|
63
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace("%40", "@")}`;
|
|
64
64
|
const res = await fetch(url, {
|
|
65
|
-
headers: { Accept:
|
|
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 {
|
|
74
|
-
const latest = data?.[
|
|
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
|
|
2
|
-
import type { ModelPricing } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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:
|
|
14
|
+
currency: "USD",
|
|
20
15
|
});
|
|
21
16
|
});
|
|
22
17
|
|
|
23
|
-
test(
|
|
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:
|
|
33
|
+
currency: "USD",
|
|
39
34
|
});
|
|
40
35
|
});
|
|
41
36
|
|
|
42
|
-
test(
|
|
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:
|
|
45
|
+
currency: "USD",
|
|
51
46
|
});
|
|
52
47
|
});
|
|
53
48
|
|
|
54
|
-
test(
|
|
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:
|
|
57
|
+
currency: "USD",
|
|
63
58
|
});
|
|
64
59
|
});
|
|
65
60
|
|
|
66
|
-
test(
|
|
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:
|
|
81
|
+
currency: "USD",
|
|
87
82
|
});
|
|
88
83
|
});
|
|
89
84
|
|
|
90
|
-
test(
|
|
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:
|
|
93
|
+
currency: "USD",
|
|
99
94
|
});
|
|
100
95
|
});
|
|
101
96
|
});
|
|
102
97
|
|
|
103
|
-
describe(
|
|
104
|
-
test(
|
|
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:
|
|
118
|
+
currency: "USD",
|
|
124
119
|
});
|
|
125
120
|
});
|
|
126
121
|
|
|
127
|
-
test(
|
|
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:
|
|
129
|
+
currency: "USD",
|
|
135
130
|
});
|
|
136
131
|
});
|
|
137
132
|
|
|
138
|
-
test(
|
|
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(
|
|
152
|
-
test(
|
|
153
|
-
expect(formatCost(0.009876)).toBe(
|
|
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(
|
|
157
|
-
expect(formatCost(0.45678)).toBe(
|
|
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(
|
|
161
|
-
expect(formatCost(12.3456)).toBe(
|
|
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(
|
|
165
|
-
expect(formatCost(12.3456,
|
|
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(
|
|
170
|
-
test(
|
|
171
|
-
expect(formatTokenCount(999)).toBe(
|
|
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(
|
|
175
|
-
expect(formatTokenCount(12_500)).toBe(
|
|
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(
|
|
179
|
-
expect(formatTokenCount(2_345_678)).toBe(
|
|
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
|
});
|
package/src/pricing/estimator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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 =
|
|
60
|
-
if (currency ===
|
|
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
|
|
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(
|
|
9
|
-
test(
|
|
10
|
-
expect(getFallbackPricing(
|
|
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:
|
|
10
|
+
source: "fallback",
|
|
15
11
|
});
|
|
16
12
|
});
|
|
17
13
|
|
|
18
|
-
test(
|
|
19
|
-
expect(getFallbackPricing(
|
|
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:
|
|
20
|
+
source: "fallback",
|
|
25
21
|
});
|
|
26
22
|
});
|
|
27
23
|
|
|
28
|
-
test(
|
|
29
|
-
expect(getFallbackPricing(
|
|
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:
|
|
28
|
+
source: "fallback",
|
|
33
29
|
});
|
|
34
30
|
});
|
|
35
31
|
|
|
36
|
-
test(
|
|
37
|
-
expect(getFallbackPricing(
|
|
32
|
+
test("returns null for unknown provider", () => {
|
|
33
|
+
expect(getFallbackPricing("unknown-provider", "gpt-4.1")).toBeNull();
|
|
38
34
|
});
|
|
39
35
|
|
|
40
|
-
test(
|
|
41
|
-
expect(getFallbackPricing(
|
|
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(
|
|
46
|
-
test(
|
|
47
|
-
const anthropicPricing = FALLBACK_PRICING[
|
|
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(
|
|
45
|
+
throw new Error("Expected anthropic fallback pricing to exist");
|
|
50
46
|
}
|
|
51
|
-
expect(getFallbackProviderPricing(
|
|
47
|
+
expect(getFallbackProviderPricing("anthropic")).toEqual(anthropicPricing);
|
|
52
48
|
});
|
|
53
49
|
|
|
54
|
-
test(
|
|
55
|
-
expect(getFallbackProviderPricing(
|
|
50
|
+
test("returns null for unknown provider", () => {
|
|
51
|
+
expect(getFallbackProviderPricing("does-not-exist")).toBeNull();
|
|
56
52
|
});
|
|
57
53
|
});
|
|
58
54
|
|
|
59
|
-
describe(
|
|
60
|
-
test(
|
|
61
|
-
expect(FALLBACK_PRICING[
|
|
55
|
+
describe("FALLBACK_PRICING", () => {
|
|
56
|
+
test("includes anthropic provider pricing data", () => {
|
|
57
|
+
expect(FALLBACK_PRICING["anthropic"]).toBeDefined();
|
|
62
58
|
});
|
|
63
59
|
|
|
64
|
-
test(
|
|
65
|
-
expect(FALLBACK_PRICING[
|
|
60
|
+
test("includes openai provider pricing data", () => {
|
|
61
|
+
expect(FALLBACK_PRICING["openai"]).toBeDefined();
|
|
66
62
|
});
|
|
67
63
|
|
|
68
|
-
test(
|
|
69
|
-
expect(FALLBACK_PRICING[
|
|
64
|
+
test("includes google provider pricing data", () => {
|
|
65
|
+
expect(FALLBACK_PRICING["google"]).toBeDefined();
|
|
70
66
|
});
|
|
71
67
|
});
|
package/src/pricing/fallback.ts
CHANGED
|
@@ -1,45 +1,72 @@
|
|
|
1
|
-
import type { ModelPricing } from
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|