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 +37 -0
- package/extensions/index.ts +229 -0
- package/package.json +20 -0
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
|
+
}
|