@linxin666/dsh-usage 0.4.0 → 0.4.2
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.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +8 -9
- package/lib/client.js.map +1 -1
- package/lib/index.js +42 -9
- package/lib/types/core/adapters.d.ts +45 -0
- package/package.json +14 -14
- package/src/client/UsageSectionCard.tsx +8 -1
- package/src/client/index.ts +5 -2
- package/src/core/adapters.ts +56 -0
- package/src/host/usage-service.ts +19 -2
package/lib/index.js
CHANGED
|
@@ -45,7 +45,7 @@ function mountOnce(packageName, fn) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
//#endregion
|
|
48
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.
|
|
48
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
49
49
|
/**
|
|
50
50
|
* Duplicate-install-safe nominal primitive helpers.
|
|
51
51
|
*
|
|
@@ -68,7 +68,7 @@ function brandString(value) {
|
|
|
68
68
|
return value;
|
|
69
69
|
}
|
|
70
70
|
//#endregion
|
|
71
|
-
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.1.7-rc.
|
|
71
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-credentials@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4_@deepseek-ai+dsh-inva_98fa173416f048996704a605a96a2b0e/node_modules/@deepseek-ai/dsh-credentials/lib/index.js
|
|
72
72
|
/**
|
|
73
73
|
* Service Definition for the credential-reference capability seam (`ctx.credentials`). Settings and composition files carry
|
|
74
74
|
* *references* to secrets — environment-variable names — while providers own
|
|
@@ -235,16 +235,11 @@ function usedPercent(used, limit) {
|
|
|
235
235
|
function bearer(apiKey) {
|
|
236
236
|
return { authorization: `Bearer ${apiKey}` };
|
|
237
237
|
}
|
|
238
|
-
/**
|
|
239
|
-
* The official pay-as-you-go balance. `deepseek` is the configurable-catalog
|
|
240
|
-
* route key; `deepseek-official` is the live provider route the llm-deepseek
|
|
241
|
-
* adapter registers (sessions and agent-default-model carry it), so both ids
|
|
242
|
-
* must resolve here or the current provider would never be probed.
|
|
243
|
-
*/
|
|
244
238
|
const DEEPSEEK = {
|
|
245
239
|
ids: ["deepseek", "deepseek-official"],
|
|
246
240
|
displayName: "DeepSeek",
|
|
247
241
|
balance: {
|
|
242
|
+
origin: "https://api.deepseek.com",
|
|
248
243
|
build: ({ apiKey }) => ({
|
|
249
244
|
url: "https://api.deepseek.com/user/balance",
|
|
250
245
|
headers: bearer(apiKey)
|
|
@@ -624,6 +619,40 @@ const PROVIDER_ADAPTERS = [
|
|
|
624
619
|
function adapterFor(provider) {
|
|
625
620
|
return PROVIDER_ADAPTERS.find((adapter) => adapter.ids.includes(provider));
|
|
626
621
|
}
|
|
622
|
+
/** Origin of a configured base URL, or undefined when it cannot be parsed. */
|
|
623
|
+
function originOf(baseURL) {
|
|
624
|
+
if (baseURL === void 0 || baseURL.trim() === "") return void 0;
|
|
625
|
+
try {
|
|
626
|
+
return new URL(baseURL).origin;
|
|
627
|
+
} catch {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Whether an adapter's balance endpoint reports THIS route's account.
|
|
633
|
+
*
|
|
634
|
+
* Some adapter families share one route key across providers that merely
|
|
635
|
+
* speak the same API: `deepseek` is both the official DeepSeek catalog entry
|
|
636
|
+
* and a common id for an OpenAI-compatible reseller (Alibaba Bailian, ...)
|
|
637
|
+
* pointed at another host by the profile's `baseURL`. The balance endpoint is
|
|
638
|
+
* per ORIGIN, not per route key, so probing the official endpoint for a
|
|
639
|
+
* reseller profile reports the official account's money under the reseller's
|
|
640
|
+
* name (issue #1688). An adapter published with a fixed `origin` therefore
|
|
641
|
+
* only applies to a route whose configured base URL is that origin, or to a
|
|
642
|
+
* route that pins no base URL at all (the catalog alias of the official
|
|
643
|
+
* route, which resolves the family's own credential).
|
|
644
|
+
*
|
|
645
|
+
* @param adapter - the matched adapter.
|
|
646
|
+
* @param baseURL - the route's configured base URL (pi-ai profile), if any.
|
|
647
|
+
* @returns true when the adapter may probe its balance endpoint for the route.
|
|
648
|
+
*/
|
|
649
|
+
function balanceAppliesToRoute(adapter, baseURL) {
|
|
650
|
+
const expected = adapter.balance?.origin;
|
|
651
|
+
if (expected === void 0) return true;
|
|
652
|
+
const configured = originOf(baseURL);
|
|
653
|
+
if (configured === void 0) return true;
|
|
654
|
+
return configured === expected;
|
|
655
|
+
}
|
|
627
656
|
/**
|
|
628
657
|
* Whether a provider route belongs to the official DeepSeek family: the only
|
|
629
658
|
* family with a spend price book and a settings-section-owned env credential
|
|
@@ -1086,7 +1115,8 @@ var UsageService = class {
|
|
|
1086
1115
|
displayName: route.displayName || adapter?.displayName || route.id,
|
|
1087
1116
|
credential: snapshot?.credential ?? "none",
|
|
1088
1117
|
supported: adapter !== void 0 && (adapter.balance !== void 0 || adapter.plan !== void 0),
|
|
1089
|
-
...adapter?.balance !== void 0 ? { balanceSupported: true } : {},
|
|
1118
|
+
...adapter?.balance !== void 0 && balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL) ? { balanceSupported: true } : {},
|
|
1119
|
+
...adapter?.balance !== void 0 && !balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL) ? { balanceSupported: false } : {},
|
|
1090
1120
|
...adapter?.plan !== void 0 ? { planSupported: true } : {},
|
|
1091
1121
|
...snapshot?.balance !== void 0 ? { balance: snapshot.balance } : {},
|
|
1092
1122
|
...snapshot?.plan !== void 0 ? { plan: snapshot.plan } : {},
|
|
@@ -1415,6 +1445,7 @@ var UsageService = class {
|
|
|
1415
1445
|
async probeRoute(route, adapter) {
|
|
1416
1446
|
const credential = await this.resolveCredential(route.id);
|
|
1417
1447
|
const previous = this.snapshots.get(route.id);
|
|
1448
|
+
const balanceApplies = balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL);
|
|
1418
1449
|
if (credential.key === void 0) {
|
|
1419
1450
|
this.snapshots.set(route.id, {
|
|
1420
1451
|
provider: route.id,
|
|
@@ -1430,11 +1461,13 @@ var UsageService = class {
|
|
|
1430
1461
|
displayName: route.displayName || adapter.displayName,
|
|
1431
1462
|
credential: credential.kind,
|
|
1432
1463
|
supported: true,
|
|
1464
|
+
...adapter.balance !== void 0 && !balanceApplies ? { balanceSupported: false } : {},
|
|
1433
1465
|
updatedAt: Date.now()
|
|
1434
1466
|
};
|
|
1435
1467
|
const runProbe = async (kind) => {
|
|
1436
1468
|
const half = adapter[kind];
|
|
1437
1469
|
if (half === void 0) return void 0;
|
|
1470
|
+
if (kind === "balance" && !balanceApplies) return void 0;
|
|
1438
1471
|
try {
|
|
1439
1472
|
const spec = half.build({
|
|
1440
1473
|
apiKey: credential.key,
|
|
@@ -19,6 +19,14 @@ export interface AdapterProbeContext {
|
|
|
19
19
|
apiKey: string;
|
|
20
20
|
/** Provider account id riding a dedicated header, when the credential carries one (Codex). */
|
|
21
21
|
accountId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The route's configured base URL (the pi-ai profile's baseURL), when it
|
|
24
|
+
* declares one. An adapter whose endpoint belongs to one provider origin
|
|
25
|
+
* uses it to refuse an account that merely shares the route key, so a
|
|
26
|
+
* differently-hosted model of the same family is never reported with the
|
|
27
|
+
* other account's number.
|
|
28
|
+
*/
|
|
29
|
+
baseURL?: string;
|
|
22
30
|
}
|
|
23
31
|
/** A parsed balance fact. */
|
|
24
32
|
export interface BalanceParse {
|
|
@@ -37,6 +45,14 @@ export interface ProviderAdapter {
|
|
|
37
45
|
/** Fallback display name when the LLM runtime has none. */
|
|
38
46
|
displayName: string;
|
|
39
47
|
balance?: {
|
|
48
|
+
/**
|
|
49
|
+
* Origin (scheme + host) whose account this balance endpoint reports.
|
|
50
|
+
* Omitted when the endpoint is per-route by construction (a profile
|
|
51
|
+
* supplies the host) or when the provider has only one origin. When set,
|
|
52
|
+
* a route whose configured baseURL resolves elsewhere is not this
|
|
53
|
+
* origin's account and must not be probed with this adapter.
|
|
54
|
+
*/
|
|
55
|
+
origin?: string;
|
|
40
56
|
build(context: AdapterProbeContext): ProbeSpec;
|
|
41
57
|
parse(status: number, body: unknown): BalanceParse | undefined;
|
|
42
58
|
};
|
|
@@ -45,6 +61,14 @@ export interface ProviderAdapter {
|
|
|
45
61
|
parse(status: number, body: unknown): PlanParse | undefined;
|
|
46
62
|
};
|
|
47
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The official pay-as-you-go balance. `deepseek` is the configurable-catalog
|
|
66
|
+
* route key; `deepseek-official` is the live provider route the llm-deepseek
|
|
67
|
+
* adapter registers (sessions and agent-default-model carry it), so both ids
|
|
68
|
+
* must resolve here or the current provider would never be probed.
|
|
69
|
+
*/
|
|
70
|
+
/** The official DeepSeek API origin: the only account its balance endpoint reports. */
|
|
71
|
+
export declare const DEEPSEEK_API_ORIGIN = "https://api.deepseek.com";
|
|
48
72
|
/**
|
|
49
73
|
* The adapter registry, in no particular order. Route keys come from the
|
|
50
74
|
* pi-ai provider catalog plus the routes this deployment observed in user
|
|
@@ -53,6 +77,27 @@ export interface ProviderAdapter {
|
|
|
53
77
|
export declare const PROVIDER_ADAPTERS: readonly ProviderAdapter[];
|
|
54
78
|
/** Find the adapter serving a provider route key, if any. */
|
|
55
79
|
export declare function adapterFor(provider: string): ProviderAdapter | undefined;
|
|
80
|
+
/** Origin of a configured base URL, or undefined when it cannot be parsed. */
|
|
81
|
+
export declare function originOf(baseURL: string | undefined): string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Whether an adapter's balance endpoint reports THIS route's account.
|
|
84
|
+
*
|
|
85
|
+
* Some adapter families share one route key across providers that merely
|
|
86
|
+
* speak the same API: `deepseek` is both the official DeepSeek catalog entry
|
|
87
|
+
* and a common id for an OpenAI-compatible reseller (Alibaba Bailian, ...)
|
|
88
|
+
* pointed at another host by the profile's `baseURL`. The balance endpoint is
|
|
89
|
+
* per ORIGIN, not per route key, so probing the official endpoint for a
|
|
90
|
+
* reseller profile reports the official account's money under the reseller's
|
|
91
|
+
* name (issue #1688). An adapter published with a fixed `origin` therefore
|
|
92
|
+
* only applies to a route whose configured base URL is that origin, or to a
|
|
93
|
+
* route that pins no base URL at all (the catalog alias of the official
|
|
94
|
+
* route, which resolves the family's own credential).
|
|
95
|
+
*
|
|
96
|
+
* @param adapter - the matched adapter.
|
|
97
|
+
* @param baseURL - the route's configured base URL (pi-ai profile), if any.
|
|
98
|
+
* @returns true when the adapter may probe its balance endpoint for the route.
|
|
99
|
+
*/
|
|
100
|
+
export declare function balanceAppliesToRoute(adapter: ProviderAdapter, baseURL: string | undefined): boolean;
|
|
56
101
|
/**
|
|
57
102
|
* Whether a provider route belongs to the official DeepSeek family: the only
|
|
58
103
|
* family with a spend price book and a settings-section-owned env credential
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-usage",
|
|
3
3
|
"description": "Usage statistics plugin for the dsh web GUI: per-provider balance and coding-plan quota detection plus a live token usage ledger, with a compact price glance card above the sidebar's Settings row",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "Apache-2.0",
|
|
34
34
|
"dsh": {
|
|
35
35
|
"engines": {
|
|
36
|
-
"dsh": ">=0.1.7-rc.
|
|
36
|
+
"dsh": ">=0.1.7-rc.2"
|
|
37
37
|
},
|
|
38
38
|
"bundle": {
|
|
39
39
|
"patch": "./cordis.patch.yml"
|
|
@@ -53,22 +53,22 @@
|
|
|
53
53
|
"schemastery": "^3.18.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/dsh": ">=0.1.7-rc.
|
|
56
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.2",
|
|
57
57
|
"react": "^18.2.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
61
|
-
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.
|
|
62
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.
|
|
63
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.
|
|
64
|
-
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.
|
|
68
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "^0.1.7-rc.
|
|
70
|
-
"@deepseek-ai/dsh-session": "^0.1.7-rc.
|
|
71
|
-
"@deepseek-ai/dsh-settings": "^0.1.7-rc.
|
|
61
|
+
"@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
|
|
72
72
|
"@testing-library/dom": "^10.4.1",
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/node": "^22.20.0",
|
|
@@ -128,6 +128,10 @@ function balanceLine(provider: ProviderSnapshotView): ReactNode {
|
|
|
128
128
|
}
|
|
129
129
|
if (provider.credential === 'oauth') return <span className={styles.muted}>{t('usage.oauth')}</span>
|
|
130
130
|
if (provider.credential === 'none') return <span className={styles.muted}>{t('usage.balance.noCredential')}</span>
|
|
131
|
+
// balanceSupported === false is the origin gate: the adapter has a balance
|
|
132
|
+
// endpoint but it belongs to another provider's account (issue #1688), so
|
|
133
|
+
// the row says so instead of silently vanishing from the card.
|
|
134
|
+
if (provider.balanceSupported === false) return <span className={styles.muted}>{t('usage.balance.unsupported')}</span>
|
|
131
135
|
if (!provider.supported) return <span className={styles.muted}>{t('usage.balance.unsupported')}</span>
|
|
132
136
|
return null
|
|
133
137
|
}
|
|
@@ -298,7 +302,10 @@ export function UsageSectionCard(props: UsageSectionProps): ReactNode {
|
|
|
298
302
|
<span className={styles.cardTitle}>{t('usage.balance')}</span>
|
|
299
303
|
{(() => {
|
|
300
304
|
const configured = snapshot.providers.filter(isConfigured)
|
|
301
|
-
|
|
305
|
+
// Include rows whose balance is deliberately not applicable
|
|
306
|
+
// (another origin's endpoint): the row itself explains why, which
|
|
307
|
+
// a single generic line cannot (#1688).
|
|
308
|
+
const rows = configured.filter((provider) => provider.balanceSupported === true || provider.balanceSupported === false || (provider.balanceSupported === undefined && provider.supported))
|
|
302
309
|
if (rows.length === 0) {
|
|
303
310
|
return <span className={styles.muted}>{configured.length === 0 ? t('usage.balance.noneConfigured') : t('usage.balance.unsupported')}</span>
|
|
304
311
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -42,8 +42,11 @@ async function usageFetch<T>(path: string, method: 'GET' | 'POST'): Promise<T> {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const usageApi: UsageHttpApi = {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// DOCUMENT-RELATIVE routes (issue #1707): the GUI is served with
|
|
46
|
+
// `<base href="./">`, so a sub-path deployment resolves these against its
|
|
47
|
+
// entry directory instead of escaping to the origin root.
|
|
48
|
+
overview: () => usageFetch('api/dsh-usage/overview', 'GET'),
|
|
49
|
+
refresh: () => usageFetch('api/dsh-usage/refresh', 'POST'),
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/** Settings namespace the section edits (dsh-web-settings maps it onto this row's profile entry id). */
|
package/src/core/adapters.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface AdapterProbeContext {
|
|
|
22
22
|
apiKey: string
|
|
23
23
|
/** Provider account id riding a dedicated header, when the credential carries one (Codex). */
|
|
24
24
|
accountId?: string
|
|
25
|
+
/**
|
|
26
|
+
* The route's configured base URL (the pi-ai profile's baseURL), when it
|
|
27
|
+
* declares one. An adapter whose endpoint belongs to one provider origin
|
|
28
|
+
* uses it to refuse an account that merely shares the route key, so a
|
|
29
|
+
* differently-hosted model of the same family is never reported with the
|
|
30
|
+
* other account's number.
|
|
31
|
+
*/
|
|
32
|
+
baseURL?: string
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** A parsed balance fact. */
|
|
@@ -43,6 +51,14 @@ export interface ProviderAdapter {
|
|
|
43
51
|
/** Fallback display name when the LLM runtime has none. */
|
|
44
52
|
displayName: string
|
|
45
53
|
balance?: {
|
|
54
|
+
/**
|
|
55
|
+
* Origin (scheme + host) whose account this balance endpoint reports.
|
|
56
|
+
* Omitted when the endpoint is per-route by construction (a profile
|
|
57
|
+
* supplies the host) or when the provider has only one origin. When set,
|
|
58
|
+
* a route whose configured baseURL resolves elsewhere is not this
|
|
59
|
+
* origin's account and must not be probed with this adapter.
|
|
60
|
+
*/
|
|
61
|
+
origin?: string
|
|
46
62
|
build(context: AdapterProbeContext): ProbeSpec
|
|
47
63
|
parse(status: number, body: unknown): BalanceParse | undefined
|
|
48
64
|
}
|
|
@@ -113,10 +129,14 @@ function bearer(apiKey: string): Record<string, string> {
|
|
|
113
129
|
* adapter registers (sessions and agent-default-model carry it), so both ids
|
|
114
130
|
* must resolve here or the current provider would never be probed.
|
|
115
131
|
*/
|
|
132
|
+
/** The official DeepSeek API origin: the only account its balance endpoint reports. */
|
|
133
|
+
export const DEEPSEEK_API_ORIGIN = 'https://api.deepseek.com'
|
|
134
|
+
|
|
116
135
|
const DEEPSEEK: ProviderAdapter = {
|
|
117
136
|
ids: ['deepseek', 'deepseek-official'],
|
|
118
137
|
displayName: 'DeepSeek',
|
|
119
138
|
balance: {
|
|
139
|
+
origin: DEEPSEEK_API_ORIGIN,
|
|
120
140
|
build: ({ apiKey }) => ({ url: 'https://api.deepseek.com/user/balance', headers: bearer(apiKey) }),
|
|
121
141
|
parse: (status, body) => {
|
|
122
142
|
if (status !== 200 || typeof body !== 'object' || body === null) return undefined
|
|
@@ -459,6 +479,42 @@ export function adapterFor(provider: string): ProviderAdapter | undefined {
|
|
|
459
479
|
return PROVIDER_ADAPTERS.find((adapter) => adapter.ids.includes(provider))
|
|
460
480
|
}
|
|
461
481
|
|
|
482
|
+
/** Origin of a configured base URL, or undefined when it cannot be parsed. */
|
|
483
|
+
export function originOf(baseURL: string | undefined): string | undefined {
|
|
484
|
+
if (baseURL === undefined || baseURL.trim() === '') return undefined
|
|
485
|
+
try {
|
|
486
|
+
return new URL(baseURL).origin
|
|
487
|
+
} catch {
|
|
488
|
+
return undefined
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Whether an adapter's balance endpoint reports THIS route's account.
|
|
494
|
+
*
|
|
495
|
+
* Some adapter families share one route key across providers that merely
|
|
496
|
+
* speak the same API: `deepseek` is both the official DeepSeek catalog entry
|
|
497
|
+
* and a common id for an OpenAI-compatible reseller (Alibaba Bailian, ...)
|
|
498
|
+
* pointed at another host by the profile's `baseURL`. The balance endpoint is
|
|
499
|
+
* per ORIGIN, not per route key, so probing the official endpoint for a
|
|
500
|
+
* reseller profile reports the official account's money under the reseller's
|
|
501
|
+
* name (issue #1688). An adapter published with a fixed `origin` therefore
|
|
502
|
+
* only applies to a route whose configured base URL is that origin, or to a
|
|
503
|
+
* route that pins no base URL at all (the catalog alias of the official
|
|
504
|
+
* route, which resolves the family's own credential).
|
|
505
|
+
*
|
|
506
|
+
* @param adapter - the matched adapter.
|
|
507
|
+
* @param baseURL - the route's configured base URL (pi-ai profile), if any.
|
|
508
|
+
* @returns true when the adapter may probe its balance endpoint for the route.
|
|
509
|
+
*/
|
|
510
|
+
export function balanceAppliesToRoute(adapter: ProviderAdapter, baseURL: string | undefined): boolean {
|
|
511
|
+
const expected = adapter.balance?.origin
|
|
512
|
+
if (expected === undefined) return true
|
|
513
|
+
const configured = originOf(baseURL)
|
|
514
|
+
if (configured === undefined) return true
|
|
515
|
+
return configured === expected
|
|
516
|
+
}
|
|
517
|
+
|
|
462
518
|
/**
|
|
463
519
|
* Whether a provider route belongs to the official DeepSeek family: the only
|
|
464
520
|
* family with a spend price book and a settings-section-owned env credential
|
|
@@ -15,7 +15,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
|
15
15
|
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
16
16
|
import { dshHome } from '../dsh-home.ts'
|
|
17
17
|
import { withIdentityEncoding } from './http.ts'
|
|
18
|
-
import { adapterFor, isDeepSeekProviderRoute, providerErrorMessage } from '../core/adapters.ts'
|
|
18
|
+
import { adapterFor, balanceAppliesToRoute, isDeepSeekProviderRoute, providerErrorMessage } from '../core/adapters.ts'
|
|
19
19
|
import type { BalanceParse, PlanParse } from '../core/adapters.ts'
|
|
20
20
|
import { foldAliasRoutes } from '../core/provider-routes.ts'
|
|
21
21
|
import { deepseekModelSpend } from '../core/pricing.ts'
|
|
@@ -202,7 +202,16 @@ export class UsageService {
|
|
|
202
202
|
displayName: route.displayName || adapter?.displayName || route.id,
|
|
203
203
|
credential: snapshot?.credential ?? 'none',
|
|
204
204
|
supported: adapter !== undefined && (adapter.balance !== undefined || adapter.plan !== undefined),
|
|
205
|
-
|
|
205
|
+
// A balance endpoint that belongs to another origin does not report
|
|
206
|
+
// THIS route's account: it is supported by the adapter but not
|
|
207
|
+
// applicable here, and the UI renders it as unsupported rather than
|
|
208
|
+
// showing the other account's number (issue #1688).
|
|
209
|
+
...(adapter?.balance !== undefined && balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
|
|
210
|
+
? { balanceSupported: true }
|
|
211
|
+
: {}),
|
|
212
|
+
...(adapter?.balance !== undefined && !balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
|
|
213
|
+
? { balanceSupported: false }
|
|
214
|
+
: {}),
|
|
206
215
|
...(adapter?.plan !== undefined ? { planSupported: true } : {}),
|
|
207
216
|
...(snapshot?.balance !== undefined ? { balance: snapshot.balance } : {}),
|
|
208
217
|
...(snapshot?.plan !== undefined ? { plan: snapshot.plan } : {}),
|
|
@@ -573,6 +582,12 @@ export class UsageService {
|
|
|
573
582
|
private async probeRoute(route: ProviderRoute, adapter: NonNullable<ReturnType<typeof adapterFor>>): Promise<void> {
|
|
574
583
|
const credential = await this.resolveCredential(route.id)
|
|
575
584
|
const previous = this.snapshots.get(route.id)
|
|
585
|
+
// The balance endpoint is per ORIGIN, not per route key: a profile that
|
|
586
|
+
// points a shared route id (`deepseek`) at another host is a different
|
|
587
|
+
// account, and probing the official endpoint would print this account's
|
|
588
|
+
// money under that route's name (issue #1688). Such a route reports the
|
|
589
|
+
// balance as unsupported instead.
|
|
590
|
+
const balanceApplies = balanceAppliesToRoute(adapter, this.piAiProfile(route.id)?.baseURL)
|
|
576
591
|
if (credential.key === undefined) {
|
|
577
592
|
// Nothing to probe with (oauth grant, no credential): retained facts
|
|
578
593
|
// would never refresh again, so the row resets to its credential
|
|
@@ -591,11 +606,13 @@ export class UsageService {
|
|
|
591
606
|
displayName: route.displayName || adapter.displayName,
|
|
592
607
|
credential: credential.kind,
|
|
593
608
|
supported: true,
|
|
609
|
+
...(adapter.balance !== undefined && !balanceApplies ? { balanceSupported: false } : {}),
|
|
594
610
|
updatedAt: Date.now(),
|
|
595
611
|
}
|
|
596
612
|
const runProbe = async (kind: 'balance' | 'plan'): Promise<BalanceView | PlanView | undefined> => {
|
|
597
613
|
const half = adapter[kind]
|
|
598
614
|
if (half === undefined) return undefined
|
|
615
|
+
if (kind === 'balance' && !balanceApplies) return undefined
|
|
599
616
|
try {
|
|
600
617
|
const spec = half.build({ apiKey: credential.key as string, ...(credential.accountId !== undefined ? { accountId: credential.accountId } : {}) })
|
|
601
618
|
const response = await fetch(spec.url, withIdentityEncoding({
|