dsh-model-think-level 1.0.0 → 1.0.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.md +8 -160
- package/lib/client/EffortEditor.d.ts +54 -2
- package/lib/client/KeyManager.d.ts +48 -0
- package/lib/client/auto-effort.d.ts +35 -0
- package/lib/client/injection/auto-effort-seat.d.ts +50 -0
- package/lib/client/injection/dom.d.ts +22 -0
- package/lib/client/injection/editor-tab-bar.d.ts +33 -0
- package/lib/client/injection/editor-tabs.d.ts +94 -0
- package/lib/client/injection/icon.d.ts +20 -0
- package/lib/client/injection/key-manager.d.ts +64 -0
- package/lib/client/injection/key-reveal.d.ts +84 -0
- package/lib/client/injection/model-menu.d.ts +34 -10
- package/lib/client/injection/models-page-editor.d.ts +30 -0
- package/lib/client/injection/provider-card-slot.d.ts +9 -4
- package/lib/client/injection/provider-delete-seat.d.ts +38 -0
- package/lib/client/key-manager-client.d.ts +71 -0
- package/lib/client/locales.d.ts +35 -0
- package/lib/client/styles.d.ts +1 -1
- package/lib/client/types.d.ts +31 -0
- package/lib/client.js +563 -6
- package/lib/client.js.map +4 -4
- package/lib/constants.d.ts +34 -0
- package/lib/headers-pi-ai.d.ts +54 -0
- package/lib/index.js +785 -13
- package/lib/index.js.map +4 -4
- package/lib/key-index.d.ts +91 -0
- package/lib/key-refs.d.ts +81 -0
- package/package.json +4 -1
- package/docs/banner-dark.svg +0 -82
- package/docs/banner-zh-dark.svg +0 -82
- package/docs/banner-zh.svg +0 -78
- package/docs/banner.svg +0 -78
- package/docs/demo-zh.svg +0 -85
- package/docs/demo.svg +0 -85
package/README.md
CHANGED
|
@@ -1,164 +1,12 @@
|
|
|
1
1
|
# DSH Model Think Level
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<picture>
|
|
5
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/banner-dark.svg">
|
|
6
|
-
<img src="docs/banner.svg" alt="DSH Model Think Level" width="720">
|
|
7
|
-
</picture>
|
|
8
|
-
</p>
|
|
3
|
+
DSH 的第三方模型补齐思考等级:在官方 Models 页每个模型行注入 Thinking 开关和强度/模态编辑器,在输入框模型菜单加供应商→模型选择器、视觉徽章与等级行,并带搜索、拖拽排序、供应商启用开关;以及按路由接管请求头与 user-agent。
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-

|
|
14
|
-
[](https://awesome-dsh-plugin.com)
|
|
5
|
+
模型选择器
|
|
6
|
+

|
|
7
|
+

|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<p align="center">
|
|
21
|
-
<img src="docs/demo.svg" alt="demo" width="640">
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
<p align="center">
|
|
25
|
-
<img src="assets/models-page-effort-editor.png" alt="The thinking-effort editor injected into a model row on the official Models page" width="720">
|
|
26
|
-
</p>
|
|
27
|
-
|
|
28
|
-
## Why
|
|
29
|
-
|
|
30
|
-
The `llm-pi-ai` adapter natively supports per-model `reasoningEfforts` and `input` declarations, but the official Models page editor deliberately keeps both fields out of reach. As a result, third-party models get **no thinking-level picker** in the composer, only the official DeepSeek API can set reasoning effort, hand-declared models are treated as **text-only**, and configuring any of this meant hand-writing `settings.yaml` blocks. This plugin brings both configuration surfaces back into the UI: edit inside the official model editor card, plus one-click auto-adapt.
|
|
31
|
-
|
|
32
|
-
## Features
|
|
33
|
-
|
|
34
|
-
- **In-page editor** — a **Thinking** switch plus a reasoning-effort block appears under each model row's disclosure on the official Models page (levels, default effort, input modalities, endpoint compatibility), committing with the card's own **Save**; changes stay pending until then, and **Cancel** discards them with the card's fields. The switch sits **outside** that block and gates it: switched off, the model is written as not reasoning and the whole block is not rendered — one row is all an un-thinking model shows.
|
|
35
|
-
- **Auto-adapt** — one click fills recommended levels, wire spellings and modalities from a built-in knowledge base (65 entries across 15 vendors — see [Supported models](docs/supported-models.md)), wire-protocol inference, and a same-origin probe of the provider's raw `/models` listing, every suggestion labeled by confidence; reference capacities show as read-only hints you copy yourself.
|
|
36
|
-
- **Auto-fill** — models without a declaration are filled at boot and when added mid-session (opt out via `autofill: false` / `modalityAutofill: false`); explicit declarations, `false` and deliberately-unset markers are never touched.
|
|
37
|
-
- **Three intents** — all levels off = unset (back to inheritance); only `off` = disable reasoning; levels armed = write the declaration.
|
|
38
|
-
- **Composer model & reasoning rows** — the official model menu's body renders a **Model** row and a **Reasoning level** row. The model row opens a provider column with that provider's models beside it, every entry carrying vision / thinking badges and the current pick marked in accent colour (no tick, so it cannot be mistaken for a badge); the reasoning row opens the model's own level list. The official trigger stays untouched, picks are optimistic and roll back on refusal, the popover stays open after a model switch so the level can be chosen next, and model switches keep your level through a per-session memory chain (issue #4's per-model default effort outranks it across sessions).
|
|
39
|
-
- **Composer model search** — a filter box above the official model list (provider / model / id tokens), always on.
|
|
40
|
-
- **Per-model default effort** — a "Default effort" picker on each model row, stored in the settings document; every new session starts the model there.
|
|
41
|
-
- **Request headers & `user-agent`** — a provider-card section edits the official `headers` field (masked, path-merged, Save-gated), and the plugin performs the `user-agent` override at the fetch layer per **provider route**, because the official adapter reserves that name. Routes that share one endpoint are told apart by their `baseURL` path and by the `model` carried in the request body, so each provider keeps its own identity; a request that identifies no single route falls back to the first declarer and the overlap is reported rather than guessed. Same-origin `/models` probes are covered.
|
|
42
|
-
- **Defensive injection** — everything keys off the official page's DOM; if an official upgrade changes the structure, injection simply pauses and the official page is unaffected.
|
|
43
|
-
- Bilingual copy (中文 / English).
|
|
44
|
-
|
|
45
|
-
## Supported models
|
|
46
|
-
|
|
47
|
-
The auto-adapt knowledge base carries **65 curated entries across 15 vendors** (DeepSeek, OpenAI, Anthropic Claude, Gemini, Grok, Qwen, GLM, Kimi, Mistral, MiniMax, MiMo, Doubao, Hunyuan, Step, ERNIE — re-verified against official docs 2026-08/09, including vision-capable variants and no-effort-control families). The full table — match patterns, level → wire-spelling ladders, defaults, modalities, reference capacities — lives in **[docs/supported-models.md](docs/supported-models.md)** (generated from [`src/knowledge.ts`](src/knowledge.ts), the authoritative source). Unlisted models fall back to protocol inference + generic levels, adjustable by hand.
|
|
48
|
-
|
|
49
|
-
## Install
|
|
50
|
-
|
|
51
|
-
Requires DeepSeek Harness **`0.1.5-alpha.1` or later** (per-line peer ranges; the `0.1.2-rc` / `0.1.3-alpha` lines are no longer supported — use plugin `0.3.7` there). Compiled and gated against `0.2.0-rc.1`. The per-kernel seam re-checks behind this live in [docs/compatibility-notes.md](docs/compatibility-notes.md).
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# from npm, under the dsh web profile
|
|
55
|
-
dsh plugin --profile web add dsh-model-think-level
|
|
56
|
-
|
|
57
|
-
# or from GitHub (source install; `lib/` builds via the prepare hook — the
|
|
58
|
-
# installer prints the `allowBuilds` key it needs, follow that and re-add)
|
|
59
|
-
dsh plugin --profile web add github:GaoHu1997/dsh-model-think-level
|
|
60
|
-
|
|
61
|
-
# or link a local checkout for development
|
|
62
|
-
npm install && npm run build
|
|
63
|
-
dsh plugin --profile web add link:D:/Project/dsh-model-think-level
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Restart `dsh web` and hard-refresh the browser.
|
|
67
|
-
|
|
68
|
-
## Usage
|
|
69
|
-
|
|
70
|
-
1. Configure a third-party provider (API key etc.) on the official Models page.
|
|
71
|
-
2. Expand a model row: the **Thinking** switch sits above the effort block, and the block sits under the official capacity fields.
|
|
72
|
-
- Leave **Thinking** on to configure levels. Switch it **off** and the model is saved as not reasoning — the whole effort block disappears with it, leaving just the switch row.
|
|
73
|
-
- Check levels (off / minimal / low / medium / high / xhigh / max) and fill the wire values (e.g. give `high` the spelling `ultra`, and the gateway receives `ultra` when you pick High in the composer);
|
|
74
|
-
- Toggle **Image input** under *Input modalities* to declare what the model accepts;
|
|
75
|
-
- Click **Auto-adapt** to fill recommended levels and modalities — reference capacities show up as read-only hints you copy into the official fields yourself;
|
|
76
|
-
- Any change is **pending** and lands when you press the card's own **Save**; **Cancel** (or a reload) discards it with the card's fields.
|
|
77
|
-
3. On a compatible protocol, the *Endpoint compatibility* section appears at the bottom — thinking budget field / vLLM priority on `openai-completions`, `max_output_tokens` handling on `openai-responses`.
|
|
78
|
-
4. All levels off + Save = unset the declaration; only `off` checked + Save = disable reasoning (`false`); *Clear declaration* + Save = back to inheriting the provider default.
|
|
79
|
-
5. In the composer, open the model menu: the **Model** row lists providers on the left and that provider's models on the right — each model carries an eye badge when it accepts images and a sparkle badge when it reasons — and the **Reasoning level** row lists the levels of the current model. Picking a model closes its list but keeps the menu open, so the level can be picked right after.
|
|
80
|
-
|
|
81
|
-
Declared models are immediately selectable for reasoning effort in the composer, and image-declared models accept attachments end to end.
|
|
82
|
-
|
|
83
|
-
## Configuration
|
|
84
|
-
|
|
85
|
-
Optional on the plugin's profile row (values shown are the defaults):
|
|
86
|
-
|
|
87
|
-
```yaml
|
|
88
|
-
- insert:
|
|
89
|
-
- id: dsh-model-think-level
|
|
90
|
-
name: dsh-model-think-level
|
|
91
|
-
config:
|
|
92
|
-
autofill: true # auto-fill undeclared models at boot
|
|
93
|
-
modalityAutofill: true # whether the boot fill also covers modalities
|
|
94
|
-
probeTimeoutMs: 15000 # /models probe fetch timeout
|
|
95
|
-
bootRetryDelaysMs: [1000, 2000, 4000, 8000, 16000, 30000]
|
|
96
|
-
defaultGuard: true # map effort-less calls on forced-thinking
|
|
97
|
-
# ladders to the vendor default
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## How it works
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
Browser (lib/client.js) Host (lib/index.js)
|
|
104
|
-
├─ DOM injector └─ Auto-fill
|
|
105
|
-
│ MutationObserver on the models page settings/document-updated →
|
|
106
|
-
│ → mounts EffortEditor in each invalidates the host cache;
|
|
107
|
-
│ model row's disclosure browser idle pass fills models
|
|
108
|
-
├─ Composer injection
|
|
109
|
-
│ MutationObserver on the document
|
|
110
|
-
│ → ComposerSlider (root pane)
|
|
111
|
-
│ → model search box (model-list pane)
|
|
112
|
-
├─ EffortEditor (React component) (knowledge base + inference)
|
|
113
|
-
│ level checkboxes / wire values /
|
|
114
|
-
│ input-modality toggle /
|
|
115
|
-
│ auto-adapt (zoned suggestions) / committed with the card's Save
|
|
116
|
-
│ └─ writes settings.mutate (llm-pi-ai)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
- `suggestEfforts()` in `src/knowledge.ts` is the knowledge base + inference engine — a pure function shared by host and browser.
|
|
120
|
-
- `reconcile()` in `src/client/injection/models-page-editor.ts` locates model rows and mounts the editor; `src/client/index.ts` assembles the browser half, one module per seam in `src/client/injection/`.
|
|
121
|
-
- `createEditorApi()` in `src/client/ops.ts` writes the declarations via `settings.mutate`, preserving every other row field and retrying once on a revision conflict.
|
|
122
|
-
|
|
123
|
-
## Development
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
npm run typecheck # tsc strict check on src
|
|
127
|
-
npm test # vitest: knowledge / inference / autofill / DOM injection / writing
|
|
128
|
-
npm run build # lib/*.js + lib/client.js (module-loader bundle)
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Compiled and gated against the `0.2.0-rc.1` official packages; see [docs/compatibility-notes.md](docs/compatibility-notes.md) for the per-kernel records.
|
|
132
|
-
|
|
133
|
-
## Known limitations
|
|
134
|
-
|
|
135
|
-
- Injection depends on the official Models page's DOM (aria-label/class); an official upgrade may pause injection until adapted — the official page is unaffected meanwhile.
|
|
136
|
-
- The auto-adapt probe route answers **loopback and IP-literal hosts only** (the core `/api` Host-allowlist discipline without `trustedHosts`), and **never follows redirects** — a gateway listing its models only behind a 30x simply yields no endpoint evidence; Auto-adapt falls back to the knowledge base and protocol inference.
|
|
137
|
-
- `reasoningEfforts` declarations are suggestions — what an endpoint actually accepts is up to its docs; tweak in the UI. The knowledge base is not exhaustive; families without an effort ladder carry no entry at all.
|
|
138
|
-
- Endpoint-compatibility switches are never auto-filled by design: they describe a gateway, not a model.
|
|
139
|
-
- The modality vocabulary follows pi-ai's core (`text` / `image` today); wider gateway support (PDF, audio, video) is recorded per family until the core vocabulary grows.
|
|
140
|
-
- Name-heuristic modality advice (vision-flavored ids) is deliberately low-confidence and labeled as such.
|
|
141
|
-
- Self-hosted relays: auto-fill pins `supportsDeveloperRole: false` on routes no official host claims (some upstreams reject the `developer` role); explicit values are never overwritten.
|
|
142
|
-
- Forced-thinking models (ladders without `off`, e.g. GLM-5.3): effort-less calls map to the vendor default instead of sending `thinking: disabled` — set `defaultGuard: false` to restore raw behavior.
|
|
143
|
-
- **Credentials inside `headers` are not redacted on disk**: the read-only view masks them, but the settings document still holds them in clear text — treat it like an API key.
|
|
144
|
-
- **The request-header section's edit-state detection reads an unofficial signal** (the official row exposes no data attribute for its editor state); if an official build renames that class root, the section stops appearing — never breaking the page.
|
|
145
|
-
- **Only one `user-agent` rewrite should be active**: sibling header plugins land on the same layer; the plugin detects and reports known ones, but the last writer on the wire wins.
|
|
146
|
-
- The request-layer takeover relies on the official adapter creating a fresh SDK client per request — guarded by an end-to-end test that fails loudly if that changes.
|
|
147
|
-
|
|
148
|
-
## Acknowledgements
|
|
149
|
-
|
|
150
|
-
This plugin is a renamed derivative of **[dsh-better-reasoning-effort](https://github.com/HaoyueQin/dsh-better-reasoning-effort) by [HaoyueQin](https://github.com/HaoyueQin)** (MIT). The request-header layer, the model knowledge base, the auto-adapt / auto-fill pipeline and the Models-page editor all come from that project — thank you for the original work. What changed here: the composer's effort slider was replaced by a provider → model picker plus a reasoning-level list, model entries gained vision / thinking badges, the selection tick was dropped in favour of an accent colour, and the per-model editor gained the master **Thinking** switch that gates it.
|
|
151
|
-
|
|
152
|
-
That project's own composer slider was **adapted from [dsh-reasoning-effort](https://github.com/HanaAyane/dsh-reasoning-effort) by [HanaAyane](https://github.com/HanaAyane)** (MIT); it is no longer shipped here, but the credit stands for the lineage. If you used the upstream plugin, remove it to avoid two effort controls on the same seat:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
dsh plugin --profile web remove dsh-reasoning-effort
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Activity
|
|
159
|
-
|
|
160
|
-
[](https://gitstock.org/GaoHu1997/dsh-model-think-level/stock.svg)
|
|
161
|
-
|
|
162
|
-
## License
|
|
163
|
-
|
|
164
|
-
MIT
|
|
9
|
+
供应商配置
|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { ReactNode } from 'react';
|
|
16
16
|
import type { CompatSuggestion, InputModalities, ReasoningEfforts } from '../knowledge.js';
|
|
17
|
-
import { type DraftLevels } from './effort.js';
|
|
18
|
-
import type { EffortEditorApi } from './types.js';
|
|
17
|
+
import { type DraftLevels, type EffortIntent } from './effort.js';
|
|
18
|
+
import type { EffortEditorApi, EffortWriteIntent, PendingWrite, PendingWriteParts } from './types.js';
|
|
19
19
|
/** One model row's data the editor needs. */
|
|
20
20
|
export interface EffortModel {
|
|
21
21
|
/** Model id (the settings key). */
|
|
@@ -77,6 +77,17 @@ export interface EffortEditorProps {
|
|
|
77
77
|
/** Localized copy. */
|
|
78
78
|
t: (key: string, params?: Record<string, string | number>) => string;
|
|
79
79
|
}
|
|
80
|
+
/** Draft state of the modality part: declared or inheriting, image or not. */
|
|
81
|
+
export interface DraftModality {
|
|
82
|
+
declared: boolean;
|
|
83
|
+
image: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** Read the stored declaration into a draft. Empty mirrors undefined: the
|
|
86
|
+
* resolved settings layer materializes absent arrays as [], and core reads
|
|
87
|
+
* that as "no answer here" -- never as a phantom text-only declaration. */
|
|
88
|
+
export declare function modalityFrom(input: InputModalities | undefined): DraftModality;
|
|
89
|
+
/** Resolve a modality draft to the write intent (null = durably unset). */
|
|
90
|
+
export declare function buildModalityIntent(draft: DraftModality): InputModalities | null;
|
|
80
91
|
/**
|
|
81
92
|
* The compat fields this editor can SET, per protocol — which is also the set
|
|
82
93
|
* it may clear. A key from neither list survives an apply untouched: a
|
|
@@ -103,6 +114,47 @@ export declare function clearedCompatKeys(routeApi: string | undefined, draft: C
|
|
|
103
114
|
export declare function compatClearIntent(routeApi: string | undefined, writeCompat: CompatSuggestion | undefined): {
|
|
104
115
|
clearCompatKeys?: readonly string[];
|
|
105
116
|
};
|
|
117
|
+
/** Semantic equality between a modality draft and a stored declaration. */
|
|
118
|
+
export declare function sameModality(draft: DraftModality, stored: InputModalities | undefined): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* The ladder part of a write, from a switch state and the draft it resolves to.
|
|
121
|
+
*
|
|
122
|
+
* `'keep'` when nothing is armed and nothing is stored: that is a no-op, NOT an
|
|
123
|
+
* unset — an undefined there would stamp the durable unset marker onto a
|
|
124
|
+
* never-declared model and silence host auto-fill.
|
|
125
|
+
* @param nextThinking - the (pre-update) switch state the edit resolved to.
|
|
126
|
+
* @param built - `buildIntent` of the draft the edit resolved to, which is the
|
|
127
|
+
* explicit `false` when the only armed level is 'off'.
|
|
128
|
+
* @param initialEfforts - the row's stored declaration.
|
|
129
|
+
* @returns the ladder intent to write.
|
|
130
|
+
*/
|
|
131
|
+
export declare function ladderIntent(nextThinking: boolean, built: EffortIntent, initialEfforts: EffortWriteIntent | undefined): EffortWriteIntent;
|
|
132
|
+
/**
|
|
133
|
+
* The compat draft a FRESH editor on this row would hold.
|
|
134
|
+
*
|
|
135
|
+
* The editor's three compat inputs are seeded from the stored block, so an
|
|
136
|
+
* untouched editor's draft is exactly the stored block restricted to the three
|
|
137
|
+
* fields it shows. The injector's provider-wide adapt has no editor to ask for
|
|
138
|
+
* a draft, so it derives the same value here: a hand-tuned field the UI does
|
|
139
|
+
* not show stays untouchable, and one it does show survives the write.
|
|
140
|
+
* @param compat - the row's stored compat block.
|
|
141
|
+
* @returns the draft, or undefined when it holds no field at all.
|
|
142
|
+
*/
|
|
143
|
+
export declare function compatDraftOf(compat: CompatSuggestion | undefined): CompatSuggestion | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Assemble one complete pending write from the parts a caller resolved: the
|
|
146
|
+
* compat merge (the applied suggestion's block with the caller's own fields on
|
|
147
|
+
* top), the thinking-budget default an openai-completions endpoint needs, and
|
|
148
|
+
* the clear list for the fields this write OWNS and left empty.
|
|
149
|
+
*
|
|
150
|
+
* The single implementation of that assembly: the row's own editor reports the
|
|
151
|
+
* user's edits through it, and the injector's provider-wide adapt reports
|
|
152
|
+
* collapsed rows through it, so the same suggestion always writes the same
|
|
153
|
+
* bytes whichever path applied it.
|
|
154
|
+
* @param parts - the resolved parts (see {@link PendingWriteParts}).
|
|
155
|
+
* @returns the write to hand the injector's ledger.
|
|
156
|
+
*/
|
|
157
|
+
export declare function pendingWriteOf(parts: PendingWriteParts): PendingWrite;
|
|
106
158
|
/**
|
|
107
159
|
* Render one model's thinking-effort + input-modality editor: the level
|
|
108
160
|
* checkboxes, the modality toggle, the auto-adapt action, and the
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The API-key list: one provider's keys, which one is in use, and the one
|
|
3
|
+
* inline form that adds, relabels or replaces a key.
|
|
4
|
+
*
|
|
5
|
+
* Rendered inline under the official key field (see `injection/key-manager`),
|
|
6
|
+
* not as a floating panel: the field's label already says "API key", so its list
|
|
7
|
+
* belongs directly beneath it, inside the card that owns the profile — the list
|
|
8
|
+
* reads as part of the field rather than as a surface that has to be opened,
|
|
9
|
+
* positioned and dismissed.
|
|
10
|
+
*
|
|
11
|
+
* The list is a view over host state and holds no truth of its own — every
|
|
12
|
+
* mutation answers with the whole list, and the list is adopted as it arrives
|
|
13
|
+
* (`adopt`), so the GUI can never drift from what the settings document and the
|
|
14
|
+
* credential store actually say. Three facts it deliberately reflects:
|
|
15
|
+
*
|
|
16
|
+
* - **the key in use is the profile's reference.** Choosing a row writes
|
|
17
|
+
* `providers.<route>.apiKeyEnv`, so it holds for every request the route
|
|
18
|
+
* makes, not only the ones this GUI starts. Exactly one key is in use per
|
|
19
|
+
* provider, which is why the mark at the head of a row is a radio.
|
|
20
|
+
* - **a value is shown masked, or not at all.** The host reads the stored
|
|
21
|
+
* secret and answers a head-and-tail preview; the middle never leaves it,
|
|
22
|
+
* and the eye beside the field stays the one gesture that reveals a value.
|
|
23
|
+
* - **one form serves all three edits.** Adding, relabelling and replacing a
|
|
24
|
+
* value differ only in which fields it shows, so they share a form rendered
|
|
25
|
+
* as the row below the row they act on — for an add, the list's last row.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-model-think-level/client/KeyManager
|
|
28
|
+
*/
|
|
29
|
+
import type { ReactNode } from 'react';
|
|
30
|
+
import { type KeyManagerClient } from './key-manager-client.js';
|
|
31
|
+
/** The repo's inline translator shape. */
|
|
32
|
+
type Translate = (key: string, params?: Record<string, string | number>) => string;
|
|
33
|
+
/** Props of {@link KeyManager}. */
|
|
34
|
+
export interface KeyManagerProps {
|
|
35
|
+
/** The provider route whose keys this list manages. */
|
|
36
|
+
readonly route: string;
|
|
37
|
+
/** Copy for the list's labels. */
|
|
38
|
+
readonly t: Translate;
|
|
39
|
+
/** Transport; injectable for tests. */
|
|
40
|
+
readonly client?: KeyManagerClient | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Render the key list for one provider route.
|
|
44
|
+
* @param props - see {@link KeyManagerProps}.
|
|
45
|
+
* @returns the list's root element.
|
|
46
|
+
*/
|
|
47
|
+
export declare function KeyManager(props: KeyManagerProps): ReactNode;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "adapt every model of this provider" request (user request ⑤).
|
|
3
|
+
*
|
|
4
|
+
* The seat beside the official fetch-models link and the per-row reasoning
|
|
5
|
+
* editor are two DIFFERENT React roots: the seat is raw DOM this plugin writes
|
|
6
|
+
* into the official catalogue head, while the editor is a root the injector
|
|
7
|
+
* mounts into each model row's disclosure. They share no props, so the request
|
|
8
|
+
* travels as one document event, and every editor whose own route matches
|
|
9
|
+
* answers it by running the very same `autoAdapt()` its own 自动适配 link runs.
|
|
10
|
+
*
|
|
11
|
+
* That indirection is the point: adapting N models this way is byte-identical
|
|
12
|
+
* to clicking that link N times, so the seat cannot drift from the control the
|
|
13
|
+
* user already trusts — and it still reaches the models whose thinking switch
|
|
14
|
+
* is OFF, which the link itself cannot (with the switch off the whole effort
|
|
15
|
+
* card, link included, is not rendered at all).
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-model-think-level/client/auto-effort
|
|
18
|
+
*/
|
|
19
|
+
/** The event a seat dispatches and every mounted editor listens for. */
|
|
20
|
+
export declare const AUTO_EFFORT_EVENT = "bre:auto-effort";
|
|
21
|
+
/** The event's payload: the provider route whose models are to be adapted. */
|
|
22
|
+
export interface AutoEffortDetail {
|
|
23
|
+
/** The provider route the request belongs to. */
|
|
24
|
+
readonly route: string;
|
|
25
|
+
}
|
|
26
|
+
/** Run one auto-adapt after every earlier one settled. */
|
|
27
|
+
export declare function queueAutoEffort(run: () => Promise<void> | void): void;
|
|
28
|
+
/** Ask every editor of `route` on this document to adapt its own model. */
|
|
29
|
+
export declare function requestAutoEffort(route: string, target?: Document): void;
|
|
30
|
+
/**
|
|
31
|
+
* Answer this document's auto-adapt requests until the returned disposer runs.
|
|
32
|
+
* A request naming another route is ignored — one card is open at a time, but a
|
|
33
|
+
* re-render can leave a previous card's rows mounted for a scan.
|
|
34
|
+
*/
|
|
35
|
+
export declare function listenAutoEffort(handler: (route: string) => void, target?: Document): () => void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "adapt every model of this provider" seat, beside the official
|
|
3
|
+
* fetch-models link (user request ⑤).
|
|
4
|
+
*
|
|
5
|
+
* The official catalogue head carries exactly one control — the link that
|
|
6
|
+
* fetches the provider's model list — so a provider with twenty models makes
|
|
7
|
+
* the user open twenty rows and click 自动适配 in each. The seat stands next to
|
|
8
|
+
* that link and asks every model of the provider to adapt at once.
|
|
9
|
+
*
|
|
10
|
+
* It is raw DOM inside the host's own head for the same reason the provider
|
|
11
|
+
* delete seat is: the plugin owns no slot there. It never moves a React-owned
|
|
12
|
+
* node — it only inserts itself as the anchor's next sibling and re-inserts
|
|
13
|
+
* itself when a re-render drops it — and every write is compare-before-write,
|
|
14
|
+
* so a settled pass emits no mutation of its own.
|
|
15
|
+
*
|
|
16
|
+
* The click does not write anything itself: it publishes a request on the
|
|
17
|
+
* document (see `../auto-effort.js`), which the per-model editors answer for
|
|
18
|
+
* their own rows, and hands the route to `deps.onRequest`, which answers for
|
|
19
|
+
* the models that have no editor on screen -- a collapsed row renders no
|
|
20
|
+
* disclosure container, so nothing else on the page can serve it. The seat
|
|
21
|
+
* therefore holds no write path of its own, and neither answer stages a
|
|
22
|
+
* document the open card is already holding.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-model-think-level/client/injection/auto-effort-seat
|
|
25
|
+
*/
|
|
26
|
+
/** One card's catalogue, paired with the route that card edits. */
|
|
27
|
+
export interface AutoEffortSeatTarget {
|
|
28
|
+
/** The card's catalogue section. */
|
|
29
|
+
readonly catalogue: HTMLElement;
|
|
30
|
+
/** The provider route the card edits. */
|
|
31
|
+
readonly route: string;
|
|
32
|
+
}
|
|
33
|
+
export interface AutoEffortSeatDeps {
|
|
34
|
+
/** The plugin's own translator, for the label and the tooltip. */
|
|
35
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Adapt every model of the route that has no on-screen editor: the injector
|
|
38
|
+
* owns the settings writes, so the seat only reports the click. Absent in
|
|
39
|
+
* tests that only care where the seat is placed.
|
|
40
|
+
*/
|
|
41
|
+
onRequest?: (route: string) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Give every target's catalogue head an auto-adapt seat. Idempotent: a head
|
|
45
|
+
* that already has one is only relabelled and re-seated, and a seat whose
|
|
46
|
+
* catalogue this pass no longer names is taken out.
|
|
47
|
+
*/
|
|
48
|
+
export declare function reconcileAutoEffortSeats(root: HTMLElement, targets: readonly AutoEffortSeatTarget[], deps: AutoEffortSeatDeps): void;
|
|
49
|
+
/** Take every seat out again (plugin dispose, HMR teardown). */
|
|
50
|
+
export declare function teardownAutoEffortSeats(): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare-before-write DOM helpers.
|
|
3
|
+
*
|
|
4
|
+
* Every write in the injection layer goes through one of these. An unchanged
|
|
5
|
+
* `setAttribute` still fires a mutation record, and the passes listen for
|
|
6
|
+
* mutations on the whole panel, so a blind write would have the scan chasing
|
|
7
|
+
* its own tail: mark → record → rescan → mark. Comparing first is what makes
|
|
8
|
+
* a settled page produce no mutations at all.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-model-think-level/client/injection/dom
|
|
11
|
+
*/
|
|
12
|
+
/** setAttribute only when the value would change. */
|
|
13
|
+
export declare function setAttr(el: Element, name: string, value: string): void;
|
|
14
|
+
/** `hidden` is a property, not an attribute these passes can compare textually. */
|
|
15
|
+
export declare function setHidden(el: HTMLElement | undefined, hidden: boolean): void;
|
|
16
|
+
/** Class writes compare first, like {@link setAttr}. `classList.add` is a
|
|
17
|
+
* no-op for a token it already holds in a spec-conforming engine, but it is
|
|
18
|
+
* not one in jsdom — and this helper's promise ("a settled page writes
|
|
19
|
+
* nothing") has to hold in both, since a write here feeds the same scan. */
|
|
20
|
+
export declare function setClass(el: HTMLElement, name: string, on: boolean): void;
|
|
21
|
+
/** Write text only when it differs, for the same reason as {@link setAttr}. */
|
|
22
|
+
export declare function setText(el: HTMLElement | undefined, text: string): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider editor's segmented control (供应商配置 / 模型配置 / 高级配置).
|
|
3
|
+
*
|
|
4
|
+
* This is the bar the editor-tabs pass drives. It lives HERE, in the plugin's
|
|
5
|
+
* own React subtree, rather than being built by the pass and inserted into the
|
|
6
|
+
* official card: the card's children belong to React, and a foreign node in
|
|
7
|
+
* that list survives only until the next render. The component is rendered
|
|
8
|
+
* inside the provider-card slot's own mount — a child of the card the slot
|
|
9
|
+
* already resolves — and the stylesheet reveals it as soon as the pass marks
|
|
10
|
+
* the card (`bre-tabbed`), which happens in the same mutation batch as the
|
|
11
|
+
* official render.
|
|
12
|
+
*
|
|
13
|
+
* The component renders STRUCTURE only: it holds no state, because the card's
|
|
14
|
+
* `data-bre-tab` attribute is the source of truth for both the pane switch
|
|
15
|
+
* (pure CSS) and the selected button. The pass owns the copy, the per-pane
|
|
16
|
+
* availability and `aria-selected`, so a language switch relabels the bar
|
|
17
|
+
* without a re-render and a pane that is not on the card hides its own button.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-model-think-level/client/injection/editor-tab-bar
|
|
20
|
+
*/
|
|
21
|
+
import type { ReactNode } from 'react';
|
|
22
|
+
import type { Translate } from '@deepseek-ai/dsh-client-ui-slots';
|
|
23
|
+
export interface EditorTabBarProps {
|
|
24
|
+
/** The plugin's own mount inside the card this bar switches. It hangs
|
|
25
|
+
* directly off the card, which is how the card is resolved on click. */
|
|
26
|
+
host: {
|
|
27
|
+
readonly current: HTMLDivElement | null;
|
|
28
|
+
};
|
|
29
|
+
/** The plugin's translator. */
|
|
30
|
+
t: Translate;
|
|
31
|
+
}
|
|
32
|
+
/** One provider editor's tab bar. */
|
|
33
|
+
export declare function EditorTabBar({ host, t }: EditorTabBarProps): ReactNode;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provider editor's three-tab takeover for the official Models settings
|
|
3
|
+
* page.
|
|
4
|
+
*
|
|
5
|
+
* The official editor presents one provider as a single long column — identity
|
|
6
|
+
* fields, then the model list, then (one section further up the card) this
|
|
7
|
+
* plugin's request-header editor — and the column keeps growing with every
|
|
8
|
+
* feature. This pass re-presents the same content as three tabs:
|
|
9
|
+
*
|
|
10
|
+
* `provider` — the official identity fields (display name, key, base URL,
|
|
11
|
+
* protocol); the editor's own header and action row stay put.
|
|
12
|
+
* `models` — the official model list, the rows the effort editor seats
|
|
13
|
+
* into.
|
|
14
|
+
* `advanced` — the plugin's request-header section, which the provider-card
|
|
15
|
+
* slot already mounts and which owns its own save/cancel.
|
|
16
|
+
*
|
|
17
|
+
* The takeover is pure CSS, by the same iron rule every pass here follows:
|
|
18
|
+
* React owns the official children, so none of them move. The card carries the
|
|
19
|
+
* active tab (`data-bre-tab`), each region carries its tab membership
|
|
20
|
+
* (`data-bre-region`), and the stylesheet — not this module — decides what is
|
|
21
|
+
* visible.
|
|
22
|
+
*
|
|
23
|
+
* The bar is NOT part of that DOM, and this module never inserts it. It is
|
|
24
|
+
* React-rendered by {@link EditorTabBar} inside the plugin's own request-header
|
|
25
|
+
* mount, which the provider-card slot already guarantees is a child of the
|
|
26
|
+
* card; the stylesheet reveals it the moment the card carries the takeover
|
|
27
|
+
* class. An earlier revision inserted the bar straight into the official child
|
|
28
|
+
* list from here, which made it a foreign node in a React-reconciled list: it
|
|
29
|
+
* was at the mercy of the next render, and the insertion itself needed the
|
|
30
|
+
* headers mount to be a DIRECT child of the card — an anchor this module cannot
|
|
31
|
+
* verify and which threw (aborting the whole scan) when it did not hold. Owning
|
|
32
|
+
* the bar inside our own subtree removes both hazards: the pass below only ever
|
|
33
|
+
* writes attributes, and the slot component publishes the card's open state the
|
|
34
|
+
* same way.
|
|
35
|
+
*
|
|
36
|
+
* Everything degrades to the official page: a card whose structure this pass
|
|
37
|
+
* cannot recognise (host redesign, partial render) is left exactly as the
|
|
38
|
+
* official page built it, a card whose takeover throws is rolled back the same
|
|
39
|
+
* way, and every attribute write is compared before it lands so the scan never
|
|
40
|
+
* feeds the MutationObserver its own tail.
|
|
41
|
+
*
|
|
42
|
+
* The same visit also dresses the provider's API-key field with its reveal eye
|
|
43
|
+
* ({@link revealKeyField}) and its key controls ({@link enhanceKeyManagerField}),
|
|
44
|
+
* because that field's shape is decided by exactly the same classification and
|
|
45
|
+
* its affordances have to come and go with the takeover.
|
|
46
|
+
*
|
|
47
|
+
* @module dsh-model-think-level/client/injection/editor-tabs
|
|
48
|
+
*/
|
|
49
|
+
declare const TAB_IDS: readonly ["provider", "models", "advanced"];
|
|
50
|
+
/** A tab id, as both the bar and the card attribute spell it. */
|
|
51
|
+
export type EditorTabId = (typeof TAB_IDS)[number];
|
|
52
|
+
/** The tab ids, in display order, for the tab-bar component. */
|
|
53
|
+
export declare const EDITOR_TAB_IDS: readonly EditorTabId[];
|
|
54
|
+
/** The bar's class, a button's class and a button's tab-id attribute, exported
|
|
55
|
+
* so the component that renders the bar and the pass that labels it spell the
|
|
56
|
+
* contract once. */
|
|
57
|
+
export declare const EDITOR_TABS_CLASS = "bre-editor-tabs";
|
|
58
|
+
export declare const EDITOR_TAB_BUTTON_CLASS = "bre-editor-tab";
|
|
59
|
+
export declare const EDITOR_TAB_ID_ATTR = "data-bre-tab-id";
|
|
60
|
+
/** Per-scan bookkeeping (one instance lives in `models-page.ts`). */
|
|
61
|
+
export interface EditorTabsState {
|
|
62
|
+
/** Cards whose takeover threw and had to be rolled back. A card that cannot
|
|
63
|
+
* be taken over must not repeat its complaint on every scan. */
|
|
64
|
+
readonly warned: WeakSet<Element>;
|
|
65
|
+
}
|
|
66
|
+
export interface EditorTabsDeps {
|
|
67
|
+
/** The plugin's own translator, for the tab labels. */
|
|
68
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
69
|
+
}
|
|
70
|
+
export declare function createEditorTabsState(): EditorTabsState;
|
|
71
|
+
/**
|
|
72
|
+
* The card a plugin mount sits in. Both the slot component (publishing the
|
|
73
|
+
* card's open state, switching tabs) and this pass resolve the card through
|
|
74
|
+
* this one rule, so a card can never be taken over by one and addressed by the
|
|
75
|
+
* other.
|
|
76
|
+
*/
|
|
77
|
+
export declare function editorCardOf(element: Element | null): HTMLElement | null;
|
|
78
|
+
/**
|
|
79
|
+
* Switch one card to a tab. The card attribute is the single source of truth:
|
|
80
|
+
* the stylesheet swaps the pane on the attribute alone, and the bar's buttons
|
|
81
|
+
* follow it here.
|
|
82
|
+
*/
|
|
83
|
+
export declare function selectEditorTab(card: HTMLElement, id: EditorTabId): void;
|
|
84
|
+
/**
|
|
85
|
+
* Visit every open provider editor and make sure it presents as tabs.
|
|
86
|
+
* Idempotent: a card that already carries the takeover is only relabelled,
|
|
87
|
+
* which is also how a language switch reaches the page. Cards the pass no
|
|
88
|
+
* longer recognises as open fall back to the official one-column layout.
|
|
89
|
+
*/
|
|
90
|
+
export declare function reconcileEditorTabs(root: HTMLElement, deps: EditorTabsDeps, state: EditorTabsState): void;
|
|
91
|
+
/** Remove every takeover mark. The bar is the tab-bar component's own DOM and
|
|
92
|
+
* is hidden, not removed, by dropping the class it is revealed under. */
|
|
93
|
+
export declare function teardownEditorTabs(): void;
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin's own outline glyphs.
|
|
3
|
+
*
|
|
4
|
+
* Shared because two injected controls draw one: the API-key eye, which swaps
|
|
5
|
+
* between two glyphs by attribute, and the key field's alias chip, which is
|
|
6
|
+
* icon-only so that it does not repeat the official "API key" label with the
|
|
7
|
+
* word for the same thing. Every glyph is a stroke-only outline on the same
|
|
8
|
+
* 16px grid, inherits `currentColor`, and is hidden from assistive tech — the
|
|
9
|
+
* button holding it carries the meaning in its own `aria-label`.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-model-think-level/client/injection/icon
|
|
12
|
+
*/
|
|
13
|
+
/** The namespace every SVG node here is created in. */
|
|
14
|
+
export declare const SVG_NS = "http://www.w3.org/2000/svg";
|
|
15
|
+
/** Marks a glyph, so the stylesheet can show one of several, and a sweep find it. */
|
|
16
|
+
export declare const ICON_ATTR = "data-bre-icon";
|
|
17
|
+
/** One outline path of a glyph, in the 16px viewBox. */
|
|
18
|
+
export declare function svgPath(d: string): SVGPathElement;
|
|
19
|
+
/** A stroke-only 16px glyph built from outline paths. */
|
|
20
|
+
export declare function svgGlyph(kind: string, paths: readonly string[]): SVGElement;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key list's seat: the official key field grows a plugin-owned host, and the
|
|
3
|
+
* list is rendered inline into it, directly under the input.
|
|
4
|
+
*
|
|
5
|
+
* The field is not reachable from the settings page's React tree — the plugin
|
|
6
|
+
* only gets keyed slots, and none of them sits beside a form field — so the seat
|
|
7
|
+
* is a DOM leaf inserted directly AFTER the field. The list then falls under the
|
|
8
|
+
* input, which is where a provider's keys belong: the label above already says
|
|
9
|
+
* "API key", and the list reads as part of the field rather than as a surface
|
|
10
|
+
* that has to be opened, positioned and dismissed.
|
|
11
|
+
*
|
|
12
|
+
* A sibling rather than a last child: the reveal eye hangs on the field's own
|
|
13
|
+
* bottom edge (`bottom: 0` against the field, which its validation-message rule
|
|
14
|
+
* also measures from), so a list inside the field would carry the eye down with
|
|
15
|
+
* it and leave it sitting on the list's last row. React tolerates the foreign
|
|
16
|
+
* sibling — it resolves a node's next sibling by walking past nodes it does not
|
|
17
|
+
* own — and the list mirrors the field's region, so a pane hides it along with
|
|
18
|
+
* the field it belongs to.
|
|
19
|
+
*
|
|
20
|
+
* The host carries the route its list was built for — that attribute is how the
|
|
21
|
+
* pass knows whether the mounted list still belongs to the field it follows, and
|
|
22
|
+
* how a test asks what is mounted there.
|
|
23
|
+
*
|
|
24
|
+
* The whole seat is skipped when the field is not a key field, or when its card
|
|
25
|
+
* announces no route: the add-provider form has no route yet, and keys belong to
|
|
26
|
+
* a route that exists.
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-model-think-level/client/injection/key-manager
|
|
29
|
+
*/
|
|
30
|
+
import type { KeyManagerClient } from '../key-manager-client.js';
|
|
31
|
+
/** Set on a field whose keys the plugin lists. */
|
|
32
|
+
export declare const KEY_MANAGED_CLASS = "bre-key-managed";
|
|
33
|
+
/** The repo's inline translator shape. */
|
|
34
|
+
export type Translator = (key: string, params?: Record<string, string | number>) => string;
|
|
35
|
+
/** What a reconcile tells the field's key list. */
|
|
36
|
+
export interface KeyManagerFieldDeps {
|
|
37
|
+
/** Copy for the list and its form. */
|
|
38
|
+
readonly t: Translator;
|
|
39
|
+
/** The provider route this card edits; absent on the add-provider form. */
|
|
40
|
+
readonly route?: string | undefined;
|
|
41
|
+
/** List transport; injectable for tests. */
|
|
42
|
+
readonly client?: KeyManagerClient | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Mount the key list into a field, replacing whatever this module mounted there
|
|
46
|
+
* before. Idempotent: a field whose list is already up and current is left
|
|
47
|
+
* exactly as it is, so a settled page sees no write at all.
|
|
48
|
+
*/
|
|
49
|
+
export declare function enhanceKeyManagerField(field: HTMLElement, deps: KeyManagerFieldDeps): void;
|
|
50
|
+
/**
|
|
51
|
+
* Drop the list of one field, leaving the official field exactly as the page
|
|
52
|
+
* rendered it. Called when the takeover ends, and for the stray sweep of a page
|
|
53
|
+
* that is being torn down.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resetKeyManagerField(field: HTMLElement): void;
|
|
56
|
+
/** Drop every list this module mounted. */
|
|
57
|
+
export declare function teardownKeyManager(): void;
|
|
58
|
+
/**
|
|
59
|
+
* The route a field's list was built for, or undefined when it has none.
|
|
60
|
+
*
|
|
61
|
+
* A test hook: the route lives on the host, so what the pass decided can be read
|
|
62
|
+
* back from the DOM alone.
|
|
63
|
+
*/
|
|
64
|
+
export declare function keyManagerRoute(field: HTMLElement): string | undefined;
|