dsh-magpie-connect 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,76 +1,100 @@
1
1
  # dsh-magpie-connect
2
2
 
3
- **Magpie LAN gateway models, natively inside DSH (DeepSeek Harness).**
3
+ 把 Magpie 局域网网关上的模型接进 DeepSeek Harness(DSH)的模型选择器。
4
4
 
5
- No extra process. Points at your configured gateway origin and serves
6
- every model from the gateway's `GET …/models` in the DSH model picker as provider
7
- `dsh-magpie-connect` (picker label `magpie`).
5
+ 装好之后,你就能在 DSH 里直接选用网关提供的模型,不需要额外跑进程,也不用配代理。
8
6
 
9
- > The gateway is **not** configured out of the box: set the API URL **and**
10
- > API key on the Settings → Magpie page (or via `cordis.patch.yml`). Both
11
- > are required — until then the picker stays empty and calls fail fast with
12
- > a "not configured" error.
7
+ <p align="center">
8
+ <img src="docs/image/screenshot-settings.png" alt="Magpie 网关设置页" width="50%">
9
+ </p>
13
10
 
14
- - **双接口** — `/v1/chat/completions` 与 `/v1/responses` 按模型 `native_endpoints`
15
- 自动分流(Muse Spark / Codex / Grok 走 responses,其余走 completions)
16
- - **图片识别** — 声明 `modalities.input: image` 的模型开放图片输入,经 harness
17
- 附件服务取字节后以 OpenAI `image_url` / `input_image` 上行
18
- - **思考级别选择** — 网关 `supported_reasoning_levels` 原样进 picker(含 `none` /
19
- `ultra` 扩展档),直通网关不截断;过期档位按就近原则收敛,不会 400
11
+ ## 功能
20
12
 
21
- ## Install
13
+ - **模型自动同步** — 网关列出的模型全部进入 DSH 模型选择器,想显示哪些由你决定。
14
+ - **图片识别** — 网关声明支持图片的模型可以直接发图。
15
+ - **思考档位** — 网关支持的思考档位(含 `none`、`ultra`)原样出现在选择器里。
16
+
17
+ ## 安装
22
18
 
23
19
  ```sh
24
20
  dsh plugin --profile web add dsh-magpie-connect
25
21
  ```
26
22
 
27
- Restart `dsh web` after installing. Requires DSH with a web profile;
28
- Node.js ≥ 20; LAN route to `http://api.lan`.
23
+ 安装后重启 `dsh web`。
24
+
25
+ 已有安装用同一条命令即可更新到最新版本。
26
+
27
+ **需要**:DSH 的 web profile、Node.js ≥ 20、能访问网关所在局域网。
28
+
29
+ ## 使用
30
+
31
+ ### 第一步:填网关地址和 Key
32
+
33
+ 打开 **设置 → Magpie 网关**,确认两项:
34
+
35
+ - **API 地址** — 网关地址,**要带版本号**。默认已填好 `http://127.0.0.1:3425/v1`,网关不在本机时改成实际地址。
36
+ - **API Key** — 网关凭据,必填。
37
+
38
+ 点「保存」后,可以点「测试连接」确认通不通(会告诉你网关上有多少模型)。两项都齐了才会生效——
39
+ 地址有默认值,Key 没有,所以没填 Key 之前模型选择器是空的。
40
+
41
+ > **地址一定要带版本段**(如 `/v1`)。插件只在你填的地址后面接资源名,不会自己补
42
+ > 版本号——所以网关以后升级到 `/v2`、`/v3`,你只要改这一个字符串。
43
+
44
+ ### 第二步:选择要显示的模型
45
+
46
+ 点 **获取可用模型**,会弹出网关当前的模型列表:勾选你想要的,再点「应用选择」。
47
+ 弹窗里可以搜索、全选、取消全选。
48
+
49
+ 主列表只显示已启用的模型。不想要的点该行末尾的「删除」,它就会从选择器里消失;
50
+ 想加回来,再回到弹窗里勾选。
29
51
 
30
- ## Settings page
52
+ > 弹窗查的是**你当前填的地址和 Key**(哪怕还没保存),所以换网关是一趟的事。
53
+ > 连不上也不会卡住——失败原因会显示在列表下方,已列出的行仍然可以操作。
31
54
 
32
- Settings → Magpie 网关 (sidebar): edit the API URL and API key, test the
33
- connection (reports how many models the endpoint serves), and choose which
34
- models appear in the model picker. Everything saves to
35
- `~/.dsh-magpie-connect/settings.json` and applies immediately — no restart.
55
+ ### 什么时候需要保存
36
56
 
37
- **两种提交时机**:地址与 Key 需要点「保存」(凭证不适合静默写入);**模型列表的
38
- 增删会自动保存**,不需要、也没有保存按钮。因为模型卡片在连接卡片下方,一个管全部
39
- 的保存按钮会让"删了一行"看起来已经生效——它其实没有。自动保存通过队列串行化,
40
- 连点多次删除不会因为响应乱序而把旧集合写回磁盘。
57
+ - **地址和 Key**:点「保存」才生效。
58
+ - **模型增删**:**自动保存**,不用点任何按钮。
41
59
 
42
- **API 地址是带版本号的完整前缀**,例如 `http://api.lan/v1`:插件只往后拼资源名
43
- (`/models`、`/chat/completions`、`/responses`),**绝不自己补版本号**。网关以后
44
- 上 `/v2`、`/v3` 时,只改这一个字符串即可,不会拼成 `/v2/v1/models`。尾斜杠会被
45
- 去掉,路径原样保留。
60
+ ## 常见问题
46
61
 
47
- **获取可用模型** 打开候选弹窗:它问的是**表单当前显示的地址与 Key**(包括还没
48
- 保存的),所以填一个新网关是一趟而不是"保存—返回—再看"。弹窗里可搜索、可全选/
49
- 取消全选、逐个勾选,**应用选择**把勾选结果换算成隐藏集合。列表只显示已启用的模型,
50
- 隐藏的只在弹窗里;行尾的「删除」即隐藏,重新加回也在弹窗。保存时会清掉网关已经
51
- 不再提供的陈旧 id。查不通不会卡死——失败信息就显示在列表下方,行仍然可以手改。
62
+ **模型选择器是空的?**
52
63
 
53
- Precedence per field: settings page > `cordis.patch.yml` config > defaults.
54
- Hidden models only come from the page; an empty list shows everything.
64
+ 先确认地址和 Key 都填了并且已经保存,再点「测试连接」。最常见的原因是地址缺了版本段
65
+ (比如填成 `http://127.0.0.1:3425`,而应该是 `http://127.0.0.1:3425/v1`)。
55
66
 
56
- Toggling visibility, changing the origin, or a refresh that added/dropped
57
- models publishes `llm/adapters-updated`, the one event DSH's picker listens
58
- to — the browser caches one catalog read per Host generation, so without it
59
- the picker keeps the stale list until `dsh web` restarts.
67
+ **删掉的模型怎么加回来?**
60
68
 
61
- ## Configuration
69
+ 点「获取可用模型」,在弹窗里重新勾上,再点「应用选择」。
62
70
 
63
- Set the gateway URL first — either on the Settings → Magpie page (recommended,
64
- applies instantly) or via the profile's `cordis.patch.yml`:
71
+ **改了网关地址,模型列表没变?**
72
+
73
+ 保存后列表会重新拉取。如果新网关暂时不通,会先用本地缓存。
74
+
75
+ **页面提示「未配置」?**
76
+
77
+ 地址和 Key 缺任意一项都会这样——此时选择器是空的,调用也会直接失败并给出提示。
78
+
79
+ **发图失败?**
80
+
81
+ 只有网关声明支持图片的模型才能收图。如果给纯文本模型发图,会直接报错而不是静默丢弃。
82
+ 另外,图片需要通过 harness 的附件服务读取,在没有该服务的组合里也会失败。
83
+
84
+ ## 进阶用法
85
+
86
+ ### 用配置文件代替设置页
87
+
88
+ 除了设置页,也可以在 profile 的 `cordis.patch.yml` 里配置:
65
89
 
66
90
  ```yaml
67
91
  - id: dsh-magpie-connect
68
92
  name: 'dsh-magpie-connect'
69
93
  config:
70
94
  providerId: dsh-magpie-connect
71
- displayName: magpie
72
- baseUrl: http://api.lan/v1 # versioned API root — include the version
73
- apiKey: not-needed # LAN gateway needs none
95
+ displayName: Magpie
96
+ baseUrl: http://127.0.0.1:3425/v1 # 带版本号的完整地址
97
+ apiKey: not-needed # 局域网网关通常不校验,但必须非空
74
98
  refreshSeconds: 300
75
99
  maxRetries: 2
76
100
  timeoutMs: 300000
@@ -78,56 +102,61 @@ applies instantly) or via the profile's `cordis.patch.yml`:
78
102
  idleTimeoutMs: 60000
79
103
  ```
80
104
 
81
- | Option | Default | Description |
105
+ | 配置项 | 默认值 | 说明 |
82
106
  | --- | --- | --- |
83
- | `providerId` | `dsh-magpie-connect` | Provider name shown in DSH. |
84
- | `displayName` | `magpie` | Picker grouping label. |
85
- | `baseUrl` | `''` (not configured) | Versioned API root, e.g. `http://api.lan/v1`. The version is part of the value, not appended — a gateway on `/v2` is reached by changing this string. Required. |
86
- | `apiKey` | `''` (not configured) | Bearer key. Required, even if `/models` answers without one. |
87
- | `dataDir` | `~/.dsh-magpie-connect` | State dir (status, settings file, cache). |
88
- | `refreshSeconds` | `300` | Live catalog refresh interval. |
89
- | `maxRetries` | `2` | Connection-setup retries on 429/5xx. |
90
- | `timeoutMs` | `300000` | Overall upstream request cap in ms. |
91
- | `firstEventTimeoutMs` | `90000` | Stall watchdog: max wait for first event. |
92
- | `idleTimeoutMs` | `60000` | Stall watchdog: max silence between events. |
93
-
94
- ## How it works
107
+ | `providerId` | `dsh-magpie-connect` | 注册到 DSH 的 provider 名称。 |
108
+ | `displayName` | `Magpie` | 模型选择器里的分组名。 |
109
+ | `baseUrl` | `http://127.0.0.1:3425/v1` | 带版本号的 API 地址。 |
110
+ | `apiKey` | `''`(未配置) | 网关凭据。必填,即使网关不校验——地址有默认值,Key 没有。 |
111
+ | `dataDir` | `~/.dsh-magpie-connect` | 状态目录(状态快照、设置文件、模型缓存)。 |
112
+ | `refreshSeconds` | `300` | 模型目录刷新间隔(秒)。 |
113
+ | `maxRetries` | `2` | 遇到 429/5xx 时的连接重试次数。 |
114
+ | `timeoutMs` | `300000` | 单次请求的总超时(毫秒)。 |
115
+ | `firstEventTimeoutMs` | `90000` | 等待首个上游事件的超时(毫秒)。 |
116
+ | `idleTimeoutMs` | `60000` | 上游事件之间的静默超时(毫秒)。 |
117
+
118
+ 优先级:设置页 > `cordis.patch.yml` > 默认值。设置页只覆盖你保存过的字段。
119
+
120
+ ### 工作原理
95
121
 
96
122
  ```
97
- DSH session
98
- │ harness chunks (block-start / text-delta / usage / finish …)
123
+ DSH 会话
124
+ │ harness 数据块(block-start / text-delta / usage / finish …)
99
125
  ▼
100
- MagpieAdapter (registered LlmAdapter)
101
- │ pi-ai openai-completions stream (default) /
102
- │ openai-responses stream (responses-only lane)
126
+ MagpieAdapter(注册为 DSH 的 LlmAdapter)
127
+ │ pi-ai openai-completions 流(默认)/
128
+ │ openai-responses 流(仅支持 responses 的模型)
103
129
  ▼
104
- http://api.lan/v1 ← chat/completions or responses per native_endpoints
130
+ http://127.0.0.1:3425/v1 ← 按 native_endpoints 选择 chat/completions 或 responses
105
131
  ```
106
132
 
107
- - **Catalog** — `GET {baseUrl}/models` 全量接入,无付费过滤;磁盘缓存 + 编译期静态
108
- 快照兜底,网关宕机时 picker 仍可用。任何暴露集合的变化(隐藏/显示、换网关
109
- 地址、刷新后模型增删)都会 `emit('llm/adapters-updated')`,让浏览器那份
110
- catalog 缓存立即重读——否则 picker 会一直显示旧列表到下次重启。
111
- - **思考档** — `reasoning=true` 且有 ladder 的模型出 picker;无 ladder 但可思考
112
- 的模型保持 wire 可思考(显式档位直通);`none` 显式关闭思考,`ultra` 直通
113
- 网关,不在 pi-ai 内截断。
114
- - **图片** — 有图的请求经 `ctx.get('attachments')` 取 request 版本字节;
115
- 文本模型误收图时直接失败(harness 按 inputModalities 已做门控)。
116
- - **韧性** — 启动即注册,目录后台预热(失败按短间隔重试);stall 看门狗让静默
117
- 挂起快速失败为 `TIMEOUT`;`length` 截断如实上抛,不做自动续写。
133
+ - **模型目录** — 全量接入网关 `GET {baseUrl}/models`,不做付费过滤;磁盘缓存加编译期
134
+ 静态快照兜底,网关宕机时选择器仍可用。暴露集合一旦变化(隐藏/显示、换网关地址、
135
+ 刷新后模型增删),会发出 `llm/adapters-updated` 让浏览器重读缓存——否则选择器会
136
+ 一直显示旧列表直到下次重启。
137
+ - **思考档位** — 有档位阶梯的模型进入选择器;没有阶梯但可思考的模型保持可思考
138
+ (显式档位直通网关);`none` 关闭思考,`ultra` 直通网关,不在本地截断。
139
+ - **图片** — 带图的请求经 harness 附件服务取字节后上行;文本模型收到图片会直接
140
+ 失败(harness 已按模型声明的输入类型做过门控)。
141
+ - **容错** — 启动即注册,目录在后台预热;静默挂起会被看门狗快速判为超时;
142
+ 上游截断如实上报,不做自动续写。
118
143
 
119
- Health snapshot: `~/.dsh-magpie-connect/adapter-status.json`.
144
+ 运行状态快照写在 `~/.dsh-magpie-connect/adapter-status.json`,设置与缓存在同一目录。
120
145
 
121
- ## Development
146
+ ## 开发
122
147
 
123
148
  ```sh
124
149
  pnpm install
125
- pnpm run check
150
+ pnpm run check # 类型检查 + 测试 + 构建
151
+ pnpm run deploy:local # 构建并同步到 web profile
126
152
  ```
127
153
 
128
- The check ladder is typecheck + tests + build; `lib/` is committed, so profile
129
- installs run without a build.
154
+ `lib/` 是提交进仓库的构建产物,所以 profile 安装时不需要现场构建。
155
+
156
+ 发布由 `.github/workflows/release.yml` 负责:把 `package.json` 和
157
+ `dsh.plugin.json` 的版本号改成一致后提交推送,流水线会打 tag、建 GitHub Release
158
+ 并发布到 npm。版本号带 `-`(如 `0.3.0-rc.1`)时自动标记为预发布。
130
159
 
131
- ## License
160
+ ## 许可证
132
161
 
133
162
  [MIT](./LICENSE)
package/cordis.patch.yml CHANGED
@@ -1,15 +1,15 @@
1
1
  # dsh-magpie-connect DSH plugin — bundle patch: insert the plugin row (deployment-global).
2
2
  #
3
- # The gateway is NOT configured out of the box: set baseUrl on the Settings
4
- # sidebar page ("Magpie") or via the config object below (see src/config.ts).
5
- # Until an API root is set the picker stays empty and calls fail fast.
3
+ # The gateway ships with the conventional local address but no key: set the key
4
+ # on the Settings sidebar page ("Magpie") or via the config object below (see
5
+ # src/config.ts). Until a key is set the picker stays empty and calls fail fast.
6
6
  #
7
7
  # The Settings sidebar page can override baseUrl/apiKey at runtime
8
8
  # (saved to <dataDir>/settings.json, applied without restart) and hides models
9
9
  # from the picker. Page values win per-field; the patch config below stays as
10
10
  # fallback (e.g. headless compositions without the settings page).
11
11
  #
12
- # - baseUrl: versioned API root, e.g. http://api.lan/v1 (required). Include the
12
+ # - baseUrl: versioned API root (default http://127.0.0.1:3425/v1). Include the
13
13
  # version segment: the plugin appends only the resource (/models, /responses),
14
14
  # so a gateway on /v2 or /v3 is reached by changing this one string.
15
15
  # - apiKey: gateway credential (required, even if /models needs none).
package/dsh.plugin.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "dsh-magpie-connect",
3
3
  "description": "Magpie LAN gateway models for the DeepSeek Harness web GUI",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "entry": {
6
6
  "name": "dsh-magpie-connect",
7
- "inject": ["llm"]
7
+ "inject": [
8
+ "llm"
9
+ ]
8
10
  }
9
11
  }
package/lib/client.js CHANGED
@@ -41,7 +41,7 @@ var EN = {
41
41
  intro: "Connect DSH to the Magpie LAN model gateway. Changes save to this machine and apply immediately \u2014 no restart needed.",
42
42
  connection: "Connection",
43
43
  baseUrl: "API URL",
44
- baseUrlPlaceholder: "http://api.lan/v1",
44
+ baseUrlPlaceholder: "http://127.0.0.1:3425/v1",
45
45
  apiKey: "API key",
46
46
  apiKeyPlaceholder: "required",
47
47
  requiredNote: "API URL and API key are both required. Include the version segment (e.g. /v1) \u2014 the URL is used exactly as written.",
@@ -90,7 +90,7 @@ var ZH = {
90
90
  intro: "\u628A DSH \u63A5\u5165 Magpie \u5C40\u57DF\u7F51\u6A21\u578B\u7F51\u5173\u3002\u4FEE\u6539\u4FDD\u5B58\u5728\u672C\u673A\u5E76\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002",
91
91
  connection: "\u8FDE\u63A5",
92
92
  baseUrl: "API \u5730\u5740",
93
- baseUrlPlaceholder: "http://api.lan/v1",
93
+ baseUrlPlaceholder: "http://127.0.0.1:3425/v1",
94
94
  apiKey: "API Key",
95
95
  apiKeyPlaceholder: "\u5FC5\u586B",
96
96
  requiredNote: "API \u5730\u5740\u4E0E Key \u5747\u4E3A\u5FC5\u586B\u3002\u5730\u5740\u9700\u5305\u542B\u7248\u672C\u6BB5\uFF08\u5982 /v1\uFF09\uFF0C\u63D2\u4EF6\u6309\u4F60\u586B\u5199\u7684\u539F\u6837\u4F7F\u7528\u3002",
@@ -304,7 +304,7 @@ function chipsFor(row) {
304
304
  return chips;
305
305
  }
306
306
  function ModelListEditor(props) {
307
- const { models, visibleIds, onFetch, onRemove, onHiddenChange, disabled, fetchable, status } = props;
307
+ const { models, visibleIds, onFetch, onRefresh, onRemove, onHiddenChange, disabled, fetchable, status } = props;
308
308
  const [filter, setFilter] = (0, import_react2.useState)("");
309
309
  const [busy, setBusy] = (0, import_react2.useState)(false);
310
310
  const [fetchFailure, setFetchFailure] = (0, import_react2.useState)(void 0);
@@ -347,6 +347,8 @@ function ModelListEditor(props) {
347
347
  setFetchFailure(text("fetchEmpty"));
348
348
  return;
349
349
  }
350
+ await onRefresh().catch(() => {
351
+ });
350
352
  setCandidateQuery("");
351
353
  setCandidates(found);
352
354
  setPicked(initialPicked(found, visibleIds));
@@ -512,7 +514,7 @@ function createWriteQueue(write, onSettled) {
512
514
 
513
515
  // src/client/page.tsx
514
516
  var import_jsx_runtime2 = require("react/jsx-runtime");
515
- var PLUGIN_VERSION = "0.2.0";
517
+ var PLUGIN_VERSION = "0.3.0";
516
518
  var css2 = {
517
519
  section: { display: "flex", flexDirection: "column", gap: 16, maxWidth: 780, paddingBottom: 32 },
518
520
  title: { margin: 0, fontSize: 22, lineHeight: "30px", fontWeight: 600, color: "var(--dsw-alias-label-primary)" },
@@ -610,6 +612,10 @@ function MagpieSettings() {
610
612
  const result = await request("/api/magpie-discover", "POST", { baseUrl, apiKey });
611
613
  return result.models ?? [];
612
614
  };
615
+ const refreshModels = async () => {
616
+ const value = await request("/api/magpie-models");
617
+ setModels(value.models ?? []);
618
+ };
613
619
  const applyHidden = (next) => {
614
620
  setHidden(next);
615
621
  setNotice(void 0);
@@ -626,8 +632,7 @@ function MagpieSettings() {
626
632
  setBaseUrl(saved.baseUrl);
627
633
  setApiKey(saved.apiKey);
628
634
  setHidden(saved.hiddenModels ?? []);
629
- const modelsValue = await request("/api/magpie-models");
630
- setModels(modelsValue.models ?? []);
635
+ await refreshModels();
631
636
  setNotice(text("saved"));
632
637
  } catch (cause) {
633
638
  setError(cause instanceof Error ? cause.message : String(cause));
@@ -678,6 +683,7 @@ function MagpieSettings() {
678
683
  models,
679
684
  visibleIds,
680
685
  onFetch: fetchCandidates,
686
+ onRefresh: refreshModels,
681
687
  onRemove: (id) => applyHidden(hideOne(hidden, id)),
682
688
  onHiddenChange: applyHidden,
683
689
  disabled: !loaded || working,
package/lib/index.js CHANGED
@@ -115,6 +115,7 @@ var ModelCatalog = class {
115
115
  #refreshSeconds;
116
116
  #cachePath;
117
117
  #baseUrl;
118
+ #apiKey;
118
119
  #fetch;
119
120
  #now;
120
121
  #timer = null;
@@ -132,6 +133,7 @@ var ModelCatalog = class {
132
133
  this.#refreshSeconds = options.refreshSeconds ?? 300;
133
134
  this.#cachePath = options.cachePath;
134
135
  this.#baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
136
+ this.#apiKey = options.apiKey ?? "";
135
137
  this.#fetch = options.fetchImpl ?? fetch;
136
138
  this.#now = options.now ?? Date.now;
137
139
  this.#onRefresh = options.onRefresh;
@@ -215,13 +217,22 @@ var ModelCatalog = class {
215
217
  get configured() {
216
218
  return this.#baseUrl !== "";
217
219
  }
220
+ /**
221
+ * Auth header for every catalog fetch. The gateway guards `GET /models` with
222
+ * the same Bearer key as inference, so omitting it is answered 401 — which
223
+ * the refresh loop then reports as a "catalog issue" while the picker keeps
224
+ * serving whatever it already had.
225
+ */
226
+ #authHeaders() {
227
+ return this.#apiKey === "" ? {} : { authorization: `Bearer ${this.#apiKey}` };
228
+ }
218
229
  async refreshModels() {
219
230
  if (!this.configured) {
220
- this.#lastError = "magpie gateway baseUrl is not configured \u2014 set it on the Magpie settings page";
231
+ this.#lastError = "magpie gateway API URL or key is not configured \u2014 set both on the Magpie settings page";
221
232
  return;
222
233
  }
223
234
  try {
224
- const entries = await fetchMagpieModels(this.#baseUrl, this.#fetch);
235
+ const entries = await fetchMagpieModels(this.#baseUrl, this.#fetch, { headers: this.#authHeaders() });
225
236
  this.#entries = entries;
226
237
  this.#updatedAt = this.#now();
227
238
  this.#lastError = "";
@@ -275,19 +286,42 @@ var ModelCatalog = class {
275
286
  isHidden(model) {
276
287
  return this.#hidden.has(model);
277
288
  }
278
- /** Point the refresh loop at another API root (settings page change). */
279
- setBaseUrl(baseUrl) {
289
+ /**
290
+ * Point the refresh loop at another API root or credential (settings page
291
+ * change).
292
+ *
293
+ * The key is part of the identity here, not just the URL: the gateway's
294
+ * `GET /models` is key-guarded, so a key-only edit — the common case of
295
+ * pasting a fresh key — must re-read the directory too. When only the key
296
+ * moves the live entries are kept and merely re-fetched; when the root
297
+ * moves they are dropped, because they describe a different gateway.
298
+ * @param baseUrl - the versioned API root ('' = unconfigured).
299
+ * @param apiKey - the gateway credential sent as a Bearer header.
300
+ */
301
+ setEndpoint(baseUrl, apiKey) {
280
302
  const root = baseUrl.replace(/\/+$/, "");
281
- if (root === this.#baseUrl) return;
303
+ const key = apiKey;
304
+ if (root === this.#baseUrl && key === this.#apiKey) return;
305
+ const rootChanged = root !== this.#baseUrl;
282
306
  this.#baseUrl = root;
283
- this.#entries = /* @__PURE__ */ new Map();
284
- this.#updatedAt = 0;
285
- this.#lastError = root === "" ? "magpie gateway baseUrl is not configured \u2014 set it on the Magpie settings page" : "";
307
+ this.#apiKey = key;
308
+ if (rootChanged) {
309
+ this.#entries = /* @__PURE__ */ new Map();
310
+ this.#updatedAt = 0;
311
+ }
312
+ this.#lastError = root === "" ? "magpie gateway API URL or key is not configured \u2014 set both on the Magpie settings page" : "";
286
313
  this.#announce();
287
314
  }
315
+ /** Point the refresh loop at another API root (keeps the current key). */
316
+ setBaseUrl(baseUrl) {
317
+ this.setEndpoint(baseUrl, this.#apiKey);
318
+ }
288
319
  get baseUrl() {
289
320
  return this.#baseUrl;
290
321
  }
322
+ get apiKey() {
323
+ return this.#apiKey;
324
+ }
291
325
  decision(model) {
292
326
  if (!this.configured) return { allowed: false, source: "unconfigured", known: false };
293
327
  if (this.#hidden.has(model)) return { allowed: false, source: "hidden_by_settings", known: true };
@@ -895,7 +929,7 @@ async function* withStallTimeout(events, timeouts, options = {}) {
895
929
 
896
930
  // src/adapter/magpie-adapter.ts
897
931
  var PROVIDER_ID = "dsh-magpie-connect";
898
- var DEFAULT_DISPLAY_NAME = "magpie";
932
+ var DEFAULT_DISPLAY_NAME = "Magpie";
899
933
  var DEFAULT_CONTEXT_WINDOW = 262144;
900
934
  var DEFAULT_MAX_TOKENS = 32768;
901
935
  var DEFAULT_API_KEY = "not-needed";
@@ -1070,7 +1104,7 @@ var MagpieAdapter = class {
1070
1104
  const raw = this.#runtime?.baseUrl() ?? this.#fallbackBaseUrl;
1071
1105
  const baseUrl = raw.replace(/\/+$/, "");
1072
1106
  if (baseUrl === "") {
1073
- throw new Error("dsh-magpie-connect: Magpie gateway API URL is not configured \u2014 open Settings \u2192 Magpie and set it");
1107
+ throw new Error("dsh-magpie-connect: Magpie gateway API URL or key is not configured \u2014 open Settings \u2192 Magpie and fill in both");
1074
1108
  }
1075
1109
  return baseUrl;
1076
1110
  }
@@ -1150,8 +1184,8 @@ import { homedir } from "node:os";
1150
1184
  import { join as join2 } from "node:path";
1151
1185
  var defaults = {
1152
1186
  providerId: "dsh-magpie-connect",
1153
- displayName: "magpie",
1154
- baseUrl: "",
1187
+ displayName: "Magpie",
1188
+ baseUrl: "http://127.0.0.1:3425/v1",
1155
1189
  apiKey: "",
1156
1190
  refreshSeconds: 300,
1157
1191
  maxRetries: 2,
@@ -1292,6 +1326,7 @@ var SETTINGS_API = "/api/magpie-settings";
1292
1326
  var MODELS_API = "/api/magpie-models";
1293
1327
  var TEST_API = "/api/magpie-test";
1294
1328
  var DISCOVER_API = "/api/magpie-discover";
1329
+ var SAVE_REFRESH_WAIT_MS = 5e3;
1295
1330
  function responsesOnlyFor(nativeEndpoints) {
1296
1331
  if (nativeEndpoints.length === 0) return false;
1297
1332
  const hasCompletions = nativeEndpoints.some((endpoint) => endpoint.includes("/chat/completions"));
@@ -1368,6 +1403,12 @@ async function handleSettingsPost(request) {
1368
1403
  }
1369
1404
  const saved = await backend.store.save(normalized);
1370
1405
  backend.applyPageSettings();
1406
+ if (normalized.baseUrl !== void 0 || normalized.apiKey !== void 0) {
1407
+ await Promise.race([
1408
+ backend.catalog.refreshOnce(),
1409
+ new Promise((resolve) => setTimeout(resolve, SAVE_REFRESH_WAIT_MS))
1410
+ ]);
1411
+ }
1371
1412
  const endpoint = resolveEffectiveEndpoint(backend.patchEndpoint, saved);
1372
1413
  return routeOk({ baseUrl: endpoint.baseUrl, apiKey: endpoint.apiKey, hiddenModels: saved.hiddenModels ?? [] });
1373
1414
  } catch (error) {
@@ -1537,6 +1578,7 @@ function apply(ctx, config = {}) {
1537
1578
  };
1538
1579
  const catalog = new ModelCatalog({
1539
1580
  baseUrl: effective().baseUrl,
1581
+ apiKey: effective().apiKey,
1540
1582
  refreshSeconds: cfg.refreshSeconds,
1541
1583
  cachePath: defaultCachePath(dataDir),
1542
1584
  onRefresh: (status, lastError) => {
@@ -1548,15 +1590,8 @@ function apply(ctx, config = {}) {
1548
1590
  const applyPageSettings = () => {
1549
1591
  const page = store.get();
1550
1592
  const endpoint = resolveEffectiveEndpoint(patchEndpoint, page);
1551
- const catalogBaseUrl = isConfigured(endpoint) ? endpoint.baseUrl.replace(/\/+$/, "") : "";
1552
- if (catalog.baseUrl !== catalogBaseUrl) {
1553
- catalog.setBaseUrl(catalogBaseUrl);
1554
- if (catalogBaseUrl !== "") {
1555
- void catalog.refreshOnce().catch((err) => {
1556
- logger.warn(`dsh-magpie-connect: catalog refresh after endpoint change failed: ${err instanceof Error ? err.message : String(err)}`);
1557
- });
1558
- }
1559
- }
1593
+ const configured = isConfigured(endpoint);
1594
+ catalog.setEndpoint(configured ? endpoint.baseUrl.replace(/\/+$/, "") : "", configured ? endpoint.apiKey : "");
1560
1595
  catalog.setHidden(page.hiddenModels ?? []);
1561
1596
  };
1562
1597
  const adapter = new MagpieAdapter(catalog, {