pi-free 1.0.6 โ†’ 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-free",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "AIO Free AI models for Pi - Access free models from Kilo, Zen, OpenRouter, NVIDIA, Cline, Mistral, and Ollama",
6
6
  "keywords": [
@@ -34,6 +34,7 @@
34
34
  "lib/**/*.ts",
35
35
  "usage/**/*.ts",
36
36
  "provider-failover/**/*.ts",
37
+ "widget/**/*.ts",
37
38
  "config.ts",
38
39
  "constants.ts",
39
40
  "provider-factory.ts",
package/widget/data.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Widget data collection - aggregates metrics from all sources
3
+ */
4
+
5
+ import {
6
+ PROVIDER_CLINE,
7
+ PROVIDER_KILO,
8
+ PROVIDER_NVIDIA,
9
+ PROVIDER_OPENROUTER,
10
+ PROVIDER_ZEN,
11
+ } from "../constants.ts";
12
+ import {
13
+ getCachedMetrics,
14
+ getDailyRequestCount,
15
+ getRequestCount,
16
+ } from "../usage/metrics.ts";
17
+ import { getAllCumulativeUsage } from "../usage/store.ts";
18
+
19
+ export interface ProviderRow {
20
+ provider: string;
21
+ key: string;
22
+ icon: string;
23
+ sessionReqs: number;
24
+ dailyReqs: number;
25
+ dailyLimit?: number;
26
+ hourlyLimit?: number;
27
+ remainingToday?: number;
28
+ totalTokensIn: number;
29
+ totalTokensOut: number;
30
+ totalRequests: number;
31
+ costEquivalent: number;
32
+ firstUsed?: string;
33
+ credits?: number;
34
+ creditsLabel?: string;
35
+ }
36
+
37
+ const KNOWN_PROVIDERS: Record<string, { icon: string; label: string }> = {
38
+ [PROVIDER_KILO]: { icon: "๐Ÿ”ฅ", label: "Kilo" },
39
+ [PROVIDER_OPENROUTER]: { icon: "๐Ÿ”€", label: "OpenRouter" },
40
+ [PROVIDER_ZEN]: { icon: "โœฆ", label: "Zen" },
41
+ [PROVIDER_NVIDIA]: { icon: "โšก", label: "NVIDIA" },
42
+ [PROVIDER_CLINE]: { icon: "๐Ÿค–", label: "Cline" },
43
+ local: { icon: "๐Ÿ’ป", label: "Local" },
44
+ };
45
+
46
+ // Session-level request tracking for non-extension providers
47
+ const sessionRequestCounts = new Map<string, number>();
48
+
49
+ export function recordSessionRequest(provider: string): void {
50
+ const current = sessionRequestCounts.get(provider) ?? 0;
51
+ sessionRequestCounts.set(provider, current + 1);
52
+ }
53
+
54
+ export function collectRows(): ProviderRow[] {
55
+ const cumulative = getAllCumulativeUsage();
56
+ const orMetrics = getCachedMetrics(PROVIDER_OPENROUTER);
57
+ const kiloMetrics = getCachedMetrics(PROVIDER_KILO);
58
+
59
+ // Discover all providers: known ones + any from cumulative store
60
+ const allKeys = new Set<string>(Object.keys(KNOWN_PROVIDERS));
61
+ for (const key of Object.keys(cumulative)) {
62
+ allKeys.add(key);
63
+ }
64
+
65
+ const rows: ProviderRow[] = [];
66
+
67
+ for (const key of allKeys) {
68
+ const meta = KNOWN_PROVIDERS[key];
69
+ const c = cumulative[key];
70
+ const sessionReqs =
71
+ getRequestCount(key) || sessionRequestCounts.get(key) || 0;
72
+
73
+ const row: ProviderRow = {
74
+ provider: meta?.label ?? key,
75
+ key,
76
+ icon: meta?.icon ?? "๐Ÿ“ฆ",
77
+ sessionReqs,
78
+ dailyReqs: getDailyRequestCount(key) || 0,
79
+ totalTokensIn: c?.tokensIn ?? 0,
80
+ totalTokensOut: c?.tokensOut ?? 0,
81
+ totalRequests: c?.requests ?? 0,
82
+ costEquivalent: c?.costEquivalent ?? 0,
83
+ firstUsed: c?.firstUsed,
84
+ };
85
+
86
+ // Provider-specific known limits
87
+ if (key === PROVIDER_OPENROUTER) {
88
+ row.dailyLimit = orMetrics?.rateLimit?.requestsPerDay;
89
+ row.remainingToday = orMetrics?.rateLimit?.remainingToday;
90
+ row.credits = orMetrics?.credits;
91
+ row.creditsLabel = "credits";
92
+ } else if (key === PROVIDER_KILO) {
93
+ row.hourlyLimit = 200;
94
+ row.credits = kiloMetrics?.balance;
95
+ row.creditsLabel = "balance";
96
+ }
97
+
98
+ rows.push(row);
99
+ }
100
+
101
+ // Sort: known providers first (in order), then unknown, then inactive
102
+ const order = Object.keys(KNOWN_PROVIDERS);
103
+ rows.sort((a, b) => {
104
+ const ai = order.indexOf(a.key);
105
+ const bi = order.indexOf(b.key);
106
+ const aOrder = ai >= 0 ? ai : 100;
107
+ const bOrder = bi >= 0 ? bi : 100;
108
+ if (aOrder !== bOrder) return aOrder - bOrder;
109
+ return b.sessionReqs + b.totalRequests - (a.sessionReqs + a.totalRequests);
110
+ });
111
+
112
+ return rows;
113
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Widget formatting utilities
3
+ */
4
+
5
+ export function formatTokens(n: number): string {
6
+ if (n < 1000) return n.toString();
7
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
8
+ if (n < 1_000_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
9
+ return `${(n / 1_000_000_000).toFixed(1)}B`;
10
+ }
11
+
12
+ export function formatCost(n: number): string {
13
+ if (n < 0.01) return `$${n.toFixed(4)}`;
14
+ if (n < 1) return `$${n.toFixed(3)}`;
15
+ return `$${n.toFixed(2)}`;
16
+ }
17
+
18
+ export function relativeTime(isoDate: string): string {
19
+ const diff = Date.now() - new Date(isoDate).getTime();
20
+ const days = Math.floor(diff / 86_400_000);
21
+ if (days === 0) return "today";
22
+ if (days === 1) return "yesterday";
23
+ if (days < 30) return `${days}d ago`;
24
+ const months = Math.floor(days / 30);
25
+ return `${months}mo ago`;
26
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Widget HTML rendering
3
+ */
4
+
5
+ import type { ProviderRow } from "./data.ts";
6
+ import { formatCost, formatTokens, relativeTime } from "./format.ts";
7
+
8
+ export function renderWidgetHTML(rows: ProviderRow[]): string {
9
+ let totalTokens = 0,
10
+ totalReqs = 0,
11
+ totalCost = 0;
12
+ for (const r of rows) {
13
+ totalTokens += r.totalTokensIn + r.totalTokensOut;
14
+ totalReqs += r.totalRequests;
15
+ totalCost += r.costEquivalent;
16
+ }
17
+
18
+ const summaryHTML =
19
+ totalReqs > 0
20
+ ? `
21
+ <div style="background: rgba(255,255,255,0.04); border-radius: 8px; padding: 10px 12px; margin-bottom: 12px;">
22
+ <div style="display: flex; justify-content: space-between; font-size: 13px; font-weight: 500;">
23
+ <span>Total free value</span>
24
+ <span style="color: #48bb78;">${formatCost(totalCost)} saved</span>
25
+ </div>
26
+ <div style="font-size: 11px; color: #888; margin-top: 3px;">
27
+ ${formatTokens(totalTokens)} tokens ยท ${totalReqs} requests
28
+ </div>
29
+ </div>`
30
+ : "";
31
+
32
+ const providerRows = rows.map((r) => renderProviderRow(r)).join("\n");
33
+
34
+ return `<!DOCTYPE html>
35
+ <html><head><meta charset="UTF-8">
36
+ <style>
37
+ * { margin: 0; padding: 0; box-sizing: border-box; }
38
+ body {
39
+ font-family: system-ui, -apple-system, sans-serif;
40
+ background: rgba(24, 24, 30, 0.95);
41
+ color: #e0e0e0; padding: 14px 16px;
42
+ min-height: 100vh; backdrop-filter: blur(20px);
43
+ }
44
+ .header { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
45
+ </style></head>
46
+ <body>
47
+ <div class="header">Free Usage</div>
48
+ ${summaryHTML}
49
+ ${providerRows}
50
+ <div style="font-size: 10px; color: #555; margin-top: 10px; padding-top: 8px; border-top: 1px solid rgba(255,255,255,0.05);">
51
+ Each gateway has independent quotas โ€” using multiple providers multiplies capacity.
52
+ </div>
53
+ </body></html>`;
54
+ }
55
+
56
+ function renderProviderRow(r: ProviderRow): string {
57
+ const hasActivity = r.sessionReqs > 0 || r.totalRequests > 0;
58
+ const quotaBar = renderQuotaBar(r);
59
+ const infoHTML = renderInfoLine(r);
60
+
61
+ return `
62
+ <div style="padding: 7px 0; ${hasActivity ? "" : "opacity: 0.3;"}">
63
+ <div style="display: flex; justify-content: space-between; align-items: center;">
64
+ <span style="font-weight: 500; font-size: 13px;">${r.icon} ${r.provider}</span>
65
+ <span style="font-size: 10px; color: #888;">${r.sessionReqs} this session</span>
66
+ </div>
67
+ ${infoHTML}
68
+ ${quotaBar}
69
+ </div>`;
70
+ }
71
+
72
+ function renderQuotaBar(r: ProviderRow): string {
73
+ if (!r.dailyLimit || r.dailyLimit <= 0) return "";
74
+
75
+ const pct = Math.min(100, Math.round((r.dailyReqs / r.dailyLimit) * 100));
76
+ const color = pct > 80 ? "#e53e3e" : pct > 50 ? "#ecc94b" : "#48bb78";
77
+
78
+ return `
79
+ <div style="display: flex; align-items: center; gap: 8px; margin-top: 3px;">
80
+ <div style="flex: 1; height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; overflow: hidden;">
81
+ <div style="width: ${pct}%; height: 100%; background: ${color}; border-radius: 2px;"></div>
82
+ </div>
83
+ <span style="font-size: 10px; color: #666;">${r.dailyReqs}/${r.dailyLimit}</span>
84
+ </div>`;
85
+ }
86
+
87
+ function renderInfoLine(r: ProviderRow): string {
88
+ const infoParts: string[] = [];
89
+
90
+ if (r.totalRequests > 0) {
91
+ infoParts.push(
92
+ `${formatTokens(r.totalTokensIn + r.totalTokensOut)} tok ยท ${r.totalRequests} reqs`,
93
+ );
94
+ if (r.costEquivalent > 0) {
95
+ infoParts.push(`โ‰ˆ${formatCost(r.costEquivalent)}`);
96
+ }
97
+ if (r.firstUsed) {
98
+ infoParts.push(`since ${relativeTime(r.firstUsed)}`);
99
+ }
100
+ }
101
+ if (r.remainingToday !== undefined) {
102
+ infoParts.push(`${r.remainingToday} left today`);
103
+ }
104
+ if (r.hourlyLimit) {
105
+ infoParts.push(`${r.hourlyLimit}/hr limit`);
106
+ }
107
+ if (r.credits !== undefined) {
108
+ infoParts.push(`๐Ÿ’ฐ ${formatCost(r.credits)}`);
109
+ }
110
+ if (r.key === "local" && r.totalRequests > 0) {
111
+ infoParts.push("always free");
112
+ }
113
+
114
+ if (infoParts.length === 0) return "";
115
+
116
+ return `<div style="font-size: 10px; color: #777; margin-top: 2px;">${infoParts.join(" ยท ")}</div>`;
117
+ }