dsh-any-deepseek 0.0.13 → 0.0.15
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/lib/balance.d.ts +19 -0
- package/lib/channels.d.ts +4 -1
- package/lib/client.js +9 -0
- package/lib/http.d.ts +66 -3
- package/lib/index.js +163 -21
- package/lib/presets.d.ts +18 -13
- package/package.json +1 -1
package/lib/balance.d.ts
CHANGED
|
@@ -119,6 +119,25 @@ export declare function parseOllamaBalance(body: unknown): BalanceReading | unde
|
|
|
119
119
|
* @returns the reading, or undefined when neither body is this shape.
|
|
120
120
|
*/
|
|
121
121
|
export declare function parseOpenAIBilling(subscription: unknown, usage: unknown): BalanceReading | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Parse OpenCode Go's `GET /zen/go/v1/usage`.
|
|
124
|
+
*
|
|
125
|
+
* 🔴 **This surface reports WINDOWS, never money.** The owner remembered correctly that OpenCode
|
|
126
|
+
* exposes a usage query — and it does; what it does not expose (to an API key) is the credit
|
|
127
|
+
* wallet, which lives behind a logged-in browser session. The only key-authenticated money signal
|
|
128
|
+
* is the error thrown at $0 (anomalyco/opencode#44189), which is why the feature request asks for
|
|
129
|
+
* a `balance` object to be added to this very endpoint.
|
|
130
|
+
*
|
|
131
|
+
* Two response shapes are accepted, because a monitor should not break when the provider ships
|
|
132
|
+
* the requested addition:
|
|
133
|
+
* · `{ usage: { rolling: {percent, resetsAt}, weekly: … , monthly: … } }`
|
|
134
|
+
* · the same keys hoisted to the top level.
|
|
135
|
+
* A `balance` object is read IF PRESENT — so the day they merge that request, the number appears
|
|
136
|
+
* with no code change here.
|
|
137
|
+
* @param body - the parsed JSON body.
|
|
138
|
+
* @returns the reading, or undefined when the body carries no recognizable window.
|
|
139
|
+
*/
|
|
140
|
+
export declare function parseOpencodeWindows(body: unknown): BalanceReading | undefined;
|
|
122
141
|
/**
|
|
123
142
|
* Map one channel's raw bodies onto a reading, dispatching on the declared shape.
|
|
124
143
|
* @param probe - the preset's declared probe.
|
package/lib/channels.d.ts
CHANGED
|
@@ -102,7 +102,10 @@ export interface ResolvedChannel {
|
|
|
102
102
|
* How this endpoint's remaining balance can be read, when it can be.
|
|
103
103
|
*
|
|
104
104
|
* Absent means "no balance API" — a real state, not a missing value. See `presets.ts` for the
|
|
105
|
-
*
|
|
105
|
+
* four verified shapes. Note the two different kinds of "no number": OpenCode Go reports usage
|
|
106
|
+
* WINDOWS rather than money (it does have a surface), while Open Design publishes no surface at
|
|
107
|
+
* all (measured: seven candidates, all 404). The panel distinguishes them, and checks liveness
|
|
108
|
+
* for the latter instead of inventing an amount.
|
|
106
109
|
*/
|
|
107
110
|
balance?: BalanceProbe;
|
|
108
111
|
/** Facts the user should see because a default was applied rather than declared. */
|
package/lib/client.js
CHANGED
|
@@ -27,12 +27,20 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
*
|
|
28
28
|
* Deliberately total: every status produces words, never an empty string. A blank cell beside a
|
|
29
29
|
* working channel is indistinguishable from "zero balance" — the one thing this must never imply.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 **`alive` is worded as 「余额未尽」, never as a number.** The check behind it is one minimal
|
|
32
|
+
* live request, which proves the endpoint serves and the account is not blocked for lack of
|
|
33
|
+
* credit. It does not measure an amount, so printing one — or printing `0.00` — would invent the
|
|
34
|
+
* very figure this status exists to avoid inventing (owner directive 2026-10-23: 「不支持查余额的,
|
|
35
|
+
* 改成直接测通,通则说明余额还是有的,没用完」).
|
|
30
36
|
* @param row - one row from `/balance`.
|
|
31
37
|
* @returns a short human label.
|
|
32
38
|
*/
|
|
33
39
|
function formatBalance(row) {
|
|
40
|
+
if (row.status === "alive") return `余额未尽(无余额接口,实测可用:${row.responded === void 0 ? `${Math.round(row.ms)}ms` : `${row.responded} · ${Math.round(row.ms)}ms`})`;
|
|
34
41
|
if (row.status !== "ok") {
|
|
35
42
|
if (row.status === "no-key") return "未配置密钥";
|
|
43
|
+
if (row.code === "QUOTA") return "余额不足(实测被拒)";
|
|
36
44
|
if (row.status === "unavailable") return row.code === "HTTP_404" ? "不支持查询" : "读取失败";
|
|
37
45
|
return "不支持查询";
|
|
38
46
|
}
|
|
@@ -508,6 +516,7 @@ window.__ModuleLoader__.load({
|
|
|
508
516
|
type: "button",
|
|
509
517
|
className: "ad-link",
|
|
510
518
|
disabled: balancesLoading || rows.length === 0,
|
|
519
|
+
title: "无余额接口的渠道会用一次最小真实请求测通(消耗一两个 token)",
|
|
511
520
|
onClick: () => {
|
|
512
521
|
refreshBalances();
|
|
513
522
|
},
|
package/lib/http.d.ts
CHANGED
|
@@ -121,10 +121,11 @@ export declare function reportedName(text: string): string;
|
|
|
121
121
|
/**
|
|
122
122
|
* Fetch one channel's remaining balance from whatever surface it declares.
|
|
123
123
|
*
|
|
124
|
-
* 🔴 **Absence is a normal answer here, and is reported as such.**
|
|
124
|
+
* 🔴 **Absence is a normal answer here, and is reported as such.** Outcomes are kept apart
|
|
125
125
|
* because they mean different things to the user:
|
|
126
|
-
* · `unsupported` — the preset declares no balance shape
|
|
127
|
-
*
|
|
126
|
+
* · `unsupported` — the preset declares no balance shape. This is no longer the end of the story
|
|
127
|
+
* for the panel: `/{@link balanceRow}` turns it into a liveness verdict, because "the endpoint
|
|
128
|
+
* will not tell me the amount" and "the account is fine" are different facts and both matter.
|
|
128
129
|
* · `unavailable` — a shape was declared but the endpoint did not answer as expected (404,
|
|
129
130
|
* non-JSON, unexpected body). The relay may simply not implement it.
|
|
130
131
|
* · `ok` — a reading, which itself may carry only a note (e.g. "unlimited, used $3.20").
|
|
@@ -147,6 +148,68 @@ export declare function fetchBalance(channel: ResolvedChannel, key: string, sign
|
|
|
147
148
|
detail: string;
|
|
148
149
|
code?: string;
|
|
149
150
|
}>;
|
|
151
|
+
/**
|
|
152
|
+
* One row of `/balance`, as it travels to the panel.
|
|
153
|
+
*
|
|
154
|
+
* `alive` is the fourth outcome and exists because of an owner directive (2026-10-23):
|
|
155
|
+
* 「对于不支持查余额的,改成直接测通,通则说明余额还是有的,没用完,显示出来(消耗一两个token)」.
|
|
156
|
+
*
|
|
157
|
+
* 🔴 **`alive` is NOT a balance number and must never be rendered as one.** It is the result of a
|
|
158
|
+
* minimal real inference request, and it proves exactly three things: the key works, the endpoint
|
|
159
|
+
* serves, and the account is not blocked for lack of credit (a $0 account answers 402/quota, it
|
|
160
|
+
* does not answer 200). It says nothing about HOW MUCH is left. The panel wording therefore says
|
|
161
|
+
* 「余额未尽」 — "not exhausted" — which is the strongest true claim available here, and stops
|
|
162
|
+
* short of the number a reader might otherwise assume.
|
|
163
|
+
*/
|
|
164
|
+
export type BalanceRowWire = {
|
|
165
|
+
id: string;
|
|
166
|
+
label: string;
|
|
167
|
+
status: 'ok';
|
|
168
|
+
reading: BalanceReading;
|
|
169
|
+
} | {
|
|
170
|
+
id: string;
|
|
171
|
+
label: string;
|
|
172
|
+
status: 'unsupported';
|
|
173
|
+
detail: string;
|
|
174
|
+
} | {
|
|
175
|
+
id: string;
|
|
176
|
+
label: string;
|
|
177
|
+
status: 'unavailable';
|
|
178
|
+
detail: string;
|
|
179
|
+
code?: string;
|
|
180
|
+
} | {
|
|
181
|
+
id: string;
|
|
182
|
+
label: string;
|
|
183
|
+
status: 'alive';
|
|
184
|
+
/** How long the liveness request took. */
|
|
185
|
+
ms: number;
|
|
186
|
+
/** What the relay called its model — a serving endpoint answers with a name. */
|
|
187
|
+
responded?: string;
|
|
188
|
+
detail: string;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* The stated reason an `alive` row carries no number.
|
|
192
|
+
*
|
|
193
|
+
* Kept as a constant so the server and every reader of the payload say the same thing: this row
|
|
194
|
+
* is about liveness, not about a balance.
|
|
195
|
+
*/
|
|
196
|
+
export declare const ALIVE_DETAIL: string;
|
|
197
|
+
/**
|
|
198
|
+
* Build one `/balance` row, falling back to a liveness probe when there is no balance surface.
|
|
199
|
+
*
|
|
200
|
+
* 🔴 **The fallback spends tokens, so it is scoped as tightly as possible**: it runs ONLY when the
|
|
201
|
+
* balance surface is absent (`unsupported`), never for a channel whose balance API answered `ok`
|
|
202
|
+
* and never for one that exists but failed (`unavailable` — that is a "try again" state, and
|
|
203
|
+
* probing it would replace a transient error with a different-looking success).
|
|
204
|
+
*
|
|
205
|
+
* The cost is one prompt plus one output token on a completions channel (`max_tokens: 1` in
|
|
206
|
+
* {@link probeRequest}); the Responses cap is a ceiling, not a spend.
|
|
207
|
+
* @param channel - the resolved channel.
|
|
208
|
+
* @param key - its resolved credential (already resolved by the caller, so a probe and a real
|
|
209
|
+
* request never read the key differently).
|
|
210
|
+
* @returns the row, always resolved — one dead relay must not blank the others.
|
|
211
|
+
*/
|
|
212
|
+
export declare function balanceRow(channel: ResolvedChannel, key: string): Promise<BalanceRowWire>;
|
|
150
213
|
/**
|
|
151
214
|
* Register the panel's routes.
|
|
152
215
|
* @param ctx - plugin context (only used to bind the disposer when the host offers one).
|
package/lib/index.js
CHANGED
|
@@ -278,7 +278,25 @@ const CHANNEL_PRESETS = {
|
|
|
278
278
|
...V41_FLASH_CAPS,
|
|
279
279
|
compat: { ...DEEPSEEK_THINKING_COMPAT },
|
|
280
280
|
headers: { ...OPENCODE_HEADERS },
|
|
281
|
-
|
|
281
|
+
/**
|
|
282
|
+
* 🔴 **The owner was right that OpenCode has a query surface — and it reports WINDOWS, not
|
|
283
|
+
* money.** Both halves of that sentence were measured, in that order:
|
|
284
|
+
*
|
|
285
|
+
* 1. A route exists at `/zen/go/v1/usage`. Proven by status code discrimination without a
|
|
286
|
+
* key: a made-up path under the same prefix answers **404**, while `/usage` answers
|
|
287
|
+
* **401** (route present, auth required). `/zen/v1/balance` answers 404 — that one really
|
|
288
|
+
* does not exist (it is what anomalyco/opencode#44189 is asking for).
|
|
289
|
+
* 2. What it returns is Go-subscription usage windows (rolling / weekly / monthly), **not**
|
|
290
|
+
* the credit wallet. The balance is only reachable from a logged-in browser session
|
|
291
|
+
* (`BillingTable.balance` behind a SolidStart server function), so a key cannot read it —
|
|
292
|
+
* the only key-authenticated money signal is the error thrown at $0.
|
|
293
|
+
*
|
|
294
|
+
* ⇒ Declared as a `windows` shape, which is exactly what this endpoint honestly provides.
|
|
295
|
+
* The credit balance stays absent rather than invented; the panel shows the windows and does
|
|
296
|
+
* not pretend a currency amount exists.
|
|
297
|
+
*/
|
|
298
|
+
balance: { shape: "opencode-windows" },
|
|
299
|
+
note: "Requires the OpenCode session headers; shipped with the preset. Usage windows only — the credit balance is not readable with an API key."
|
|
282
300
|
},
|
|
283
301
|
commandcode: {
|
|
284
302
|
displayName: "CommandCode",
|
|
@@ -304,8 +322,7 @@ const CHANNEL_PRESETS = {
|
|
|
304
322
|
api: "openai-completions",
|
|
305
323
|
baseURL: "https://amr-link.open-design.ai/v1",
|
|
306
324
|
model: "deepseek-v4.1-flash",
|
|
307
|
-
...V41_FLASH_CAPS
|
|
308
|
-
balance: { shape: "openai-billing" }
|
|
325
|
+
...V41_FLASH_CAPS
|
|
309
326
|
},
|
|
310
327
|
"deepseek-official": {
|
|
311
328
|
displayName: "DeepSeek Official",
|
|
@@ -1792,6 +1809,7 @@ function balanceUrls(probe, channelBaseURL) {
|
|
|
1792
1809
|
switch (probe.shape) {
|
|
1793
1810
|
case "deepseek": return [`${root}/user/balance`];
|
|
1794
1811
|
case "ollama": return [`${root}/api/balance`];
|
|
1812
|
+
case "opencode-windows": return [`${root}/usage`];
|
|
1795
1813
|
case "openai-billing": return [`${root}/dashboard/billing/subscription`, `${root}/dashboard/billing/usage`];
|
|
1796
1814
|
}
|
|
1797
1815
|
}
|
|
@@ -1951,6 +1969,58 @@ function parseOpenAIBilling(subscription, usage) {
|
|
|
1951
1969
|
};
|
|
1952
1970
|
}
|
|
1953
1971
|
/**
|
|
1972
|
+
* Parse OpenCode Go's `GET /zen/go/v1/usage`.
|
|
1973
|
+
*
|
|
1974
|
+
* 🔴 **This surface reports WINDOWS, never money.** The owner remembered correctly that OpenCode
|
|
1975
|
+
* exposes a usage query — and it does; what it does not expose (to an API key) is the credit
|
|
1976
|
+
* wallet, which lives behind a logged-in browser session. The only key-authenticated money signal
|
|
1977
|
+
* is the error thrown at $0 (anomalyco/opencode#44189), which is why the feature request asks for
|
|
1978
|
+
* a `balance` object to be added to this very endpoint.
|
|
1979
|
+
*
|
|
1980
|
+
* Two response shapes are accepted, because a monitor should not break when the provider ships
|
|
1981
|
+
* the requested addition:
|
|
1982
|
+
* · `{ usage: { rolling: {percent, resetsAt}, weekly: … , monthly: … } }`
|
|
1983
|
+
* · the same keys hoisted to the top level.
|
|
1984
|
+
* A `balance` object is read IF PRESENT — so the day they merge that request, the number appears
|
|
1985
|
+
* with no code change here.
|
|
1986
|
+
* @param body - the parsed JSON body.
|
|
1987
|
+
* @returns the reading, or undefined when the body carries no recognizable window.
|
|
1988
|
+
*/
|
|
1989
|
+
function parseOpencodeWindows(body) {
|
|
1990
|
+
const root = asRecord(body);
|
|
1991
|
+
if (root === void 0) return void 0;
|
|
1992
|
+
const usage = asRecord(root.usage) ?? root;
|
|
1993
|
+
const windows = [];
|
|
1994
|
+
for (const name of [
|
|
1995
|
+
"rolling",
|
|
1996
|
+
"weekly",
|
|
1997
|
+
"monthly"
|
|
1998
|
+
]) {
|
|
1999
|
+
const window = asRecord(usage[name]);
|
|
2000
|
+
if (window === void 0) continue;
|
|
2001
|
+
const used = num(window.percent);
|
|
2002
|
+
if (used === void 0) continue;
|
|
2003
|
+
windows.push({
|
|
2004
|
+
name,
|
|
2005
|
+
remainingPercent: Math.max(0, Math.min(100, 100 - used)),
|
|
2006
|
+
...text(window.resetsAt) === void 0 ? {} : { resetsAt: text(window.resetsAt) }
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
const amounts = [];
|
|
2010
|
+
const balance = asRecord(root.balance);
|
|
2011
|
+
const usd = num(balance?.usd);
|
|
2012
|
+
if (usd !== void 0) amounts.push({
|
|
2013
|
+
currency: text(balance?.currency) ?? "USD",
|
|
2014
|
+
remaining: usd
|
|
2015
|
+
});
|
|
2016
|
+
if (windows.length === 0 && amounts.length === 0) return void 0;
|
|
2017
|
+
return {
|
|
2018
|
+
amounts,
|
|
2019
|
+
windows,
|
|
2020
|
+
note: amounts.length === 0 ? "subscription usage windows; the credit balance is not readable with an API key" : void 0
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
1954
2024
|
* Map one channel's raw bodies onto a reading, dispatching on the declared shape.
|
|
1955
2025
|
* @param probe - the preset's declared probe.
|
|
1956
2026
|
* @param bodies - response bodies in the order {@link balanceUrls} returned URLs.
|
|
@@ -1960,6 +2030,7 @@ function readBalance(probe, bodies) {
|
|
|
1960
2030
|
switch (probe.shape) {
|
|
1961
2031
|
case "deepseek": return parseDeepSeekBalance(bodies[0]);
|
|
1962
2032
|
case "ollama": return parseOllamaBalance(bodies[0]);
|
|
2033
|
+
case "opencode-windows": return parseOpencodeWindows(bodies[0]);
|
|
1963
2034
|
case "openai-billing": return parseOpenAIBilling(bodies[0], bodies[1]);
|
|
1964
2035
|
}
|
|
1965
2036
|
}
|
|
@@ -2164,10 +2235,11 @@ function reportedName(text) {
|
|
|
2164
2235
|
/**
|
|
2165
2236
|
* Fetch one channel's remaining balance from whatever surface it declares.
|
|
2166
2237
|
*
|
|
2167
|
-
* 🔴 **Absence is a normal answer here, and is reported as such.**
|
|
2238
|
+
* 🔴 **Absence is a normal answer here, and is reported as such.** Outcomes are kept apart
|
|
2168
2239
|
* because they mean different things to the user:
|
|
2169
|
-
* · `unsupported` — the preset declares no balance shape
|
|
2170
|
-
*
|
|
2240
|
+
* · `unsupported` — the preset declares no balance shape. This is no longer the end of the story
|
|
2241
|
+
* for the panel: `/{@link balanceRow}` turns it into a liveness verdict, because "the endpoint
|
|
2242
|
+
* will not tell me the amount" and "the account is fine" are different facts and both matter.
|
|
2171
2243
|
* · `unavailable` — a shape was declared but the endpoint did not answer as expected (404,
|
|
2172
2244
|
* non-JSON, unexpected body). The relay may simply not implement it.
|
|
2173
2245
|
* · `ok` — a reading, which itself may carry only a note (e.g. "unlimited, used $3.20").
|
|
@@ -2200,11 +2272,29 @@ async function fetchBalance(channel, key, signal) {
|
|
|
2200
2272
|
...signal === void 0 ? {} : { signal }
|
|
2201
2273
|
});
|
|
2202
2274
|
const text = await response.text();
|
|
2203
|
-
if (!response.ok)
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2275
|
+
if (!response.ok) {
|
|
2276
|
+
/**
|
|
2277
|
+
* 🔴 **404 means "this relay has no such route", not "the read failed".**
|
|
2278
|
+
*
|
|
2279
|
+
* The distinction decides what the user does next: `unsupported` says "this endpoint
|
|
2280
|
+
* does not publish a balance — stop looking", while `unavailable` says "it should have
|
|
2281
|
+
* but did not answer — try again". Reporting a 404 as a failure invites a retry loop
|
|
2282
|
+
* against a route that will never exist.
|
|
2283
|
+
*
|
|
2284
|
+
* Measured: Open Design answers 404 on every billing candidate while `/v1/models`
|
|
2285
|
+
* answers 401 (route exists, auth required) — that difference is how the two were told
|
|
2286
|
+
* apart without a key.
|
|
2287
|
+
*/
|
|
2288
|
+
if (response.status === 404) return {
|
|
2289
|
+
status: "unsupported",
|
|
2290
|
+
detail: "this relay has no balance route (HTTP 404)"
|
|
2291
|
+
};
|
|
2292
|
+
return {
|
|
2293
|
+
status: "unavailable",
|
|
2294
|
+
code: `HTTP_${response.status}`,
|
|
2295
|
+
detail: `balance endpoint answered HTTP ${response.status}`
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2208
2298
|
try {
|
|
2209
2299
|
bodies.push(JSON.parse(text));
|
|
2210
2300
|
} catch {
|
|
@@ -2230,6 +2320,61 @@ async function fetchBalance(channel, key, signal) {
|
|
|
2230
2320
|
reading
|
|
2231
2321
|
};
|
|
2232
2322
|
}
|
|
2323
|
+
/**
|
|
2324
|
+
* The stated reason an `alive` row carries no number.
|
|
2325
|
+
*
|
|
2326
|
+
* Kept as a constant so the server and every reader of the payload say the same thing: this row
|
|
2327
|
+
* is about liveness, not about a balance.
|
|
2328
|
+
*/
|
|
2329
|
+
const ALIVE_DETAIL = "this endpoint publishes no balance API; a minimal live request succeeded, so it is serving and its credit is not exhausted (no amount is readable)";
|
|
2330
|
+
/**
|
|
2331
|
+
* Build one `/balance` row, falling back to a liveness probe when there is no balance surface.
|
|
2332
|
+
*
|
|
2333
|
+
* 🔴 **The fallback spends tokens, so it is scoped as tightly as possible**: it runs ONLY when the
|
|
2334
|
+
* balance surface is absent (`unsupported`), never for a channel whose balance API answered `ok`
|
|
2335
|
+
* and never for one that exists but failed (`unavailable` — that is a "try again" state, and
|
|
2336
|
+
* probing it would replace a transient error with a different-looking success).
|
|
2337
|
+
*
|
|
2338
|
+
* The cost is one prompt plus one output token on a completions channel (`max_tokens: 1` in
|
|
2339
|
+
* {@link probeRequest}); the Responses cap is a ceiling, not a spend.
|
|
2340
|
+
* @param channel - the resolved channel.
|
|
2341
|
+
* @param key - its resolved credential (already resolved by the caller, so a probe and a real
|
|
2342
|
+
* request never read the key differently).
|
|
2343
|
+
* @returns the row, always resolved — one dead relay must not blank the others.
|
|
2344
|
+
*/
|
|
2345
|
+
async function balanceRow(channel, key) {
|
|
2346
|
+
const outcome = await fetchBalance(channel, key);
|
|
2347
|
+
if (outcome.status !== "unsupported") return {
|
|
2348
|
+
id: channel.id,
|
|
2349
|
+
label: channel.label,
|
|
2350
|
+
...outcome
|
|
2351
|
+
};
|
|
2352
|
+
const verdict = await probeChannel(channel, key, `probe:${channel.id}`);
|
|
2353
|
+
if (!verdict.ok)
|
|
2354
|
+
/**
|
|
2355
|
+
* The liveness request failed, so nothing was learned about the account.
|
|
2356
|
+
*
|
|
2357
|
+
* It is reported as `unavailable` rather than as a verdict of its own because that is what it
|
|
2358
|
+
* is: we tried to find out and did not. `code` is passed through untouched, which is what lets
|
|
2359
|
+
* the panel tell the one FAILURE THAT IS ACTUALLY AN ANSWER (`QUOTA` — the account is out of
|
|
2360
|
+
* credit) from the ones that are just errors.
|
|
2361
|
+
*/
|
|
2362
|
+
return {
|
|
2363
|
+
id: channel.id,
|
|
2364
|
+
label: channel.label,
|
|
2365
|
+
status: "unavailable",
|
|
2366
|
+
detail: `no balance API, and the liveness request failed: ${verdict.message ?? verdict.code ?? "unknown"}`,
|
|
2367
|
+
...verdict.code === void 0 ? {} : { code: verdict.code }
|
|
2368
|
+
};
|
|
2369
|
+
return {
|
|
2370
|
+
id: channel.id,
|
|
2371
|
+
label: channel.label,
|
|
2372
|
+
status: "alive",
|
|
2373
|
+
ms: verdict.ms,
|
|
2374
|
+
...verdict.responded === void 0 || verdict.responded === "" ? {} : { responded: verdict.responded },
|
|
2375
|
+
detail: ALIVE_DETAIL
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2233
2378
|
/** Map an HTTP status from a probe onto the same codes the runtime uses. */
|
|
2234
2379
|
function probeCode(status, body) {
|
|
2235
2380
|
if (status === 401 || status === 403) return "AUTH";
|
|
@@ -2380,10 +2525,12 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
2380
2525
|
/**
|
|
2381
2526
|
* Remaining balance per channel.
|
|
2382
2527
|
*
|
|
2383
|
-
* GET because it
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2528
|
+
* GET because it is a read, but 🔴 **it is no longer free**: a channel whose endpoint has no
|
|
2529
|
+
* balance surface is now checked with one minimal live request (owner directive 2026-10-23),
|
|
2530
|
+
* which costs a prompt plus one output token. The route stays GET because the panel's
|
|
2531
|
+
* button is what triggers it — nothing here fires on render. Every channel is attempted
|
|
2532
|
+
* independently: one relay without a balance API, or one that is down, must not blank the
|
|
2533
|
+
* others — the same rule the channel list follows for a broken entry.
|
|
2387
2534
|
*/
|
|
2388
2535
|
kind: "exact",
|
|
2389
2536
|
path: `${ROUTE_PREFIX}/balance`,
|
|
@@ -2397,12 +2544,7 @@ function registerStatsRoutes(ctx, webServer, deps) {
|
|
|
2397
2544
|
status: "no-key",
|
|
2398
2545
|
detail: "no key configured"
|
|
2399
2546
|
};
|
|
2400
|
-
|
|
2401
|
-
return {
|
|
2402
|
-
id: channel.id,
|
|
2403
|
-
label: channel.label,
|
|
2404
|
-
...outcome
|
|
2405
|
-
};
|
|
2547
|
+
return balanceRow(channel, key);
|
|
2406
2548
|
})) });
|
|
2407
2549
|
}
|
|
2408
2550
|
}
|
package/lib/presets.d.ts
CHANGED
|
@@ -32,23 +32,26 @@ export type ChannelModality = 'text' | 'image';
|
|
|
32
32
|
* How to read one endpoint's remaining balance.
|
|
33
33
|
*
|
|
34
34
|
* Owner request (2026-10-23): 「其它渠道的查看方式你需要找到并实现它们的」. There is no single
|
|
35
|
-
* answer — every provider invented its own surface, and some
|
|
36
|
-
* fact written down instead of guessed: a preset either names a known shape or
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* · `
|
|
41
|
-
* · `
|
|
35
|
+
* answer — every provider invented its own surface, and some report windows instead of money.
|
|
36
|
+
* This type is that fact written down instead of guessed: a preset either names a known shape or
|
|
37
|
+
* says it has none.
|
|
38
|
+
*
|
|
39
|
+
* Four shapes were verified against real endpoints (see `balance.ts` for the response mapping):
|
|
40
|
+
* · `deepseek` — DeepSeek's own `GET /user/balance`.
|
|
41
|
+
* · `ollama` — `GET /api/balance` (note: under `/api`, NOT the `/v1` used for inference).
|
|
42
|
+
* · `opencode-windows` — OpenCode Go's `GET /zen/go/v1/usage`: rolling/weekly/monthly WINDOWS.
|
|
43
|
+
* No currency amount — the credit wallet is not API-readable.
|
|
44
|
+
* · `openai-billing` — the OpenAI-legacy billing pair that new-api / one-api relays clone:
|
|
42
45
|
* `GET /v1/dashboard/billing/subscription` + `GET /v1/dashboard/billing/usage`.
|
|
43
46
|
*
|
|
44
|
-
* 🔴 **
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
47
|
+
* 🔴 **"This endpoint has no balance API" must be representable, and is**: a preset simply omits
|
|
48
|
+
* `balance`. Open Design is a measured case (every billing candidate answers 404 while
|
|
49
|
+
* `/v1/models` answers 401, i.e. inference only). Pretending otherwise would mean showing a
|
|
50
|
+
* number nobody measured.
|
|
48
51
|
*/
|
|
49
52
|
export interface BalanceProbe {
|
|
50
53
|
/** Which response shape to read. */
|
|
51
|
-
shape: 'deepseek' | 'ollama' | 'openai-billing';
|
|
54
|
+
shape: 'deepseek' | 'ollama' | 'opencode-windows' | 'openai-billing';
|
|
52
55
|
/**
|
|
53
56
|
* Overrides the base URL for the balance call.
|
|
54
57
|
*
|
|
@@ -95,7 +98,9 @@ export interface ChannelPreset {
|
|
|
95
98
|
*
|
|
96
99
|
* Absent means "this endpoint has no balance API we know of" — a real and common state, not an
|
|
97
100
|
* omission. The panel says so explicitly rather than leaving a blank cell that could be read
|
|
98
|
-
* as zero
|
|
101
|
+
* as zero, and then checks the endpoint is ALIVE with one minimal live request (owner directive
|
|
102
|
+
* 2026-10-23): "no amount is readable" and "the credit is spent" are different facts, and a
|
|
103
|
+
* successful call proves the second is not true. See `http.ts`'s `balanceRow`.
|
|
99
104
|
*/
|
|
100
105
|
balance?: BalanceProbe;
|
|
101
106
|
/** One-line note for the panel, when the relay needs explaining. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-deepseek",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "One DeepSeek V4.1 Flash provider for DeepSeek Harness: paste a key per channel, switch channels manually, token usage booked per real upstream.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|