dsh-endpoint 1.0.0 → 1.0.1
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 +6 -0
- package/README.md +19 -0
- package/README.zh.md +19 -0
- package/package.json +1 -1
- package/src/client.js +3 -0
- package/src/config.js +6 -0
- package/src/openai/media.js +9 -5
- package/src/server.js +7 -1
- package/src/version.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.1] - 2026-11-02
|
|
4
|
+
|
|
5
|
+
- `/healthz` and the settings page report the running version.
|
|
6
|
+
- `limits.maxTextFileBytes` (default 1 MiB) controls how much of one inline text
|
|
7
|
+
file is inlined; the request body cap remains the hard ceiling.
|
|
8
|
+
|
|
3
9
|
## [1.0.0] - 2026-11-02
|
|
4
10
|
|
|
5
11
|
First public release.
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# dsh-endpoint
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-endpoint)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/wangzhongyang/dsh-endpoint/actions/workflows/ci.yml)
|
|
6
|
+
|
|
3
7
|
Expose the model providers and models already configured in DeepSeek Harness as an
|
|
4
8
|
OpenAI-compatible HTTP endpoint.
|
|
5
9
|
|
|
@@ -120,6 +124,21 @@ Inline base64 only — the endpoint never fetches remote URLs on a client's beha
|
|
|
120
124
|
Attachments live in DSH's attachment store, so the mirrored session keeps a compact reference
|
|
121
125
|
instead of megabytes of base64.
|
|
122
126
|
|
|
127
|
+
### Limits
|
|
128
|
+
|
|
129
|
+
| Limit | Value | Source |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Request body | 8 MiB | `limits.maxBodyBytes`, a plugin default |
|
|
132
|
+
| One inline text file | 1 MiB, truncated with a marker | `limits.maxTextFileBytes`, a plugin default |
|
|
133
|
+
| One image | 20 MiB | DSH attachment service (not this plugin) |
|
|
134
|
+
| Images per message | 20 | DSH attachment service |
|
|
135
|
+
| Total image bytes per message | 200 MiB | DSH attachment service |
|
|
136
|
+
| Image pixels / longest side | 64 MP / 8192 px | DSH attachment service |
|
|
137
|
+
|
|
138
|
+
The text-file cap is a prompt-size guard, not a protocol limit: the body is already
|
|
139
|
+
bounded by `maxBodyBytes`, and a file near the cap is far past most models' context
|
|
140
|
+
windows. Truncation is announced in the inlined text, so the model knows it is partial.
|
|
141
|
+
|
|
123
142
|
**Ignored on purpose:** every other unknown field (`top_p`, `seed`, `service_tier`,
|
|
124
143
|
`metadata`, …). Real clients send fields this build does not model; rejecting them would
|
|
125
144
|
break more than it protects, so they are logged and dropped rather than silently applied.
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# dsh-endpoint
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-endpoint)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/wangzhongyang/dsh-endpoint/actions/workflows/ci.yml)
|
|
6
|
+
|
|
3
7
|
把 DeepSeek Harness 里已经配置好的模型供应商和模型,暴露成一个 OpenAI 兼容的 HTTP 端点。
|
|
4
8
|
|
|
5
9
|
端点只负责转发:它不持有任何 provider 凭据,也不直接访问三方 API。每一次模型调用都走
|
|
@@ -108,6 +112,21 @@ dsh plugin --profile web add /absolute/path/to/dsh-endpoint
|
|
|
108
112
|
|
|
109
113
|
附件存放在 DSH 的附件库里,所以镜像会话只保留一个紧凑引用,而不是几 MB 的 base64。
|
|
110
114
|
|
|
115
|
+
### 限制
|
|
116
|
+
|
|
117
|
+
| 限制 | 值 | 来源 |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| 请求体 | 8 MiB | 插件默认值 `limits.maxBodyBytes` |
|
|
120
|
+
| 单个内联文本文件 | 1 MiB,超出会截断并标注 | 插件默认值 `limits.maxTextFileBytes` |
|
|
121
|
+
| 单张图片 | 20 MiB | DSH 附件服务(不是本插件) |
|
|
122
|
+
| 每条消息图片数 | 20 | DSH 附件服务 |
|
|
123
|
+
| 每条消息图片总字节 | 200 MiB | DSH 附件服务 |
|
|
124
|
+
| 图片像素 / 最长边 | 64 MP / 8192 px | DSH 附件服务 |
|
|
125
|
+
|
|
126
|
+
文本文件这个上限是**提示词体积保护**,不是协议限制:整个请求体已经被 `maxBodyBytes` 卡住,
|
|
127
|
+
而接近这个上限的文件早就超出多数模型的上下文窗口了。截断会在内联文本里明确写出来,模型知道
|
|
128
|
+
自己拿到的是片段。
|
|
129
|
+
|
|
111
130
|
**故意忽略**:其余未知字段(`top_p`、`seed`、`service_tier`、`metadata` …)。真实客户端
|
|
112
131
|
会发一堆本版本没有建模的字段,严格拒绝会破坏更多东西,所以记录日志后丢弃,而不是假装生效。
|
|
113
132
|
|
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -17,6 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
var DICT = {
|
|
18
18
|
zh: {
|
|
19
19
|
nav: '模型端点',
|
|
20
|
+
version: '版本',
|
|
20
21
|
status: '状态',
|
|
21
22
|
address: '运行地址',
|
|
22
23
|
providers: 'Provider',
|
|
@@ -56,6 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
56
57
|
},
|
|
57
58
|
en: {
|
|
58
59
|
nav: 'Model Endpoint',
|
|
60
|
+
version: 'Version',
|
|
59
61
|
status: 'Status',
|
|
60
62
|
address: 'Address',
|
|
61
63
|
providers: 'Providers',
|
|
@@ -331,6 +333,7 @@ window.__ModuleLoader__.load({
|
|
|
331
333
|
? [h('div', { key: 'loading', style: S.hint }, t('loading'))]
|
|
332
334
|
: [
|
|
333
335
|
h(Field, { key: 'url', label: t('address') }, h('div', { style: S.mono }, status.url)),
|
|
336
|
+
h(Field, { key: 'version', label: t('version') }, h('div', { style: S.hint }, status.version || '—')),
|
|
334
337
|
h(
|
|
335
338
|
Field,
|
|
336
339
|
{ key: 'providers', label: t('providers') },
|
package/src/config.js
CHANGED
|
@@ -60,6 +60,12 @@ export const Config = z.object({
|
|
|
60
60
|
.natural()
|
|
61
61
|
.default(8 * 1024 * 1024)
|
|
62
62
|
.description('Reject request bodies larger than this.'),
|
|
63
|
+
maxTextFileBytes: z
|
|
64
|
+
.natural()
|
|
65
|
+
.default(1024 * 1024)
|
|
66
|
+
.description(
|
|
67
|
+
'How much of one inline text file is inlined into the prompt. The request body cap is the hard ceiling; truncation is marked in the text.',
|
|
68
|
+
),
|
|
63
69
|
maxOutputTokens: z
|
|
64
70
|
.natural()
|
|
65
71
|
.default(32768)
|
package/src/openai/media.js
CHANGED
|
@@ -18,7 +18,7 @@ const TEXT_TYPES = [
|
|
|
18
18
|
'application/sql',
|
|
19
19
|
]
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const DEFAULT_TEXT_MAX_BYTES = 1024 * 1024
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Parse one `data:` URL. Remote URLs are rejected on purpose: the endpoint
|
|
@@ -44,8 +44,8 @@ export function parseDataUrl(url, param) {
|
|
|
44
44
|
const payload = match[3] ?? ''
|
|
45
45
|
let data
|
|
46
46
|
if (/;base64/i.test(parameters)) {
|
|
47
|
-
//
|
|
48
|
-
|
|
47
|
+
// The request body cap is the real gate; by the time we get here the whole
|
|
48
|
+
// body is already bounded by `limits.maxBodyBytes`.
|
|
49
49
|
data = Buffer.from(payload, 'base64')
|
|
50
50
|
} else {
|
|
51
51
|
data = Buffer.from(decodeURIComponent(payload), 'utf8')
|
|
@@ -88,8 +88,12 @@ export async function decodeInlineFile({ url, name, param }, options = {}) {
|
|
|
88
88
|
return { type: 'image', attachment: ref }
|
|
89
89
|
}
|
|
90
90
|
if (isTextMediaType(mediaType) && options.allowText === true) {
|
|
91
|
-
const
|
|
92
|
-
|
|
91
|
+
const limit =
|
|
92
|
+
Number.isFinite(options.maxTextBytes) && options.maxTextBytes > 0
|
|
93
|
+
? Math.floor(options.maxTextBytes)
|
|
94
|
+
: DEFAULT_TEXT_MAX_BYTES
|
|
95
|
+
const truncated = data.length > limit
|
|
96
|
+
const text = data.subarray(0, limit).toString('utf8')
|
|
93
97
|
const header = `[file${name ? `: ${name}` : ''}${truncated ? ', truncated' : ''}]\n`
|
|
94
98
|
return { type: 'text', text: header + text }
|
|
95
99
|
}
|
package/src/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { mergeConfig, readOverlay, registerAdminRoutes } from './admin.js'
|
|
6
|
+
import { VERSION } from './version.js'
|
|
6
7
|
import { ModelCatalog } from './dsh/catalog.js'
|
|
7
8
|
import { SessionMirror, toolResultEvent, userMessageEvent } from './mirror.js'
|
|
8
9
|
import { MappingStore } from './session/registry.js'
|
|
@@ -43,6 +44,7 @@ export function createHandler(deps) {
|
|
|
43
44
|
return sendJson(res, 200, {
|
|
44
45
|
ok: true,
|
|
45
46
|
object: 'dsh-endpoint.health',
|
|
47
|
+
version: VERSION,
|
|
46
48
|
providers: deps.catalog.providers().map((provider) => provider.id),
|
|
47
49
|
})
|
|
48
50
|
}
|
|
@@ -85,7 +87,10 @@ async function handleChat(req, res, body, deps, requestId) {
|
|
|
85
87
|
|
|
86
88
|
const route = catalog.resolve(body.model)
|
|
87
89
|
const wantsStream = body.stream === true
|
|
88
|
-
const messages = await toDshMessages(body.messages, route, {
|
|
90
|
+
const messages = await toDshMessages(body.messages, route, {
|
|
91
|
+
saveImages: deps.saveImages,
|
|
92
|
+
maxTextBytes: config.limits.maxTextFileBytes,
|
|
93
|
+
})
|
|
89
94
|
const tools = toDshTools(body.tools)
|
|
90
95
|
|
|
91
96
|
const identity =
|
|
@@ -519,6 +524,7 @@ export async function startEndpoint(ctx, config, options) {
|
|
|
519
524
|
catalog.defaultModel = next.defaultModel ?? ''
|
|
520
525
|
},
|
|
521
526
|
getState: async () => ({
|
|
527
|
+
version: VERSION,
|
|
522
528
|
url: `http://${formatHost(effective.host)}:${current.port}`,
|
|
523
529
|
host: effective.host,
|
|
524
530
|
port: current.port,
|