dsh-endpoint 0.0.0-stage → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2026-11-02
4
+
5
+ First public release.
6
+
7
+ - OpenAI-compatible `POST /v1/chat/completions` (streaming and non-streaming) and
8
+ `GET /v1/models`, every call forwarded through `ctx.llm.stream()`.
9
+ - Tool calls pass through both ways; inline images and text files are supported.
10
+ - Stable conversation identity and read-only session mirroring into DSH.
11
+ - Settings page (Chinese/English) with in-place listener rebind.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wangzhongyang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # dsh-endpoint
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Expose the model providers and models already configured in DeepSeek Harness as an
4
+ OpenAI-compatible HTTP endpoint.
5
+
6
+ The endpoint only forwards. It holds no provider credentials and never talks to a
7
+ provider directly: every model call goes through `ctx.llm.stream()`, so DSH's provider
8
+ adapters (credentials, protocol differences, retries, image handling, token usage) keep
9
+ owning the real request.
10
+
11
+ [中文](README.zh.md)
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ OpenAI-compatible client
17
+ │ POST /v1/chat/completions
18
+ ▼
19
+ ┌────────────────────── DSH host process ───────────────────────┐
20
+ │ dsh-endpoint (Cordis plugin) │
21
+ │ HTTP listener → OpenAI mapping → SessionMirror → ctx.llm │
22
+ │ │ │
23
+ │ dsh-llm-pi-ai / dsh-llm-deepseek-* │
24
+ └──────────────────────────────────────────┼─────────────────────┘
25
+ ▼
26
+ provider API
27
+ ```
28
+
29
+ - **Plugin only.** No second process, no second credential store.
30
+ - **Sessions are real.** Each conversation is mirrored into a DSH session, so it shows
31
+ up in the sidebar and can be inspected.
32
+ - **Read-only mirrors.** The mirrored session holds the persistence write handle, so the
33
+ GUI refuses to resume it (`already owned by an active write handle`).
34
+
35
+ ## Install
36
+
37
+ Requires DeepSeek Harness Desktop (web profile) 0.2.1-rc.x.
38
+
39
+ From npm:
40
+
41
+ ```sh
42
+ dsh plugin --profile web add dsh-endpoint
43
+ ```
44
+
45
+ From a checkout (useful while developing):
46
+
47
+ ```sh
48
+ dsh plugin --profile web add /absolute/path/to/dsh-endpoint
49
+ ```
50
+
51
+ In the Desktop app you can also use **Settings → Plugins** and install it from the
52
+ market once the package is listed.
53
+
54
+ > **No authentication, no rate limiting.** The endpoint spends your configured
55
+ > model quota for anyone who can reach it. Keep it on `127.0.0.1` unless you put
56
+ > it behind a proxy you control, and disable the plugin to switch it off.
57
+
58
+ Restart the app after installing: host plugins are ESM modules, and reinstalling does
59
+ not replace the module already loaded in the running process.
60
+
61
+ ## Settings page
62
+
63
+ **Settings → Model Endpoint** (a custom section, same level as plugin pages).
64
+
65
+ ![dsh-endpoint settings page](docs/settings.png)
66
+
67
+ | Field | Meaning |
68
+ |---|---|
69
+ | Listen address | `127.0.0.1` (local only) or `0.0.0.0` (all interfaces) |
70
+ | Port | default `43130`; the GUI owns `43129` |
71
+ | Default model | used when a request omits `model` |
72
+ | Model aliases | JSON map, e.g. `{"gpt-4o": "opencode-go/deepseek-v4.1-flash"}` |
73
+ | Session mirror | write each API conversation into a DSH session |
74
+ | Mapping retention | days an external conversation id keeps its session after last use |
75
+ | Workspace directory | workspace that groups mirrored sessions |
76
+
77
+ Saving applies immediately: **changing the listen address or port rebinds the listener
78
+ in place** (the old listener closes, the new one binds, and a failed bind rolls back to
79
+ the previous address). Switching `127.0.0.1 ↔ 0.0.0.0` takes the same path. Toggling the
80
+ mirror or changing the workspace directory still needs a plugin reload, because those are
81
+ built at startup.
82
+
83
+ In-flight streaming responses are dropped when the listener moves.
84
+
85
+ The page reads and writes two loopback-only routes: `GET /dsh-endpoint/state` and
86
+ `GET|PUT /dsh-endpoint/config`. Saves go to the user layer
87
+ `$DSH_HOME/dsh-endpoint/config.json`, which is merged over the plugin config at startup.
88
+
89
+ ## HTTP API
90
+
91
+ | Method | Path | Notes |
92
+ |---|---|---|
93
+ | `GET` | `/healthz` | liveness + registered providers |
94
+ | `GET` | `/v1/models` | `provider/model` ids plus aliases |
95
+ | `GET` | `/v1/models/{id}` | one model |
96
+ | `POST` | `/v1/chat/completions` | streaming and non-streaming |
97
+
98
+ Model ids are `provider/model`, e.g. `opencode-go/deepseek-v4.1-flash`.
99
+
100
+ **Supported:** `messages` (system/user/assistant/tool), `tools` and `tool_calls`
101
+ pass-through, `temperature`, `max_tokens` / `max_completion_tokens`, `stream`,
102
+ `stream_options.include_usage`, `stop` (truncated endpoint-side because not every
103
+ adapter accepts it).
104
+
105
+ **Rejected explicitly:** `n > 1`, `tool_choice` other than `"auto"`, remote (`http(s)`) media
106
+ URLs, `file_id` references, and binary documents (PDF/DOCX).
107
+
108
+ ## Attachments
109
+
110
+ Inline base64 only — the endpoint never fetches remote URLs on a client's behalf.
111
+
112
+ | Input | Handling |
113
+ |---|---|
114
+ | `image_url` with `data:image/{png,jpeg,webp,gif};base64,…` | decoded → `ctx.attachments.saveImages` → `ImageBlock`; the adapter resizes per route |
115
+ | `image_url` with `http(s)://…` | 400: send a data URL instead |
116
+ | `file` part with `file_data` of `text/*`, JSON, YAML, XML, JS, SQL | inlined as a `text` block (512 KiB cap, filename preserved) |
117
+ | `file` part with a binary document (PDF, DOCX, …) | 400: text extraction is not implemented |
118
+ | `file` part with `file_id` | 400: the Files API is not implemented |
119
+
120
+ Attachments live in DSH's attachment store, so the mirrored session keeps a compact reference
121
+ instead of megabytes of base64.
122
+
123
+ **Ignored on purpose:** every other unknown field (`top_p`, `seed`, `service_tier`,
124
+ `metadata`, …). Real clients send fields this build does not model; rejecting them would
125
+ break more than it protects, so they are logged and dropped rather than silently applied.
126
+
127
+ ## Session identity
128
+
129
+ Resolution order, first hit wins:
130
+
131
+ 1. `x-dsh-session-id` (this plugin's own header)
132
+ 2. known agent headers: `x-session-id`, `x-conversation-id`, `x-chat-id`, `x-thread-id`,
133
+ `x-agent-session-id`, `x-claude-code-session-id`, `x-codex-session-id`
134
+ 3. body fields: `session_id`, `conversation_id`, `chat_id`, `thread_id`,
135
+ `metadata.session_id`, `metadata.conversation_id`, `user`
136
+ 4. content fingerprint: `sha256(model + first user message)` — deterministic across
137
+ requests and restarts
138
+ 5. only when there is no user message at all, a fresh id is minted
139
+
140
+ `x-request-id`, `x-correlation-id` and `x-trace-id` are never treated as session ids.
141
+
142
+ Every response echoes `x-dsh-session-id`. Clients may send it back to pin the
143
+ conversation; clients that send nothing still get a stable id from step 4.
144
+
145
+ Only new user/tool messages are appended to the mirrored session (assistant messages are
146
+ always the ones this endpoint produced), so logs grow per turn instead of quadratically.
147
+ If the client's user history diverges from what was recorded, a new DSH session is
148
+ started and the divergence is logged.
149
+
150
+ ## Supported clients
151
+
152
+ Any client that can point an OpenAI-compatible provider at a custom base URL:
153
+
154
+ ```bash
155
+ export OPENAI_BASE_URL=http://127.0.0.1:43130/v1
156
+ export OPENAI_API_KEY=anything # there is no auth; any placeholder works
157
+ ```
158
+
159
+ | Client | How to configure | Status |
160
+ |---|---|---|
161
+ | `curl` / any HTTP client | POST the body directly | verified |
162
+ | OpenAI Python SDK | `OpenAI(base_url="http://127.0.0.1:43130/v1", api_key="any")` | request shape verified |
163
+ | OpenAI Node SDK | `new OpenAI({ baseURL: "http://127.0.0.1:43130/v1", apiKey: "any" })` | request shape verified |
164
+ | Cline (VS Code) | Provider: OpenAI Compatible → Base URL + any key | expected; not yet end-to-end tested |
165
+ | Roo Code (VS Code) | Provider: OpenAI Compatible | expected; not yet end-to-end tested |
166
+ | Continue (VS Code / JetBrains) | `provider: openai`, `apiBase: http://127.0.0.1:43130/v1` | expected; not yet end-to-end tested |
167
+ | OpenCode (CLI) | custom provider in `opencode.json`, see below | **verified**, tool loop included |
168
+ | Cherry Studio | Add provider → OpenAI → API Host | expected |
169
+ | Chatbox | Model provider → OpenAI API → API Host | expected |
170
+ | LobeChat | OpenAI provider → proxy/base URL | expected |
171
+ | Open WebUI | Connections → OpenAI → Base URL | expected |
172
+
173
+ ### OpenCode
174
+
175
+ Model ids contain a slash, which breaks `provider/model` parsing on the client, so add a
176
+ slash-free alias first (Settings → Model Endpoint → Model aliases), e.g.
177
+ `{"flash": "opencode-go/deepseek-v4.1-flash"}`. Then point a project `opencode.json` at the
178
+ endpoint:
179
+
180
+ ```json
181
+ {
182
+ "provider": {
183
+ "dsh": {
184
+ "name": "DSH Endpoint",
185
+ "npm": "@ai-sdk/openai-compatible",
186
+ "options": { "baseURL": "http://127.0.0.1:43130/v1", "apiKey": "anything" },
187
+ "models": { "flash": { "name": "DeepSeek via DSH" } }
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ ```sh
194
+ opencode run -m dsh/flash "list the files here and tell me which is largest"
195
+ ```
196
+
197
+ A ready-to-copy file lives at [`examples/opencode.json`](examples/opencode.json).
198
+
199
+ OpenCode sends `x-session-id`, which the endpoint already treats as the conversation key,
200
+ so a whole multi-step tool loop lands in one mirrored session.
201
+
202
+ Tool calls are forwarded both ways (`tools` in, `tool_calls` out); OpenCode's shell/read
203
+ tool loop has been verified end to end.
204
+
205
+ ## Limitations
206
+
207
+ - No authentication and no rate limiting. The stop switch is disabling the plugin. Keep
208
+ it on `127.0.0.1`; binding `0.0.0.0` lets anyone who can reach the port spend your quota.
209
+ - Inline images and text files only; remote media URLs and binary documents are refused.
210
+ - `n > 1`, `tool_choice` other than `auto`, `logprobs`, `top_p`, `seed` are not applied.
211
+ - Mirrored sessions cannot be continued in the GUI by design.
212
+ - Pinned to DSH internal packages (`0.2.1-rc.x`); API changes there can break this plugin.
213
+
214
+ ## Development
215
+
216
+ ```sh
217
+ pnpm install
218
+ node --test # 47 cases, no DSH required
219
+ node spike/mock-provider.mjs 44001 # local fake provider
220
+ ```
221
+
222
+ `spike/` also contains the P0 harness that creates a session in a throwaway `DSH_HOME`,
223
+ persists it, restores it, and checks that GUI takeover is refused (`spike/run.sh`).
224
+
225
+ ## Verified / not verified
226
+
227
+ Verified on a real Desktop install:
228
+
229
+ - `/v1/models` (31 models across `opencode-go`, `deepseek-official`, `deepseek-account`)
230
+ - non-streaming and streaming chat with usage
231
+ - the same conversation twice → the same `x-dsh-session-id`
232
+ - mirrored session persists with `turn/start → step/start → user → assistant → step/end →
233
+ turn/end`, second turn appends only the new user message
234
+ - mirrored sessions appear in the sidebar workspace
235
+ - `agentLoop.resume` refused while the write handle is held
236
+ - settings page: section registered, config read/write round-trip
237
+ - live listener rebind (`43130 → 43131 → 43130`) without restarting the app
238
+ - inline image input (a 1152×883 screenshot was described correctly)
239
+ - OpenCode driving a full tool loop (two shell calls in one turn, mirrored with tool results)
240
+
241
+ Not verified yet:
242
+
243
+ - other IDE agents (Cline, Roo Code, Continue)
244
+ - multiple concurrent conversations against one provider quota
package/README.zh.md ADDED
@@ -0,0 +1,225 @@
1
+ # dsh-endpoint
2
+
3
+ 把 DeepSeek Harness 里已经配置好的模型供应商和模型,暴露成一个 OpenAI 兼容的 HTTP 端点。
4
+
5
+ 端点只负责转发:它不持有任何 provider 凭据,也不直接访问三方 API。每一次模型调用都走
6
+ `ctx.llm.stream()`,凭据、协议差异、重试、图片处理、用量统计仍然由 DSH 的 provider 适配器负责。
7
+
8
+ [English](README.md)
9
+
10
+ ## 架构
11
+
12
+ ```
13
+ OpenAI 兼容客户端
14
+ │ POST /v1/chat/completions
15
+ ▼
16
+ ┌────────────────────── DSH Host 进程 ──────────────────────────┐
17
+ │ dsh-endpoint(Cordis 插件) │
18
+ │ HTTP listener → OpenAI 映射 → SessionMirror → ctx.llm │
19
+ │ │ │
20
+ │ dsh-llm-pi-ai / dsh-llm-deepseek-* │
21
+ └──────────────────────────────────────────┼─────────────────────┘
22
+ ▼
23
+ 三方模型 API
24
+ ```
25
+
26
+ - **只做插件**:没有第二个进程,也没有第二套凭据。
27
+ - **会话是真的**:每段对话镜像成一条 DSH 会话,侧栏可见、可查看。
28
+ - **镜像只读**:镜像会话持有持久化写句柄,GUI 里点「继续」会被拒
29
+ (`already owned by an active write handle`)。
30
+
31
+ ## 安装
32
+
33
+ 需要 DeepSeek Harness Desktop(web profile)0.2.1-rc.x。
34
+
35
+ 从 npm 安装:
36
+
37
+ ```sh
38
+ dsh plugin --profile web add dsh-endpoint
39
+ ```
40
+
41
+ 从源码目录安装(开发时用):
42
+
43
+ ```sh
44
+ dsh plugin --profile web add /absolute/path/to/dsh-endpoint
45
+ ```
46
+
47
+ 在 Desktop 里也可以走 **设置 → 插件**,等包进入市场后直接搜 `dsh-endpoint`。
48
+
49
+ > **没有鉴权、没有限流。** 谁能连上这个端口,就能花你配置的模型额度。除非前面有你自己
50
+ > 控制的反向代理,否则请保持在 `127.0.0.1`;停用插件就是关闭它。
51
+
52
+ 安装后要重启应用:宿主插件是 ESM 模块,重装只换磁盘快照,不换已经加载进进程的模块。
53
+
54
+ ## 设置页
55
+
56
+ **设置 → 模型端点**(自定义分区,和插件页同级)。
57
+
58
+ ![dsh-endpoint 设置页](docs/settings.png)
59
+
60
+ | 字段 | 含义 |
61
+ |---|---|
62
+ | 监听地址 | `127.0.0.1`(仅本机)或 `0.0.0.0`(所有网卡) |
63
+ | 端口 | 默认 `43130`;GUI 占用 `43129` |
64
+ | 默认模型 | 请求不带 `model` 时使用 |
65
+ | 模型别名 | JSON 映射,例如 `{"gpt-4o": "opencode-go/deepseek-v4.1-flash"}` |
66
+ | 会话镜像 | 把每段 API 对话写成一条 DSH 会话 |
67
+ | 映射保留 | 外部会话 ID 超过多少天没再用就解绑 |
68
+ | 工作区目录 | 镜像会话归属的 workspace |
69
+
70
+ 保存即时生效:**改监听地址或端口会就地重绑 listener**(先关旧的、再绑新的,绑定失败回滚到
71
+ 原地址)。`127.0.0.1 ↔ 0.0.0.0` 走同一条路径。切换镜像开关、改工作区目录仍然需要重载插件,
72
+ 因为它们在启动时构建。
73
+
74
+ 重绑时正在进行的流式响应会断开。
75
+
76
+ 页面通过两条 loopback 路由读写:`GET /dsh-endpoint/state`、`GET|PUT /dsh-endpoint/config`。
77
+ 保存写入用户层 `$DSH_HOME/dsh-endpoint/config.json`,启动时叠加在插件 config 之上。
78
+
79
+ ## HTTP 接口
80
+
81
+ | 方法 | 路径 | 说明 |
82
+ |---|---|---|
83
+ | `GET` | `/healthz` | 存活 + 已注册 provider |
84
+ | `GET` | `/v1/models` | `provider/model` 列表 + 别名 |
85
+ | `GET` | `/v1/models/{id}` | 单个模型 |
86
+ | `POST` | `/v1/chat/completions` | 流式与非流式 |
87
+
88
+ 模型名为 `provider/model`,例如 `opencode-go/deepseek-v4.1-flash`。
89
+
90
+ **支持**:`messages`(system/user/assistant/tool)、`tools` 与 `tool_calls` 透传、
91
+ `temperature`、`max_tokens` / `max_completion_tokens`、`stream`、
92
+ `stream_options.include_usage`、`stop`(端侧截断,因为不是所有适配器都接受它)。
93
+
94
+ **明确拒绝**:`n > 1`、`tool_choice` 非 `"auto"`、远程(`http(s)`)媒体 URL、`file_id`
95
+ 引用、二进制文档(PDF/DOCX)。
96
+
97
+ ## 附件
98
+
99
+ 只接受内联 base64——端点不会代替客户端去下载远程地址。
100
+
101
+ | 输入 | 处理 |
102
+ |---|---|
103
+ | `image_url` + `data:image/{png,jpeg,webp,gif};base64,…` | 解码 → `ctx.attachments.saveImages` → `ImageBlock`,由适配器按路由缩放 |
104
+ | `image_url` + `http(s)://…` | 400,要求改用 data URL |
105
+ | `file` 部件的 `file_data` 为 `text/*`、JSON、YAML、XML、JS、SQL | 直接内联成 `text` 块(上限 512 KiB,保留文件名) |
106
+ | `file` 部件为二进制文档(PDF、DOCX 等) | 400,未实现文本抽取 |
107
+ | `file` 部件用 `file_id` | 400,未实现 Files API |
108
+
109
+ 附件存放在 DSH 的附件库里,所以镜像会话只保留一个紧凑引用,而不是几 MB 的 base64。
110
+
111
+ **故意忽略**:其余未知字段(`top_p`、`seed`、`service_tier`、`metadata` …)。真实客户端
112
+ 会发一堆本版本没有建模的字段,严格拒绝会破坏更多东西,所以记录日志后丢弃,而不是假装生效。
113
+
114
+ ## 会话身份
115
+
116
+ 解析顺序,先命中先用:
117
+
118
+ 1. `x-dsh-session-id`(本插件自己的头)
119
+ 2. 已知 agent 会话头:`x-session-id`、`x-conversation-id`、`x-chat-id`、`x-thread-id`、
120
+ `x-agent-session-id`、`x-claude-code-session-id`、`x-codex-session-id`
121
+ 3. body 字段:`session_id`、`conversation_id`、`chat_id`、`thread_id`、
122
+ `metadata.session_id`、`metadata.conversation_id`、`user`
123
+ 4. 内容指纹:`sha256(model + 首条 user 消息)`——跨请求、跨重启都是确定值
124
+ 5. 只有在完全没有 user 消息时,才新生成
125
+
126
+ `x-request-id`、`x-correlation-id`、`x-trace-id` 永远不会被当成会话 ID。
127
+
128
+ 每个响应都回写 `x-dsh-session-id`。客户端回传即可锁定这段对话;什么都不发的客户端也能靠
129
+ 第 4 条拿到稳定 ID。
130
+
131
+ 镜像只追加新的 user / tool 消息(assistant 消息永远由本端点自己的流产生),所以日志按轮次
132
+ 线性增长,不会二次膨胀。如果客户端的 user 历史与记录的前缀不一致,会新开一条 DSH 会话并记日志。
133
+
134
+ ## 支持的客户端
135
+
136
+ 任何能把 OpenAI 兼容 provider 指向自定义 base URL 的客户端:
137
+
138
+ ```bash
139
+ export OPENAI_BASE_URL=http://127.0.0.1:43130/v1
140
+ export OPENAI_API_KEY=anything # 没有鉴权,随便填占位符
141
+ ```
142
+
143
+ | 客户端 | 配置方式 | 状态 |
144
+ |---|---|---|
145
+ | `curl` / 任意 HTTP 客户端 | 直接 POST | 已验证 |
146
+ | OpenAI Python SDK | `OpenAI(base_url=..., api_key="any")` | 请求形态已验证 |
147
+ | OpenAI Node SDK | `new OpenAI({ baseURL: ..., apiKey: "any" })` | 请求形态已验证 |
148
+ | Cline(VS Code) | Provider: OpenAI Compatible → Base URL + 任意 key | 理论上可用,未端到端验证 |
149
+ | Roo Code(VS Code) | Provider: OpenAI Compatible | 理论上可用,未端到端验证 |
150
+ | Continue(VS Code / JetBrains) | `provider: openai`、`apiBase: http://127.0.0.1:43130/v1` | 理论上可用,未端到端验证 |
151
+ | OpenCode(CLI) | 在 `opencode.json` 里加自定义 provider,见下 | **已验证**,含工具循环 |
152
+ | Cherry Studio | 添加供应商 → OpenAI → API Host | 理论上可用 |
153
+ | Chatbox | 模型供应商 → OpenAI API → API Host | 理论上可用 |
154
+ | LobeChat | OpenAI provider → 代理/base URL | 理论上可用 |
155
+ | Open WebUI | 连接 → OpenAI → Base URL | 理论上可用 |
156
+
157
+ ### OpenCode
158
+
159
+ 模型 id 里带斜杠,会破坏客户端的 `provider/model` 解析,所以先在设置页加一个无斜杠别名
160
+ (模型别名),例如 `{"flash": "opencode-go/deepseek-v4.1-flash"}`,再在项目里放一个
161
+ `opencode.json`:
162
+
163
+ ```json
164
+ {
165
+ "provider": {
166
+ "dsh": {
167
+ "name": "DSH Endpoint",
168
+ "npm": "@ai-sdk/openai-compatible",
169
+ "options": { "baseURL": "http://127.0.0.1:43130/v1", "apiKey": "anything" },
170
+ "models": { "flash": { "name": "DeepSeek via DSH" } }
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ ```sh
177
+ opencode run -m dsh/flash "列出当前目录的文件,告诉我哪个最大"
178
+ ```
179
+
180
+ 可直接复制的文件在 [`examples/opencode.json`](examples/opencode.json)。
181
+
182
+ OpenCode 会发 `x-session-id`,端点本来就把它当会话键,所以整个多步工具循环会落进同一条镜像会话。
183
+
184
+ 工具调用双向透传(进来 `tools`,出去 `tool_calls`);OpenCode 的 shell/read 工具循环已端到端验证。
185
+
186
+ ## 限制
187
+
188
+ - 没有鉴权、没有限流。停止开关就是停用插件。请保持在 `127.0.0.1`;绑 `0.0.0.0`
189
+ 意味着同网段任何人都能花你的额度。
190
+ - 只支持内联图片与文本文件;远程媒体 URL 和二进制文档会被拒绝。
191
+ - `n > 1`、`tool_choice` 非 `auto`、`logprobs`、`top_p`、`seed` 都不会生效。
192
+ - 镜像会话按设计不能在 GUI 里继续对话。
193
+ - 绑定 DSH 内部包(`0.2.1-rc.x`),那边接口变化可能导致本插件失效。
194
+
195
+ ## 开发
196
+
197
+ ```sh
198
+ pnpm install
199
+ node --test # 47 个用例,不需要 DSH
200
+ node spike/mock-provider.mjs 44001 # 本地假 provider
201
+ ```
202
+
203
+ `spike/` 里还有 P0 验证脚本:在临时 `DSH_HOME` 里创建会话、落盘、恢复,并验证 GUI 接管被拒
204
+ (`spike/run.sh`)。
205
+
206
+ ## 已验证 / 未验证
207
+
208
+ 在真实 Desktop 安装上验证过:
209
+
210
+ - `/v1/models`(`opencode-go`、`deepseek-official`、`deepseek-account` 共 31 个模型)
211
+ - 非流式与流式对话,含 usage
212
+ - 同一对话两次请求 → 同一个 `x-dsh-session-id`
213
+ - 镜像会话落盘为 `turn/start → step/start → user → assistant → step/end → turn/end`,
214
+ 第二轮只追加新的 user 消息
215
+ - 镜像会话出现在侧栏 workspace
216
+ - 持有写句柄时 `agentLoop.resume` 被拒
217
+ - 设置页:分区已注册、配置读写往返
218
+ - 在线重绑监听地址(`43130 → 43131 → 43130`),不用重启应用
219
+ - 内联图片(1152×883 截图被正确描述)
220
+ - OpenCode 的完整工具循环(一轮两个 shell 调用,连工具结果一起镜像)
221
+
222
+ 尚未验证:
223
+
224
+ - 其它 IDE agent(Cline、Roo Code、Continue)
225
+ - 同一 provider 额度下的多会话并发
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please do not open a public issue for security problems. Use GitHub's
6
+ [private vulnerability reporting](https://github.com/wangzhongyang/dsh-endpoint/security/advisories/new)
7
+ on this repository. Include the version, your platform, and reproduction steps;
8
+ you will get an acknowledgement within a few days.
9
+
10
+ ## Threat model
11
+
12
+ `dsh-endpoint` is a **local development tool**. Its security posture is deliberate
13
+ and documented rather than accidental:
14
+
15
+ - **No authentication, no rate limiting.** Anyone who can reach the listening port
16
+ can spend the model quota configured in your harness. This is why the default
17
+ bind address is `127.0.0.1` and why the plugin logs a warning when you switch to
18
+ `0.0.0.0`.
19
+ - **No provider credentials.** The plugin never reads or stores API keys. Every
20
+ call goes through `ctx.llm`, so credentials stay in DSH's credential store and
21
+ are resolved per request by the provider adapter.
22
+ - **Loopback-only management routes.** The settings page talks to
23
+ `/dsh-endpoint/state` and `/dsh-endpoint/config` on the GUI server, and both
24
+ routes reject non-loopback peers.
25
+ - **Remote media URLs are refused.** Only inline `data:` URLs are accepted, so the
26
+ endpoint cannot be used to make the host fetch arbitrary internal addresses
27
+ (SSRF).
28
+
29
+ If you need to expose the endpoint beyond your machine, put it behind a reverse
30
+ proxy that terminates TLS and enforces authentication, and keep the plugin's own
31
+ listener bound to `127.0.0.1`.
32
+
33
+ ## Supported versions
34
+
35
+ Only the latest released version receives fixes.
@@ -0,0 +1,9 @@
1
+ # Bundle layer for dsh-endpoint.
2
+ #
3
+ # This package is a *bundle*: list it in a profile's `dsh.profile.bundles` and
4
+ # this patch contributes the host plugin that serves the OpenAI-compatible
5
+ # endpoint. Every model call is executed by `ctx.llm`, so provider routing and
6
+ # credentials stay owned by whichever profile mounted the adapters.
7
+ - insert:
8
+ - id: dsh-endpoint
9
+ name: dsh-endpoint
package/docs/curl.png ADDED
Binary file
Binary file
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "provider": {
4
+ "dsh": {
5
+ "name": "DSH Endpoint",
6
+ "npm": "@ai-sdk/openai-compatible",
7
+ "options": {
8
+ "baseURL": "http://127.0.0.1:43130/v1",
9
+ "apiKey": "anything"
10
+ },
11
+ "models": {
12
+ "flash": {
13
+ "name": "DeepSeek V4.1 Flash (via DSH)"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
package/icon.svg ADDED
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128" role="img" aria-label="dsh-endpoint">
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#5b9dff"/>
5
+ <stop offset="1" stop-color="#2f6fe4"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="128" height="128" rx="28" fill="url(#g)"/>
9
+ <path d="M40 52h26a10 10 0 0 1 10 10v14" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round"/>
10
+ <path d="M88 76H62a10 10 0 0 1-10-10V52" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" opacity=".55"/>
11
+ <circle cx="88" cy="76" r="7" fill="#fff"/>
12
+ <circle cx="40" cy="52" r="7" fill="#fff"/>
13
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "dsh-endpoint",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "OpenAI-compatible HTTP endpoint that forwards model calls through a running DeepSeek Harness",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "dsh-plugin",
9
+ "openai-compatible",
10
+ "openai-api",
11
+ "llm-gateway",
12
+ "endpoint"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "wangzhongyang",
16
+ "homepage": "https://github.com/wangzhongyang/dsh-endpoint#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/wangzhongyang/dsh-endpoint.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/wangzhongyang/dsh-endpoint/issues"
23
+ },
24
+ "icon": "icon.svg",
25
+ "type": "module",
26
+ "main": "src/index.js",
27
+ "exports": {
28
+ ".": "./src/index.js",
29
+ "./package.json": "./package.json",
30
+ "./client": "./src/client.js"
31
+ },
32
+ "files": [
33
+ "src",
34
+ "cordis.patch.yml",
35
+ "icon.svg",
36
+ "examples",
37
+ "docs",
38
+ "README.md",
39
+ "README.zh.md",
40
+ "CHANGELOG.md",
41
+ "SECURITY.md"
42
+ ],
43
+ "engines": {
44
+ "node": ">=22.19.0",
45
+ "dsh": ">=0.2.1-rc.2"
46
+ },
47
+ "dsh": {
48
+ "bundle": {
49
+ "patch": "./cordis.patch.yml"
50
+ },
51
+ "client": {
52
+ "platform": "web"
53
+ },
54
+ "compatibility": {
55
+ "dshReleases": {
56
+ "0.2.1-rc.3": "compatible"
57
+ }
58
+ }
59
+ },
60
+ "dependencies": {
61
+ "@deepseek-ai/schemastery": "~3.18.5"
62
+ },
63
+ "scripts": {
64
+ "test": "node --test",
65
+ "prepublishOnly": "node --test"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }