pi-free 1.0.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 (68) hide show
  1. package/.github/workflows/update-benchmarks.yml +67 -0
  2. package/.pi/skills/pi-extension-dev/SKILL.md +155 -0
  3. package/CHANGELOG.md +59 -0
  4. package/LICENSE +21 -0
  5. package/README.md +289 -0
  6. package/config.ts +224 -0
  7. package/constants.ts +110 -0
  8. package/docs/free-tier-limits.md +213 -0
  9. package/docs/model-hopping.md +214 -0
  10. package/docs/plans/file-reorganization.md +172 -0
  11. package/docs/plans/package-json-fix.md +143 -0
  12. package/docs/provider-failover-plan.md +279 -0
  13. package/lib/json-persistence.ts +102 -0
  14. package/lib/logger.ts +94 -0
  15. package/lib/model-enhancer.ts +20 -0
  16. package/lib/types.ts +108 -0
  17. package/lib/util.ts +256 -0
  18. package/package.json +52 -0
  19. package/provider-factory.ts +221 -0
  20. package/provider-failover/errors.ts +275 -0
  21. package/provider-failover/hardcoded-benchmarks.ts +9889 -0
  22. package/provider-failover/index.ts +194 -0
  23. package/provider-helper.ts +336 -0
  24. package/providers/cline-auth.ts +473 -0
  25. package/providers/cline-models.ts +77 -0
  26. package/providers/cline.ts +257 -0
  27. package/providers/factory.ts +125 -0
  28. package/providers/fireworks.ts +49 -0
  29. package/providers/kilo-auth.ts +172 -0
  30. package/providers/kilo-models.ts +26 -0
  31. package/providers/kilo.ts +144 -0
  32. package/providers/mistral.ts +144 -0
  33. package/providers/model-fetcher.ts +138 -0
  34. package/providers/nvidia.ts +97 -0
  35. package/providers/ollama.ts +113 -0
  36. package/providers/openrouter.ts +175 -0
  37. package/providers/zen.ts +416 -0
  38. package/scripts/update-benchmarks.ts +255 -0
  39. package/tests/cline.test.ts +149 -0
  40. package/tests/errors.test.ts +139 -0
  41. package/tests/failover.test.ts +94 -0
  42. package/tests/fireworks.test.ts +148 -0
  43. package/tests/free-tier-limits.test.ts +191 -0
  44. package/tests/json-persistence.test.ts +105 -0
  45. package/tests/kilo.test.ts +186 -0
  46. package/tests/mistral.test.ts +138 -0
  47. package/tests/nvidia.test.ts +55 -0
  48. package/tests/ollama.test.ts +261 -0
  49. package/tests/openrouter.test.ts +192 -0
  50. package/tests/usage-tracking.test.ts +150 -0
  51. package/tests/util.test.ts +413 -0
  52. package/tests/zen.test.ts +180 -0
  53. package/todo.md +153 -0
  54. package/tsconfig.json +26 -0
  55. package/usage/commands.ts +17 -0
  56. package/usage/cumulative.ts +193 -0
  57. package/usage/formatters.ts +131 -0
  58. package/usage/index.ts +46 -0
  59. package/usage/limits.ts +166 -0
  60. package/usage/metrics.ts +222 -0
  61. package/usage/sessions.ts +355 -0
  62. package/usage/store.ts +99 -0
  63. package/usage/tracking.ts +329 -0
  64. package/usage/widget.ts +90 -0
  65. package/vitest.config.ts +20 -0
  66. package/widget/data.ts +113 -0
  67. package/widget/format.ts +26 -0
  68. package/widget/render.ts +117 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Zen Provider Tests
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ProviderModelConfig,
8
+ } from "@mariozechner/pi-coding-agent";
9
+ import { beforeEach, describe, expect, it, vi } from "vitest";
10
+
11
+ // Mock dependencies
12
+ vi.mock("../config.ts", () => ({
13
+ OPENCODE_API_KEY: "test-key",
14
+ ZEN_SHOW_PAID: false,
15
+ PROVIDER_ZEN: "zen",
16
+ applyHidden: (models: ProviderModelConfig[]) => models,
17
+ }));
18
+
19
+ vi.mock("../constants.ts", () => ({
20
+ BASE_URL_ZEN: "https://opencode.ai/zen/v1",
21
+ DEFAULT_FETCH_TIMEOUT_MS: 10000,
22
+ URL_MODELS_DEV: "https://models.dev/api.json",
23
+ URL_ZEN_TOS: "https://opencode.ai/terms",
24
+ }));
25
+
26
+ vi.mock("../provider-helper.ts", () => ({
27
+ createReRegister: vi.fn(() => vi.fn()),
28
+ createCtxReRegister: vi.fn(() => vi.fn()),
29
+ setupProvider: vi.fn(),
30
+ }));
31
+
32
+ vi.mock("../util.ts", () => ({
33
+ fetchWithRetry: vi.fn(),
34
+ logWarning: vi.fn(),
35
+ }));
36
+
37
+ import { fetchWithRetry } from "../lib/util.ts";
38
+ import { setupProvider } from "../provider-helper.ts";
39
+ import zenProvider from "../providers/zen.ts";
40
+
41
+ describe("Zen Provider", () => {
42
+ let mockPi: ExtensionAPI;
43
+ let mockOn: ReturnType<typeof vi.fn>;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ mockOn = vi.fn();
48
+
49
+ mockPi = {
50
+ on: mockOn,
51
+ } as unknown as ExtensionAPI;
52
+ });
53
+
54
+ describe("initialization", () => {
55
+ it("should register event handlers", async () => {
56
+ vi.mocked(fetchWithRetry).mockResolvedValue({
57
+ ok: true,
58
+ json: vi.fn().mockResolvedValue({ data: [{ id: "test-model" }] }),
59
+ } as unknown as Response);
60
+
61
+ await zenProvider(mockPi);
62
+
63
+ expect(mockOn).toHaveBeenCalledWith(
64
+ "session_start",
65
+ expect.any(Function),
66
+ );
67
+ expect(mockOn).toHaveBeenCalledWith(
68
+ "before_agent_start",
69
+ expect.any(Function),
70
+ );
71
+ });
72
+ });
73
+
74
+ describe("session_start handling", () => {
75
+ it("should fetch and register models", async () => {
76
+ const gatewayModels = {
77
+ data: [{ id: "big-pickle" }, { id: "mimo-v2-pro-free" }],
78
+ };
79
+ const modelsDevResponse = {
80
+ opencode: {
81
+ id: "opencode",
82
+ models: {
83
+ "big-pickle": {
84
+ id: "big-pickle",
85
+ name: "Big Pickle",
86
+ reasoning: true,
87
+ cost: { input: 0, output: 0 },
88
+ limit: { context: 200000, output: 128000 },
89
+ modalities: { input: ["text"] },
90
+ },
91
+ },
92
+ },
93
+ };
94
+
95
+ vi.mocked(fetchWithRetry)
96
+ .mockResolvedValueOnce({
97
+ ok: true,
98
+ json: vi.fn().mockResolvedValue(gatewayModels),
99
+ } as unknown as Response)
100
+ .mockResolvedValueOnce({
101
+ ok: true,
102
+ json: vi.fn().mockResolvedValue(modelsDevResponse),
103
+ } as unknown as Response);
104
+
105
+ await zenProvider(mockPi);
106
+
107
+ const sessionStartHandler = mockOn.mock.calls.find(
108
+ (call) => call[0] === "session_start",
109
+ )?.[1];
110
+
111
+ const mockRegisterProvider = vi.fn();
112
+ const mockCtx = {
113
+ modelRegistry: {
114
+ getAvailable: vi.fn().mockReturnValue([]),
115
+ registerProvider: mockRegisterProvider,
116
+ },
117
+ };
118
+
119
+ await sessionStartHandler({}, mockCtx);
120
+
121
+ expect(mockRegisterProvider).toHaveBeenCalledWith(
122
+ "zen",
123
+ expect.objectContaining({
124
+ baseUrl: "https://opencode.ai/zen/v1",
125
+ apiKey: "PI_FREE_ZEN_API_KEY",
126
+ headers: expect.objectContaining({
127
+ "X-Title": "Pi",
128
+ "x-opencode-session": expect.any(String),
129
+ }),
130
+ }),
131
+ );
132
+ });
133
+ });
134
+
135
+ describe("fallback behavior", () => {
136
+ it("should use static fallback when API fails", async () => {
137
+ vi.mocked(fetchWithRetry).mockRejectedValue(new Error("Network error"));
138
+
139
+ await zenProvider(mockPi);
140
+
141
+ const sessionStartHandler = mockOn.mock.calls.find(
142
+ (call) => call[0] === "session_start",
143
+ )?.[1];
144
+
145
+ const mockRegisterProvider = vi.fn();
146
+ const mockCtx = {
147
+ modelRegistry: {
148
+ getAvailable: vi.fn().mockReturnValue([]),
149
+ registerProvider: mockRegisterProvider,
150
+ },
151
+ };
152
+
153
+ await sessionStartHandler({}, mockCtx);
154
+
155
+ // When API fails, it should not register (let Pi use built-in)
156
+ expect(mockRegisterProvider).not.toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ describe("setupProvider integration", () => {
161
+ it("should call setupProvider with correct config", async () => {
162
+ vi.mocked(fetchWithRetry).mockResolvedValue({
163
+ ok: true,
164
+ json: vi.fn().mockResolvedValue({ data: [] }),
165
+ } as unknown as Response);
166
+
167
+ await zenProvider(mockPi);
168
+
169
+ expect(setupProvider).toHaveBeenCalledWith(
170
+ mockPi,
171
+ expect.objectContaining({
172
+ providerId: "zen",
173
+ tosUrl: "https://opencode.ai/terms",
174
+ hasKey: true,
175
+ }),
176
+ expect.any(Object),
177
+ );
178
+ });
179
+ });
180
+ });
package/todo.md ADDED
@@ -0,0 +1,153 @@
1
+ # TODO: Fix Usage Tracking & Rate Limit Attribution
2
+
3
+ ## Current State
4
+
5
+ The usage tracking infrastructure exists but is **not fully functional**:
6
+
7
+ 1. **Usage commands are disabled** (`usage/commands.ts`)
8
+ - `/free-sessionusage` and `/free-totalusage` commands exist but are commented out
9
+ - Reason: "duplicate registration issues" across providers
10
+
11
+ 2. **Usage tracking works at runtime** (`usage/tracking.ts`)
12
+ - Per-model request counts tracked in `modelUsageCounts` Map
13
+ - Session stats tracked in `sessionStats` Map
14
+ - Called from `provider-helper.ts` on each `turn_end` event
15
+
16
+ 3. **Cumulative usage persists** (`usage/cumulative.ts`, `usage/store.ts`)
17
+ - Stored at `~/.pi/free-usage.json`
18
+ - Tracks all-time usage per provider/model
19
+
20
+ 4. **Free tier limits defined** (`free-tier-limits.ts`)
21
+ - Hardcoded limits per provider
22
+ - NOT correctly integrated with usage tracking
23
+
24
+ ## Problems to Fix
25
+
26
+ ### 1. Usage Commands Disabled
27
+ **File:** `usage/commands.ts`
28
+
29
+ The `registerUsageCommands()` function is empty. The slash commands need to:
30
+ - Register once globally (not per-provider)
31
+ - Show current session usage with rate limit status
32
+ - Show cumulative all-time usage
33
+
34
+ **Current:**
35
+ ```typescript
36
+ export function registerUsageCommands(_pi: ExtensionAPI): void {
37
+ // Commands disabled - Pi shows duplicate registrations across providers
38
+ // TODO: Find reliable way to register global commands once
39
+ }
40
+ ```
41
+
42
+ **Needed:**
43
+ - Register `/free-sessionusage` - shows current session breakdown
44
+ - Register `/free-totalusage` - shows cumulative usage from disk
45
+ - Include rate limit warnings (🟢🟡🔴) based on current usage vs limits
46
+
47
+ ### 2. Rate Limit Attribution Wrong
48
+ **Files:** `usage/tracking.ts`, `free-tier-limits.ts`, `usage/formatters.ts`
49
+
50
+ The `FREE_TIER_LIMITS` in `free-tier-limits.ts` defines limits, but:
51
+ - Zen has no entry (has free tier but not in the list)
52
+ - Ollama has no entry (has free tier but not in the list)
53
+ - Fireworks has no entry (has free tier but not in the list)
54
+ - Mistral has no entry (has free tier but not in the list)
55
+ - Cline has no entry (has free tier but not in the list)
56
+
57
+ **Current `FREE_TIER_LIMITS`:**
58
+ ```typescript
59
+ export const FREE_TIER_LIMITS: Record<string, FreeTierLimit> = {
60
+ kilo: { provider: "kilo", requestsPerHour: 200, ... },
61
+ openrouter: { provider: "openrouter", requestsPerDay: 1000, ... },
62
+ nvidia: { provider: "nvidia", requestsPerMonth: 1000, ... },
63
+ // Missing: zen, ollama, fireworks, mistral, cline
64
+ };
65
+ ```
66
+
67
+ **Needed:**
68
+ - Add all providers with free tiers
69
+ - Research actual limits from provider docs
70
+ - Track per-provider usage counts separately (currently aggregated in `metrics.ts`)
71
+
72
+ ### 3. Usage Formatters Not Integrated
73
+ **File:** `usage/formatters.ts`
74
+
75
+ The formatters exist but are not called anywhere:
76
+ - `formatSessionUsage()` - formats session stats
77
+ - `formatCumulativeUsage()` - formats cumulative stats
78
+ - `formatFreeTierStatus()` - formats rate limit status with 🟢🟡🔴
79
+
80
+ **Needed:**
81
+ - Call formatters from usage commands
82
+ - Show rate limit warnings based on `FREE_TIER_LIMITS`
83
+ - Calculate percentage used: `(currentUsage / limit) * 100`
84
+
85
+ ### 4. Provider-Level Usage Tracking Missing
86
+ **File:** `usage/tracking.ts`
87
+
88
+ Current tracking aggregates by model, but doesn't track per-provider totals correctly for rate limits.
89
+
90
+ The `sessionStats.providers` Map exists but:
91
+ - Doesn't reset per time window (hour/day/month)
92
+ - Doesn't check against `FREE_TIER_LIMITS`
93
+
94
+ **Needed:**
95
+ - Track usage per time window (hourly for Kilo, daily for OpenRouter, monthly for NVIDIA)
96
+ - Check limits before incrementing (warn at 80%, critical at 95%)
97
+ - Show warning in UI when approaching limits
98
+
99
+ ## Implementation Plan
100
+
101
+ ### Phase 1: Fix Free Tier Limits
102
+ - [ ] Add missing providers to `FREE_TIER_LIMITS`
103
+ - [ ] Zen: 1000/day (verify with opencode.ai docs)
104
+ - [ ] Ollama: 5 hours + 7 days reset (explain credit system)
105
+ - [ ] Fireworks: research free tier limits
106
+ - [ ] Mistral: research free tier limits
107
+ - [ ] Cline: research free tier limits
108
+
109
+ ### Phase 2: Wire Up Usage Commands
110
+ - [ ] Create single registration point for global commands
111
+ - [ ] Register `/free-sessionusage` command
112
+ - [ ] Register `/free-totalusage` command
113
+ - [ ] Test commands don't duplicate across providers
114
+
115
+ ### Phase 3: Integrate Formatters
116
+ - [ ] Call `formatSessionUsage()` in `/free-sessionusage`
117
+ - [ ] Call `formatCumulativeUsage()` in `/free-totalusage`
118
+ - [ ] Add rate limit status to both commands using `formatFreeTierStatus()`
119
+
120
+ ### Phase 4: Per-Provider Rate Limit Warnings
121
+ - [ ] Track usage per time window (hour/day/month)
122
+ - [ ] Add limit checking in `incrementRequestCount()`
123
+ - [ ] Show UI notification when approaching limit (80% = 🟡, 95% = 🔴)
124
+ - [ ] Reset counters when time window expires
125
+
126
+ ## Provider Free Tier Research
127
+
128
+ | Provider | Free Tier | Rate Limit | Documentation |
129
+ |----------|-----------|------------|---------------|
130
+ | Kilo | 14 models | 200 req/hour | kilo.ai/terms |
131
+ | Zen | 11 models | 1000 req/day | opencode.ai |
132
+ | OpenRouter | 29 models | 1000 req/day | openrouter.ai/docs |
133
+ | NVIDIA | Curated 70B+ | 1000 credits/mo | build.nvidia.com |
134
+ | Ollama | Cloud models | 5hrs + 7 days reset | ollama.com |
135
+ | Fireworks | ? | ? | app.fireworks.ai |
136
+ | Mistral | ? | ? | mistral.ai |
137
+ | Cline | ? | ? | cline.bot |
138
+
139
+ ## Files to Modify
140
+
141
+ 1. `free-tier-limits.ts` - Add missing providers, verify limits
142
+ 2. `usage/commands.ts` - Implement command registration
143
+ 3. `usage/formatters.ts` - Ensure formatters use correct limits
144
+ 4. `usage/tracking.ts` - Add per-time-window tracking
145
+ 5. `provider-helper.ts` - Call limit check on turn_end
146
+ 6. `README.md` - Document usage commands once working
147
+
148
+ ## Notes
149
+
150
+ - The `metrics.ts` file has `getDailyRequestCount()` but it tracks ALL providers together
151
+ - Need separate counters per provider per time window
152
+ - Consider using `usage/sessions.ts` for session-level tracking
153
+ - The cumulative storage in `usage/store.ts` works - don't break it
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "allowImportingTsExtensions": true,
16
+ "noEmit": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@mariozechner/pi-coding-agent": [
20
+ "node_modules/@mariozechner/pi-coding-agent"
21
+ ]
22
+ }
23
+ },
24
+ "include": ["**/*.ts", "providers/**/*.ts"],
25
+ "exclude": ["node_modules", ".pi-lens"]
26
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Free tier usage commands
3
+ *
4
+ * Provides:
5
+ * - /free-sessionusage: Current session breakdown
6
+ * - /free-totalusage: Cumulative usage from disk
7
+ *
8
+ * NOTE: Commands temporarily disabled due to duplicate registration issues.
9
+ * Use /kilo-sessionusage or /zen-sessionusage instead.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+
14
+ export function registerUsageCommands(_pi: ExtensionAPI): void {
15
+ // Commands disabled - Pi shows duplicate registrations across providers
16
+ // TODO: Find reliable way to register global commands once
17
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Cumulative usage persistence - disk storage for all-time stats
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import { createJSONStore } from "../lib/json-persistence.ts";
7
+ import { createLogger } from "../lib/logger.ts";
8
+
9
+ const _logger = createLogger("usage:cumulative");
10
+
11
+ const PI_DIR = join(process.env.HOME || process.env.USERPROFILE || "", ".pi");
12
+ const USAGE_FILE = join(PI_DIR, "free-cumulative-usage.json");
13
+
14
+ interface CumulativeProviderStats {
15
+ totalRequests: number;
16
+ totalTokensIn: number;
17
+ totalTokensOut: number;
18
+ totalCacheRead: number;
19
+ totalCacheWrite: number;
20
+ totalCost: number;
21
+ models: Record<
22
+ string,
23
+ {
24
+ count: number;
25
+ tokensIn: number;
26
+ tokensOut: number;
27
+ cacheRead: number;
28
+ cacheWrite: number;
29
+ cost: number;
30
+ }
31
+ >;
32
+ firstUsed: string;
33
+ lastUsed: string;
34
+ }
35
+
36
+ interface CumulativeUsage {
37
+ providers: Record<string, CumulativeProviderStats>;
38
+ grandTotalRequests: number;
39
+ grandTotalTokensIn: number;
40
+ grandTotalTokensOut: number;
41
+ grandTotalCacheRead: number;
42
+ grandTotalCacheWrite: number;
43
+ grandTotalCost: number;
44
+ }
45
+
46
+ const cumulativeStore = createJSONStore<CumulativeUsage>(USAGE_FILE, {
47
+ providers: {},
48
+ grandTotalRequests: 0,
49
+ grandTotalTokensIn: 0,
50
+ grandTotalTokensOut: 0,
51
+ grandTotalCacheRead: 0,
52
+ grandTotalCacheWrite: 0,
53
+ grandTotalCost: 0,
54
+ });
55
+
56
+ export function persistUsage(
57
+ provider: string,
58
+ modelId: string,
59
+ tokensIn: number,
60
+ tokensOut: number,
61
+ cacheRead: number,
62
+ cacheWrite: number,
63
+ cost: number,
64
+ ): void {
65
+ const data = cumulativeStore.load();
66
+ const now = new Date().toISOString();
67
+
68
+ let providerStats = data.providers[provider];
69
+ if (!providerStats) {
70
+ providerStats = {
71
+ totalRequests: 0,
72
+ totalTokensIn: 0,
73
+ totalTokensOut: 0,
74
+ totalCacheRead: 0,
75
+ totalCacheWrite: 0,
76
+ totalCost: 0,
77
+ models: {},
78
+ firstUsed: now,
79
+ lastUsed: now,
80
+ };
81
+ data.providers[provider] = providerStats;
82
+ }
83
+
84
+ providerStats.totalRequests++;
85
+ providerStats.totalTokensIn += tokensIn;
86
+ providerStats.totalTokensOut += tokensOut;
87
+ providerStats.totalCacheRead += cacheRead;
88
+ providerStats.totalCacheWrite += cacheWrite;
89
+ providerStats.totalCost += cost;
90
+ providerStats.lastUsed = now;
91
+
92
+ const modelStats = providerStats.models[modelId] ?? {
93
+ count: 0,
94
+ tokensIn: 0,
95
+ tokensOut: 0,
96
+ cacheRead: 0,
97
+ cacheWrite: 0,
98
+ cost: 0,
99
+ };
100
+ modelStats.count++;
101
+ modelStats.tokensIn += tokensIn;
102
+ modelStats.tokensOut += tokensOut;
103
+ modelStats.cacheRead += cacheRead;
104
+ modelStats.cacheWrite += cacheWrite;
105
+ modelStats.cost += cost;
106
+ providerStats.models[modelId] = modelStats;
107
+
108
+ data.grandTotalRequests++;
109
+ data.grandTotalTokensIn += tokensIn;
110
+ data.grandTotalTokensOut += tokensOut;
111
+ data.grandTotalCacheRead += cacheRead;
112
+ data.grandTotalCacheWrite += cacheWrite;
113
+ data.grandTotalCost += cost;
114
+
115
+ cumulativeStore.save(data);
116
+ }
117
+
118
+ export interface CumulativeUsageReport {
119
+ providers: Array<{
120
+ name: string;
121
+ totalRequests: number;
122
+ totalTokensIn: number;
123
+ totalTokensOut: number;
124
+ totalCacheRead: number;
125
+ totalCacheWrite: number;
126
+ totalCost: number;
127
+ modelCount: number;
128
+ firstUsed: string;
129
+ lastUsed: string;
130
+ topModels: Array<{
131
+ modelId: string;
132
+ count: number;
133
+ tokensIn: number;
134
+ tokensOut: number;
135
+ cacheRead: number;
136
+ cacheWrite: number;
137
+ cost: number;
138
+ }>;
139
+ }>;
140
+ grandTotalRequests: number;
141
+ grandTotalTokensIn: number;
142
+ grandTotalTokensOut: number;
143
+ grandTotalCacheRead: number;
144
+ grandTotalCacheWrite: number;
145
+ grandTotalCost: number;
146
+ }
147
+
148
+ export function getCumulativeUsage(): CumulativeUsageReport {
149
+ const data = cumulativeStore.load();
150
+
151
+ const providers: CumulativeUsageReport["providers"] = [];
152
+
153
+ for (const [name, stats] of Object.entries(data.providers)) {
154
+ const topModels = Object.entries(stats.models)
155
+ .map(([modelId, m]) => ({
156
+ modelId,
157
+ count: m.count,
158
+ tokensIn: m.tokensIn,
159
+ tokensOut: m.tokensOut,
160
+ cacheRead: m.cacheRead,
161
+ cacheWrite: m.cacheWrite,
162
+ cost: m.cost,
163
+ }))
164
+ .sort((a, b) => b.count - a.count)
165
+ .slice(0, 5);
166
+
167
+ providers.push({
168
+ name,
169
+ totalRequests: stats.totalRequests,
170
+ totalTokensIn: stats.totalTokensIn,
171
+ totalTokensOut: stats.totalTokensOut,
172
+ totalCacheRead: stats.totalCacheRead,
173
+ totalCacheWrite: stats.totalCacheWrite,
174
+ totalCost: stats.totalCost,
175
+ modelCount: Object.keys(stats.models).length,
176
+ firstUsed: stats.firstUsed,
177
+ lastUsed: stats.lastUsed,
178
+ topModels,
179
+ });
180
+ }
181
+
182
+ providers.sort((a, b) => b.totalRequests - a.totalRequests);
183
+
184
+ return {
185
+ providers,
186
+ grandTotalRequests: data.grandTotalRequests,
187
+ grandTotalTokensIn: data.grandTotalTokensIn,
188
+ grandTotalTokensOut: data.grandTotalTokensOut,
189
+ grandTotalCacheRead: data.grandTotalCacheRead,
190
+ grandTotalCacheWrite: data.grandTotalCacheWrite,
191
+ grandTotalCost: data.grandTotalCost,
192
+ };
193
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Usage report formatters - text formatting for display
3
+ */
4
+
5
+ import type { CumulativeUsageReport } from "./cumulative.ts";
6
+ import {
7
+ type FreeTierLimit,
8
+ getFreeTierUsage,
9
+ getLimitWarning,
10
+ } from "./limits.ts";
11
+ import type { SessionUsageReport } from "./tracking.ts";
12
+
13
+ export interface FreeTierUsage {
14
+ provider: string;
15
+ requestsToday: number;
16
+ requestsThisHour: number;
17
+ requestsThisMonth?: number;
18
+ limit: FreeTierLimit;
19
+ remainingToday?: number;
20
+ remainingThisHour?: number;
21
+ remainingThisMonth?: number;
22
+ percentUsed: number;
23
+ status: "ok" | "warning" | "critical" | "unknown";
24
+ }
25
+
26
+ export function formatSessionUsage(report: SessionUsageReport): string {
27
+ if (report.providers.length === 0) {
28
+ return "No usage recorded in this session yet.";
29
+ }
30
+
31
+ const lines: string[] = [];
32
+ lines.push("━".repeat(50));
33
+ lines.push(`📊 Session Usage (${report.durationFormatted})`);
34
+ lines.push("━".repeat(50));
35
+ lines.push("");
36
+
37
+ for (const p of report.providers) {
38
+ const warning = getLimitWarning(p.name);
39
+ const statusEmoji = warning ? (warning.includes("⚠️") ? "🔴" : "🟡") : "🟢";
40
+ lines.push(`${statusEmoji} ${p.name}`);
41
+ lines.push(` Requests: ${p.requests}`);
42
+ lines.push(
43
+ ` Tokens: ~${Math.round(p.tokensIn / 1000)}K in, ~${Math.round(p.tokensOut / 1000)}K out`,
44
+ );
45
+
46
+ if (p.topModels.length > 0) {
47
+ lines.push(` Top models:`);
48
+ for (const m of p.topModels.slice(0, 3)) {
49
+ lines.push(` • ${m.modelId.split("/").pop()}: ${m.count} req`);
50
+ }
51
+ }
52
+ lines.push("");
53
+ }
54
+
55
+ lines.push("━".repeat(50));
56
+ lines.push(
57
+ `📈 Totals: ${report.totalRequests} requests, ~${Math.round(report.totalTokensIn / 1000)}K tokens`,
58
+ );
59
+ lines.push("━".repeat(50));
60
+
61
+ return lines.join("\n");
62
+ }
63
+
64
+ export function formatCumulativeUsage(report: CumulativeUsageReport): string {
65
+ if (report.providers.length === 0) {
66
+ return "No cumulative usage data yet. Start using free models!";
67
+ }
68
+
69
+ const lines: string[] = [];
70
+ lines.push("━".repeat(50));
71
+ lines.push("📊 Total Usage (All Time)");
72
+ lines.push("━".repeat(50));
73
+ lines.push("");
74
+
75
+ for (const p of report.providers) {
76
+ lines.push(`🔹 ${p.name}`);
77
+ lines.push(` Requests: ${p.totalRequests.toLocaleString()}`);
78
+ lines.push(
79
+ ` Tokens: ~${Math.round(p.totalTokensIn / 1000).toLocaleString()}K in, ~${Math.round(p.totalTokensOut / 1000).toLocaleString()}K out`,
80
+ );
81
+ lines.push(` Models used: ${p.modelCount}`);
82
+
83
+ if (p.topModels.length > 0) {
84
+ lines.push(` Top models:`);
85
+ for (const m of p.topModels.slice(0, 3)) {
86
+ lines.push(
87
+ ` • ${m.modelId.split("/").pop()}: ${m.count.toLocaleString()} req`,
88
+ );
89
+ }
90
+ }
91
+
92
+ lines.push(` Active since: ${p.firstUsed.split("T")[0]}`);
93
+ lines.push("");
94
+ }
95
+
96
+ lines.push("━".repeat(50));
97
+ lines.push(`📈 Grand Totals:`);
98
+ lines.push(` ${report.grandTotalRequests.toLocaleString()} requests`);
99
+ lines.push(
100
+ ` ~${Math.round(report.grandTotalTokensIn / 1000).toLocaleString()}K input tokens`,
101
+ );
102
+ lines.push(
103
+ ` ~${Math.round(report.grandTotalTokensOut / 1000).toLocaleString()}K output tokens`,
104
+ );
105
+ lines.push("━".repeat(50));
106
+
107
+ return lines.join("\n");
108
+ }
109
+
110
+ export function formatFreeTierStatus(provider: string): string {
111
+ const usage = getFreeTierUsage(provider);
112
+ const parts: string[] = [];
113
+
114
+ if (usage.limit.requestsPerHour) {
115
+ parts.push(`${usage.requestsThisHour}/${usage.limit.requestsPerHour}/h`);
116
+ }
117
+ if (usage.limit.requestsPerDay) {
118
+ parts.push(`${usage.requestsToday}/${usage.limit.requestsPerDay}/d`);
119
+ }
120
+ if (usage.limit.requestsPerMonth) {
121
+ parts.push(
122
+ `${usage.requestsThisMonth ?? 0}/${usage.limit.requestsPerMonth}/mo`,
123
+ );
124
+ }
125
+
126
+ if (parts.length === 0) {
127
+ return `${provider}: ${usage.limit.description}`;
128
+ }
129
+
130
+ return `${provider}: ${parts.join(" | ")}`;
131
+ }