@linxin666/dsh-usage 0.4.2 → 0.4.4
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 +104 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +252 -45
- package/lib/types/client/body-mutations.d.ts +4 -4
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/host/usage-service.d.ts +4 -0
- package/lib/types/index.d.ts +61 -10
- package/lib/types/mount-once.d.ts +38 -7
- package/package.json +15 -15
- package/src/client/body-mutations.ts +4 -4
- package/src/client/index.ts +17 -7
- package/src/client/settings-entry-form.ts +142 -0
- package/src/host/usage-service.ts +129 -8
- package/src/index.ts +138 -43
- package/src/mount-once.ts +107 -13
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
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.4",
|
|
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.
|
|
36
|
+
"dsh": ">=0.2.0-rc.1"
|
|
37
37
|
},
|
|
38
38
|
"bundle": {
|
|
39
39
|
"patch": "./cordis.patch.yml"
|
|
@@ -50,25 +50,25 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"schemastery": "^3.18.
|
|
53
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/dsh": ">=0.
|
|
56
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.1",
|
|
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.
|
|
62
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
64
|
-
"@deepseek-ai/dsh-client-store": "^0.
|
|
65
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
68
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "^0.
|
|
70
|
-
"@deepseek-ai/dsh-session": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
61
|
+
"@deepseek-ai/dsh-credentials": "^0.2.0-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-session": "^0.2.0-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
|
|
72
72
|
"@testing-library/dom": "^10.4.1",
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/node": "^22.20.0",
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* plugin bundle.
|
|
5
5
|
*
|
|
6
6
|
* Family plugins that inject at the DOM level must notice when the shell
|
|
7
|
-
* re-renders around their injected node: the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* re-renders around their injected node: the aggregate shell's column shims
|
|
8
|
+
* and any plugin surface still extending the shell from outside the official
|
|
9
|
+
* slot seats. Each consumer used to install its OWN `MutationObserver` on
|
|
10
|
+
* `document.body` with
|
|
11
11
|
* `{ childList: true, subtree: true }`, so a page carrying N family plugins
|
|
12
12
|
* paid N native observers and N callback invocations for EVERY mutation batch
|
|
13
13
|
* the app produced — chat token streaming alone produces many per second, and
|
package/src/client/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { createUsageStore, type UsageStoreInstance } from './usage-store.ts'
|
|
|
21
21
|
import { UsageSectionCard, type UsageSectionFace, type UsageSettings } from './UsageSectionCard.tsx'
|
|
22
22
|
import { mountUsageFootCard, openUsageSettings } from './foot-card-mount.tsx'
|
|
23
23
|
import { NS, en, zh, t } from './locales.ts'
|
|
24
|
+
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
24
25
|
import type { UsageOverviewView } from '../core/types.ts'
|
|
25
26
|
|
|
26
27
|
/** The host usage API as the browser sees it (same-origin JSON endpoints). */
|
|
@@ -49,9 +50,18 @@ const usageApi: UsageHttpApi = {
|
|
|
49
50
|
refresh: () => usageFetch('api/dsh-usage/refresh', 'POST'),
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Settings namespace the section edits: the family identity of this plugin's
|
|
55
|
+
* own settings form, and the row id a standalone bundle install carries.
|
|
56
|
+
*/
|
|
53
57
|
const USAGE_SETTINGS_NS = 'dsh-usage'
|
|
54
58
|
|
|
59
|
+
/** Profile entry id the family aggregate's generated row carries. */
|
|
60
|
+
const AGGREGATE_ENTRY_ID = 'web-ui-usage'
|
|
61
|
+
|
|
62
|
+
/** Profile entry ids this package's patch rows carry, most likely first. */
|
|
63
|
+
const USAGE_ENTRY_IDS: readonly string[] = [AGGREGATE_ENTRY_ID, 'usage', USAGE_SETTINGS_NS]
|
|
64
|
+
|
|
55
65
|
/** First-level nav position: directly below the Workshop section (order 150). */
|
|
56
66
|
const SECTION_ORDER = 151
|
|
57
67
|
|
|
@@ -101,15 +111,15 @@ export function apply(ctx: ClientContext): void {
|
|
|
101
111
|
}, 'dsh-usage: dictionaries')
|
|
102
112
|
|
|
103
113
|
// The family binder is what maps this namespace onto the row's profile entry
|
|
104
|
-
// id and hands back the native form. Without the group plugin the
|
|
105
|
-
//
|
|
106
|
-
// the
|
|
107
|
-
//
|
|
108
|
-
//
|
|
114
|
+
// id and hands back the native form. Without the group plugin the shared forms
|
|
115
|
+
// service is addressed on the entry id the describe mirror justifies, rebound
|
|
116
|
+
// as soon as the mirror answers: the namespace itself is only an entry id on a
|
|
117
|
+
// standalone install, so binding it on the aggregate left the form unavailable
|
|
118
|
+
// and the row's controls disabled.
|
|
109
119
|
const binder = ctx.get('webUiSettings')
|
|
110
120
|
const settingsForm = binder !== undefined
|
|
111
121
|
? binder.bind<UsageSettings>({ namespace: USAGE_SETTINGS_NS })
|
|
112
|
-
:
|
|
122
|
+
: createServedEntryForm<UsageSettings>({ forms: ctx.configForms, entryIds: USAGE_ENTRY_IDS })
|
|
113
123
|
|
|
114
124
|
// One store instance per apply body; the section mounts and unmounts with
|
|
115
125
|
// the settings page, and the store survives between visits so the last
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/settings-entry-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/settings-entry-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
3
|
+
/**
|
|
4
|
+
* Family settings form bound to the profile entry id a Host actually serves.
|
|
5
|
+
*
|
|
6
|
+
* Under the 0.1.7 settings model a form is addressed by PROFILE ENTRY ID, while
|
|
7
|
+
* a family card knows only its own settings namespace. The family binder
|
|
8
|
+
* (`ctx.get('webUiSettings')`, published by dsh-web-settings) resolves one onto
|
|
9
|
+
* the other and is the preferred transport; this module is the fallback for a
|
|
10
|
+
* page that serves no binder — the group plugin is not installed, or its client
|
|
11
|
+
* half has not applied yet (the aggregate mounts its children in order, and a
|
|
12
|
+
* child still waiting on its own injects publishes nothing).
|
|
13
|
+
*
|
|
14
|
+
* The shared describe mirror is the only local evidence of which row id this
|
|
15
|
+
* profile serves, but it answers asynchronously: at plugin activation it usually
|
|
16
|
+
* holds nothing yet. A one-shot guess therefore cannot be right for every
|
|
17
|
+
* deployment, and the form is bound once per session:
|
|
18
|
+
*
|
|
19
|
+
* - binding the bare namespace made every save answer `No configurable plugin
|
|
20
|
+
* entry "<namespace>"` on an aggregate install (the namespace is not an entry
|
|
21
|
+
* id there), which the card reported as "the deployment did not accept these
|
|
22
|
+
* values";
|
|
23
|
+
* - binding the aggregate row id would break a standalone install, whose row id
|
|
24
|
+
* is the namespace or the package's own `ui-*` row.
|
|
25
|
+
*
|
|
26
|
+
* So the fallback starts on the most likely row and REBINDS the moment the
|
|
27
|
+
* mirror names one of this package's rows. Until the mirror answers, the form
|
|
28
|
+
* reports the binding it holds (usually `loading`), which the card renders as an
|
|
29
|
+
* unavailable form rather than a doomed one.
|
|
30
|
+
*/
|
|
31
|
+
import type { ConfigForm, ConfigFormSnapshot, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
32
|
+
|
|
33
|
+
/** Options of {@link createServedEntryForm}. */
|
|
34
|
+
export interface ServedEntryFormOptions<T> {
|
|
35
|
+
/** The shared configuration forms service (`ctx.configForms`). */
|
|
36
|
+
forms: ConfigForms
|
|
37
|
+
/**
|
|
38
|
+
* Profile entry ids this package's rows carry, most likely first: the
|
|
39
|
+
* aggregate's generated row, the package's own standalone row, then the bare
|
|
40
|
+
* namespace. The first is the fallback while the mirror has no answer; the
|
|
41
|
+
* last is the answer when the mirror serves none of them.
|
|
42
|
+
*/
|
|
43
|
+
entryIds: readonly string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The snapshot a form reports before the Host has answered with this entry. */
|
|
47
|
+
function pendingSnapshot<T>(): ConfigFormSnapshot<T> {
|
|
48
|
+
return {
|
|
49
|
+
status: 'loading',
|
|
50
|
+
value: undefined,
|
|
51
|
+
base: undefined,
|
|
52
|
+
user: undefined,
|
|
53
|
+
revision: undefined,
|
|
54
|
+
writable: false,
|
|
55
|
+
mode: 'host',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a settings form bound to the profile entry id this Host serves, and
|
|
61
|
+
* rebound whenever the shared describe mirror names a different one of this
|
|
62
|
+
* package's rows.
|
|
63
|
+
* @param options - the shared forms service and this package's candidate row ids.
|
|
64
|
+
* @returns a form delegating to the currently resolved entry's own form.
|
|
65
|
+
*/
|
|
66
|
+
export function createServedEntryForm<T>(options: ServedEntryFormOptions<T>): ConfigForm<T> {
|
|
67
|
+
const { forms, entryIds } = options
|
|
68
|
+
const fallbackId = entryIds[0]
|
|
69
|
+
const namespaceId = entryIds[entryIds.length - 1]
|
|
70
|
+
const listeners = new Set<() => void>()
|
|
71
|
+
let boundId: string | undefined
|
|
72
|
+
let bound: ConfigForm<T> | undefined
|
|
73
|
+
let offBound: (() => void) | undefined
|
|
74
|
+
let snapshot: ConfigFormSnapshot<T> = pendingSnapshot<T>()
|
|
75
|
+
|
|
76
|
+
/** Republish: the delegated snapshot when one is bound, the pending one otherwise. */
|
|
77
|
+
const publish = (): void => {
|
|
78
|
+
if (bound !== undefined) snapshot = bound.getSnapshot()
|
|
79
|
+
for (const listener of [...listeners]) listener()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The entry id the mirror currently justifies. An unanswered or empty mirror
|
|
84
|
+
* is not evidence of absence, so it keeps the first candidate; a mirror that
|
|
85
|
+
* answers without any of this package's rows leaves the namespace itself.
|
|
86
|
+
* @returns the entry id to bind.
|
|
87
|
+
*/
|
|
88
|
+
const resolve = (): string => {
|
|
89
|
+
let served: readonly string[] | undefined
|
|
90
|
+
try {
|
|
91
|
+
served = forms.describe().getSnapshot().view?.namespaces.map(row => row.ns)
|
|
92
|
+
} catch {
|
|
93
|
+
// A mirror that refuses the read is unanswered too, not absent.
|
|
94
|
+
served = undefined
|
|
95
|
+
}
|
|
96
|
+
if (served === undefined || served.length === 0) return fallbackId
|
|
97
|
+
return entryIds.find(id => served.includes(id)) ?? namespaceId
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Bind (or rebind) the resolved entry's form; a no-op while it is unchanged. */
|
|
101
|
+
const bind = (): void => {
|
|
102
|
+
const target = resolve()
|
|
103
|
+
if (target === boundId) return
|
|
104
|
+
offBound?.()
|
|
105
|
+
offBound = undefined
|
|
106
|
+
let form: ConfigForm<T>
|
|
107
|
+
try {
|
|
108
|
+
form = forms.get<T>(target)
|
|
109
|
+
} catch {
|
|
110
|
+
// A service that refuses the id stays unbound; the next mirror answer
|
|
111
|
+
// retries rather than pinning a wrong entry for the session.
|
|
112
|
+
boundId = undefined
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
boundId = target
|
|
116
|
+
bound = form
|
|
117
|
+
offBound = form.subscribe(() => { publish() })
|
|
118
|
+
publish()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The mirror subscription lives for the page: the card owns its form for the
|
|
122
|
+
// session, and the ConfigForm contract carries no disposer to hang it on. The
|
|
123
|
+
// callback is cheap (a resolve plus a no-op while the id is unchanged). A
|
|
124
|
+
// mirror without a subscription seat keeps the initial binding.
|
|
125
|
+
try {
|
|
126
|
+
forms.describe().subscribe(() => { bind() })
|
|
127
|
+
} catch {
|
|
128
|
+
// A describe face that refuses the subscription: the initial binding stands.
|
|
129
|
+
}
|
|
130
|
+
bind()
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getSnapshot: () => snapshot,
|
|
134
|
+
subscribe: (listener) => {
|
|
135
|
+
listeners.add(listener)
|
|
136
|
+
return () => { listeners.delete(listener) }
|
|
137
|
+
},
|
|
138
|
+
set: (field, value) => bound?.set(field, value) ?? Promise.resolve(false),
|
|
139
|
+
unset: (field) => bound?.unset(field) ?? Promise.resolve(false),
|
|
140
|
+
mutate: (ops, expectedRevision) => bound?.mutate(ops, expectedRevision) ?? Promise.resolve(false),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -103,19 +103,129 @@ async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
function unwrapVolatiles(value: unknown): unknown {
|
|
107
|
+
if (value === null || value === undefined) return value
|
|
108
|
+
if (typeof value === 'object') {
|
|
109
|
+
if (typeof (value as { get?: unknown }).get === 'function') {
|
|
110
|
+
return unwrapVolatiles((value as { get(): unknown }).get())
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
return value.map(unwrapVolatiles)
|
|
114
|
+
}
|
|
115
|
+
const result: Record<string, unknown> = {}
|
|
116
|
+
for (const [k, v] of Object.entries(value)) {
|
|
117
|
+
result[k] = unwrapVolatiles(v)
|
|
118
|
+
}
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
125
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function deepMerge(base: unknown, patch: unknown): unknown {
|
|
129
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) return patch !== undefined ? patch : base
|
|
130
|
+
const result: Record<string, unknown> = { ...base }
|
|
131
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
132
|
+
if (value === undefined) continue
|
|
133
|
+
result[key] = isPlainObject(value) && isPlainObject(result[key]) ? deepMerge(result[key], value) : value
|
|
134
|
+
}
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
106
138
|
/**
|
|
107
139
|
* Read a foreign settings namespace's resolved value (the llm adapter
|
|
108
|
-
* profiles, the agent default model).
|
|
109
|
-
*
|
|
140
|
+
* profiles, the agent default model). Resolves across pre-0.1.7 settings.get(),
|
|
141
|
+
* 0.1.7 SettingsForms.describe(), live loader entries, and configEditor.
|
|
142
|
+
* Unregistered namespaces read as undefined; nothing here throws into the poll loop.
|
|
110
143
|
*/
|
|
111
144
|
function readNamespace(ctx: Context, ns: string): unknown {
|
|
112
145
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
146
|
+
// 1. Try legacy settings.get(ns) or 0.1.7 settings.describe()
|
|
147
|
+
const settings = service<{
|
|
148
|
+
get?(ns: unknown): unknown
|
|
149
|
+
describe?(options?: unknown): Array<{ ns: string; value: unknown }>
|
|
150
|
+
}>(ctx, 'settings')
|
|
151
|
+
if (settings !== undefined) {
|
|
152
|
+
if (typeof settings.get === 'function') {
|
|
153
|
+
const value = settings.get(ns)
|
|
154
|
+
if (value !== undefined) return unwrapVolatiles(value)
|
|
155
|
+
}
|
|
156
|
+
if (typeof settings.describe === 'function') {
|
|
157
|
+
const descriptors = settings.describe({ redactSecrets: false })
|
|
158
|
+
if (Array.isArray(descriptors)) {
|
|
159
|
+
const match = descriptors.find((d) =>
|
|
160
|
+
d.ns === ns
|
|
161
|
+
|| (ns === 'llm-pi-ai' && (d.ns === 'llm' || isPlainObject((d.value as { providers?: unknown })?.providers)))
|
|
162
|
+
|| (ns === 'llm-deepseek' && (d.ns === 'llm-deepseek' || d.ns === 'llm-deepseek-api-key'))
|
|
163
|
+
)
|
|
164
|
+
if (match !== undefined && match.value !== undefined) {
|
|
165
|
+
return unwrapVolatiles(match.value)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Try ctx.root.loader or ctx.loader entries
|
|
172
|
+
const loader = (ctx as unknown as {
|
|
173
|
+
root?: { loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> } }
|
|
174
|
+
loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> }
|
|
175
|
+
}).root?.loader
|
|
176
|
+
?? (ctx as unknown as {
|
|
177
|
+
loader?: { entries(): Iterable<{ options: { id: string; name?: string }; fiber?: { config?: unknown } }> }
|
|
178
|
+
}).loader
|
|
179
|
+
if (loader !== undefined && typeof loader.entries === 'function') {
|
|
180
|
+
for (const entry of loader.entries()) {
|
|
181
|
+
const id = entry.options?.id
|
|
182
|
+
const name = entry.options?.name
|
|
183
|
+
if (
|
|
184
|
+
id === ns
|
|
185
|
+
|| name === `@deepseek-ai/dsh-${ns}`
|
|
186
|
+
|| name?.endsWith(`/${ns}`)
|
|
187
|
+
|| (ns === 'llm-pi-ai' && (id === 'llm' || name === '@deepseek-ai/dsh-llm-pi-ai'))
|
|
188
|
+
|| (ns === 'llm-deepseek' && (id === 'llm-deepseek-api-key' || name === '@deepseek-ai/dsh-llm-deepseek-api-key'))
|
|
189
|
+
) {
|
|
190
|
+
if (entry.fiber?.config !== undefined) {
|
|
191
|
+
return unwrapVolatiles(entry.fiber.config)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 3. Try configEditor service
|
|
198
|
+
const editor = service<{
|
|
199
|
+
configuration?(): Array<{
|
|
200
|
+
entry: { options: { id: string; name?: string }; fiber?: { config?: unknown } }
|
|
201
|
+
inherited: Record<string, unknown>
|
|
202
|
+
override: Record<string, unknown>
|
|
203
|
+
}>
|
|
204
|
+
}>(ctx, 'configEditor')
|
|
205
|
+
if (editor !== undefined && typeof editor.configuration === 'function') {
|
|
206
|
+
const rows = editor.configuration()
|
|
207
|
+
const match = rows.find((r) => {
|
|
208
|
+
const id = r.entry.options.id
|
|
209
|
+
const name = r.entry.options.name
|
|
210
|
+
return (
|
|
211
|
+
id === ns
|
|
212
|
+
|| name === `@deepseek-ai/dsh-${ns}`
|
|
213
|
+
|| name?.endsWith(`/${ns}`)
|
|
214
|
+
|| (ns === 'llm-pi-ai' && (id === 'llm' || name === '@deepseek-ai/dsh-llm-pi-ai'))
|
|
215
|
+
|| (ns === 'llm-deepseek' && (id === 'llm-deepseek-api-key' || name === '@deepseek-ai/dsh-llm-deepseek-api-key'))
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
if (match !== undefined) {
|
|
219
|
+
if (match.entry.fiber?.config !== undefined) {
|
|
220
|
+
return unwrapVolatiles(match.entry.fiber.config)
|
|
221
|
+
}
|
|
222
|
+
return unwrapVolatiles(deepMerge(match.inherited, match.override))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
116
225
|
} catch {
|
|
117
226
|
return undefined
|
|
118
227
|
}
|
|
228
|
+
return undefined
|
|
119
229
|
}
|
|
120
230
|
|
|
121
231
|
export class UsageService {
|
|
@@ -206,10 +316,10 @@ export class UsageService {
|
|
|
206
316
|
// THIS route's account: it is supported by the adapter but not
|
|
207
317
|
// applicable here, and the UI renders it as unsupported rather than
|
|
208
318
|
// showing the other account's number (issue #1688).
|
|
209
|
-
...(adapter?.balance !== undefined && balanceAppliesToRoute(adapter, this.
|
|
319
|
+
...(adapter?.balance !== undefined && balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
|
|
210
320
|
? { balanceSupported: true }
|
|
211
321
|
: {}),
|
|
212
|
-
...(adapter?.balance !== undefined && !balanceAppliesToRoute(adapter, this.
|
|
322
|
+
...(adapter?.balance !== undefined && !balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
|
|
213
323
|
? { balanceSupported: false }
|
|
214
324
|
: {}),
|
|
215
325
|
...(adapter?.plan !== undefined ? { planSupported: true } : {}),
|
|
@@ -587,7 +697,7 @@ export class UsageService {
|
|
|
587
697
|
// account, and probing the official endpoint would print this account's
|
|
588
698
|
// money under that route's name (issue #1688). Such a route reports the
|
|
589
699
|
// balance as unsupported instead.
|
|
590
|
-
const balanceApplies = balanceAppliesToRoute(adapter, this.
|
|
700
|
+
const balanceApplies = balanceAppliesToRoute(adapter, this.routeBaseUrl(route.id))
|
|
591
701
|
if (credential.key === undefined) {
|
|
592
702
|
// Nothing to probe with (oauth grant, no credential): retained facts
|
|
593
703
|
// would never refresh again, so the row resets to its credential
|
|
@@ -705,4 +815,15 @@ export class UsageService {
|
|
|
705
815
|
return section?.apiKeyEnv ?? 'DEEPSEEK_API_KEY'
|
|
706
816
|
}
|
|
707
817
|
|
|
818
|
+
/** The DeepSeek official adapter's configured baseURL. */
|
|
819
|
+
private deepseekBaseUrl(): string | undefined {
|
|
820
|
+
const section = readNamespace(this.ctx, 'llm-deepseek') as { baseURL?: string } | undefined
|
|
821
|
+
return section?.baseURL
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** The effective base URL for a provider route (pi-ai or deepseek). */
|
|
825
|
+
private routeBaseUrl(provider: string): string | undefined {
|
|
826
|
+
return this.piAiProfile(provider)?.baseURL ?? (isDeepSeekProviderRoute(provider) ? this.deepseekBaseUrl() : undefined)
|
|
827
|
+
}
|
|
828
|
+
|
|
708
829
|
}
|