pi-sophnet 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.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # pi-sophnet
2
+
3
+ Sophnet provider extension for [pi](https://pi.dev). Registers Sophnet as a custom LLM provider and displays real-time billing info (balance, monthly cost, today's cost) in the status bar.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:pi-sophnet
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Set your Sophnet API key using one of:
14
+
15
+ 1. **Environment variable:** `export SOPHNET_API_KEY=your-key`
16
+ 2. **pi auth:** Run `/login sophnet` inside pi and follow the prompt
17
+
18
+ ## Features
19
+
20
+ - **Provider registration** — Adds the Sophnet provider with DeepSeek, GLM, MiniMax, Kimi, and Qwen models
21
+ - **Status bar** — Shows balance (¥), monthly cost (M), and today's cost (D), refreshed every 5 minutes and after each agent turn
22
+ - **`/sophnet-balance` command** — Detailed billing breakdown with paid vs. gift balance
23
+
24
+ ## Models
25
+
26
+ | Model | Context | Pricing (input/output ¥/1M tokens) |
27
+ |-------|---------|-------------------------------------|
28
+ | DeepSeek-V4-Pro | 1M | ¥9 / ¥18 |
29
+ | DeepSeek-V4-Flash | 1M | ¥1 / ¥2 |
30
+ | GLM-5.1 | 200K | ¥8 / ¥28 |
31
+ | MiniMax-M3 | 512K | ¥2.1 / ¥8.4 |
32
+ | Kimi-K2.6 | 256K | ¥6.5 / ¥27 |
33
+ | qwen3.7-max | 524K | ¥6 / ¥18 |
34
+
35
+ ## License
36
+
37
+ MIT
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Sophnet Provider Extension for pi
3
+ *
4
+ * - Registers the Sophnet provider with all models and thinking-level maps
5
+ * - Shows balance / monthly cost / today's cost in the status bar
6
+ * - /sophnet-balance command for detailed billing info
7
+ *
8
+ * Setup: set SOPHNET_API_KEY env var, or run /login sophnet
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import { readFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ // ═══════════════════════════════════════════════════════════════════════════════
17
+ // API Key Resolution
18
+ // ═══════════════════════════════════════════════════════════════════════════════
19
+
20
+ function resolveApiKey(): string {
21
+ const envKey = process.env.SOPHNET_API_KEY;
22
+ if (envKey) return envKey;
23
+ try {
24
+ const authPath = join(homedir(), ".pi", "agent", "auth.json");
25
+ const auth = JSON.parse(readFileSync(authPath, "utf-8"));
26
+ const entry = auth.sophnet;
27
+ if (entry) return typeof entry === "string" ? entry : entry.key ?? "";
28
+ } catch { /* ignore */ }
29
+ return "";
30
+ }
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════════
33
+ // Sophnet Billing API
34
+ // ═══════════════════════════════════════════════════════════════════════════════
35
+
36
+ const API_BASE = "https://www.sophnet.com/api/open-apis";
37
+
38
+ interface Balance { total: number; paid: number; gift: number }
39
+
40
+ async function apiFetch(path: string, apiKey: string, signal?: AbortSignal): Promise<any> {
41
+ const res = await fetch(`${API_BASE}${path}`, {
42
+ headers: { Authorization: `Bearer ${apiKey}` },
43
+ signal,
44
+ });
45
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
46
+ const data = await res.json();
47
+ if (data.status !== 0) throw new Error(data.message ?? "API error");
48
+ return data.result;
49
+ }
50
+
51
+ const getBalance = (k: string, s?: AbortSignal) =>
52
+ apiFetch("/projects/balance", k, s).then((r: any): Balance => ({
53
+ total: r.currentBalance ?? 0,
54
+ paid: r.currentBalanceWithoutGift ?? 0,
55
+ gift: r.currentGiftBalance ?? 0,
56
+ }));
57
+
58
+ const getUsageCost = (k: string, begin: string, end: string, s?: AbortSignal) =>
59
+ apiFetch(`/projects/usage_detail?beginTime=${begin}&endTime=${end}`, k, s)
60
+ .then((r: any) => (r.costSummary as number) ?? 0);
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════════════
63
+ // Formatting Helpers
64
+ // ═══════════════════════════════════════════════════════════════════════════════
65
+
66
+ const STATUS_KEY = "sophnet-billing";
67
+ const REFRESH_MS = 5 * 60 * 1000;
68
+
69
+ interface BillingState { balance: Balance; monthlyCost: number; todayCost: number }
70
+
71
+ function ymd(d: Date) {
72
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
73
+ }
74
+ function monthStart(d: Date) {
75
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`;
76
+ }
77
+ function fmtCNY(n: number): string {
78
+ if (n >= 10000) return `${(n / 10000).toFixed(1)}w`;
79
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
80
+ return n.toFixed(2);
81
+ }
82
+ function renderBilling(s: BillingState | null, err: string | null, theme: any): string {
83
+ if (err) return theme.fg("dim", "sophnet ") + theme.fg("error", err);
84
+ if (!s) return theme.fg("dim", "sophnet ···");
85
+ return [
86
+ theme.fg("success", `¥${Math.round(s.balance.total)}`),
87
+ theme.fg("dim", "M ") + theme.fg("accent", `¥${fmtCNY(s.monthlyCost)}`),
88
+ theme.fg("dim", "D ") + theme.fg("accent", `¥${fmtCNY(s.todayCost)}`),
89
+ ].join(" ");
90
+ }
91
+
92
+ // ═══════════════════════════════════════════════════════════════════════════════
93
+ // Provider Config
94
+ // ═══════════════════════════════════════════════════════════════════════════════
95
+
96
+ const SHARED_COMPAT = {
97
+ supportsDeveloperRole: false,
98
+ maxTokensField: "max_tokens" as const,
99
+ supportsReasoningEffort: true,
100
+ thinkingFormat: "deepseek" as const,
101
+ };
102
+
103
+ const DEEPSEEK_THINKING = { off: "disabled", minimal: null, low: "low", medium: "medium", high: "high", xhigh: "max" };
104
+ const GLM_THINKING = { off: "none", minimal: "minimal", low: "low", medium: "medium", high: "high", xhigh: "xhigh" };
105
+ const MINIMAX_THINKING = { off: null, minimal: "minimal", low: "low", medium: "medium", high: "high", xhigh: "xhigh" };
106
+ const KIMI_THINKING = { off: "none", minimal: "minimal", low: "low", medium: "medium", high: "high", xhigh: "xhigh" };
107
+ const QWEN_THINKING = { off: "none", minimal: "minimal", low: "low", medium: "medium", high: "high", xhigh: "xhigh" };
108
+
109
+ const SOPHNET_MODELS = [
110
+ {
111
+ id: "DeepSeek-V4-Pro", name: "DeepSeek-V4-Pro", reasoning: true, input: ["text"] as const,
112
+ contextWindow: 1_000_000, maxTokens: 65536,
113
+ cost: { input: 9, output: 18, cacheRead: 0, cacheWrite: 0 },
114
+ compat: SHARED_COMPAT, thinkingLevelMap: DEEPSEEK_THINKING,
115
+ },
116
+ {
117
+ id: "DeepSeek-V4-Flash", name: "DeepSeek-V4-Flash", reasoning: true, input: ["text"] as const,
118
+ contextWindow: 1_000_000, maxTokens: 65536,
119
+ cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
120
+ compat: SHARED_COMPAT, thinkingLevelMap: DEEPSEEK_THINKING,
121
+ },
122
+ {
123
+ id: "GLM-5.1", name: "GLM-5.1", reasoning: true, input: ["text"] as const,
124
+ contextWindow: 200_000, maxTokens: 65536,
125
+ cost: { input: 8, output: 28, cacheRead: 0, cacheWrite: 0 },
126
+ compat: SHARED_COMPAT, thinkingLevelMap: GLM_THINKING,
127
+ },
128
+ {
129
+ id: "MiniMax-M3", name: "MiniMax-M3", reasoning: true, input: ["text"] as const,
130
+ contextWindow: 512_000, maxTokens: 65536,
131
+ cost: { input: 2.1, output: 8.4, cacheRead: 0, cacheWrite: 0 },
132
+ compat: SHARED_COMPAT, thinkingLevelMap: MINIMAX_THINKING,
133
+ },
134
+ {
135
+ id: "Kimi-K2.6", name: "Kimi-K2.6", reasoning: true, input: ["text"] as const,
136
+ contextWindow: 256_000, maxTokens: 65536,
137
+ cost: { input: 6.5, output: 27, cacheRead: 0, cacheWrite: 0 },
138
+ compat: SHARED_COMPAT, thinkingLevelMap: KIMI_THINKING,
139
+ },
140
+ {
141
+ id: "qwen3.7-max", name: "qwen3.7-max", reasoning: true, input: ["text"] as const,
142
+ contextWindow: 524_288, maxTokens: 65536,
143
+ cost: { input: 6, output: 18, cacheRead: 0, cacheWrite: 0 },
144
+ compat: SHARED_COMPAT, thinkingLevelMap: QWEN_THINKING,
145
+ },
146
+ ];
147
+
148
+ // ═══════════════════════════════════════════════════════════════════════════════
149
+ // Extension Entry Point
150
+ // ═══════════════════════════════════════════════════════════════════════════════
151
+
152
+ export default function (pi: ExtensionAPI) {
153
+ const apiKey = resolveApiKey();
154
+
155
+ // ── Register Provider ─────────────────────────────────────────────────
156
+ pi.registerProvider("sophnet", {
157
+ name: "Sophnet",
158
+ baseUrl: "https://www.sophnet.com/api/open-apis/v1",
159
+ apiKey: apiKey || "$SOPHNET_API_KEY",
160
+ api: "openai-completions",
161
+ compat: SHARED_COMPAT,
162
+ models: SOPHNET_MODELS,
163
+ });
164
+
165
+ // ── Billing State ─────────────────────────────────────────────────────
166
+ if (!apiKey) return;
167
+
168
+ let billing: BillingState | null = null;
169
+ let billingErr: string | null = null;
170
+ let refreshTimer: ReturnType<typeof setInterval> | null = null;
171
+
172
+ async function refreshBilling(): Promise<void> {
173
+ const d = new Date();
174
+ try {
175
+ const [bal, mCost, dCost] = await Promise.all([
176
+ getBalance(apiKey),
177
+ getUsageCost(apiKey, monthStart(d), ymd(d)),
178
+ getUsageCost(apiKey, ymd(d), ymd(d)),
179
+ ]);
180
+ billing = { balance: bal, monthlyCost: mCost, todayCost: dCost };
181
+ billingErr = null;
182
+ } catch (err: any) {
183
+ if (err.name === "AbortError") return;
184
+ billingErr = err.message.slice(0, 40);
185
+ }
186
+ }
187
+
188
+ // Fetch immediately + periodically
189
+ refreshBilling();
190
+ refreshTimer = setInterval(refreshBilling, REFRESH_MS);
191
+
192
+ // Update status bar on session start
193
+ pi.on("session_start", async (_event, ctx) => {
194
+ await refreshBilling();
195
+ ctx.ui.setStatus(STATUS_KEY, renderBilling(billing, billingErr, ctx.ui.theme));
196
+ });
197
+
198
+ // Refresh after each agent turn
199
+ pi.on("turn_end", async (_event, ctx) => {
200
+ await refreshBilling();
201
+ ctx.ui.setStatus(STATUS_KEY, renderBilling(billing, billingErr, ctx.ui.theme));
202
+ });
203
+
204
+ // Cleanup
205
+ pi.on("session_shutdown", async () => {
206
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
207
+ });
208
+
209
+ // ── /sophnet-balance ──────────────────────────────────────────────────
210
+ pi.registerCommand("sophnet-balance", {
211
+ description: "Show Sophnet balance and usage details",
212
+ handler: async (_args, ctx) => {
213
+ await ctx.waitForIdle();
214
+ ctx.ui.notify("Fetching Sophnet billing...", "info");
215
+ await refreshBilling();
216
+ ctx.ui.setStatus(STATUS_KEY, renderBilling(billing, billingErr, ctx.ui.theme));
217
+ if (billingErr) { ctx.ui.notify(`Error: ${billingErr}`, "error"); return; }
218
+ if (!billing) return;
219
+ ctx.ui.notify(
220
+ [
221
+ `💰 Balance: ¥${billing.balance.total.toFixed(4)}`,
222
+ ` Paid: ¥${billing.balance.paid.toFixed(4)} Gift: ¥${billing.balance.gift.toFixed(4)}`,
223
+ `📅 Monthly: ¥${billing.monthlyCost.toFixed(4)}`,
224
+ `📆 Today: ¥${billing.todayCost.toFixed(4)}`,
225
+ ].join("\n"), "info",
226
+ );
227
+ },
228
+ });
229
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "pi-sophnet",
3
+ "version": "1.0.0",
4
+ "description": "Sophnet provider extension for pi — register Sophnet models, display billing info in the status bar",
5
+ "keywords": ["pi-package"],
6
+ "license": "MIT",
7
+ "author": "zisen",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/zisen123/pi-sophnet"
11
+ },
12
+ "pi": {
13
+ "extensions": ["./extensions"]
14
+ },
15
+ "files": ["extensions/"],
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "*",
18
+ "typebox": "*"
19
+ }
20
+ }