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 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
+ [![npm](https://img.shields.io/npm/v/dsh-endpoint)](https://www.npmjs.com/package/dsh-endpoint)
4
+ [![license](https://img.shields.io/npm/l/dsh-endpoint)](LICENSE)
5
+ [![ci](https://github.com/wangzhongyang/dsh-endpoint/actions/workflows/ci.yml/badge.svg)](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
+ [![npm](https://img.shields.io/npm/v/dsh-endpoint)](https://www.npmjs.com/package/dsh-endpoint)
4
+ [![license](https://img.shields.io/npm/l/dsh-endpoint)](LICENSE)
5
+ [![ci](https://github.com/wangzhongyang/dsh-endpoint/actions/workflows/ci.yml/badge.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-endpoint",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "OpenAI-compatible HTTP endpoint that forwards model calls through a running DeepSeek Harness",
5
5
  "keywords": [
6
6
  "dsh",
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)
@@ -18,7 +18,7 @@ const TEXT_TYPES = [
18
18
  'application/sql',
19
19
  ]
20
20
 
21
- const TEXT_MAX_BYTES = 512 * 1024
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
- // Guard before decoding: base64 expands by ~4/3.
48
- if (payload.length > 64 * 1024 * 1024) throw badRequest(`${param} is too large`, param)
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 truncated = data.length > TEXT_MAX_BYTES
92
- const text = data.subarray(0, TEXT_MAX_BYTES).toString('utf8')
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, { saveImages: deps.saveImages })
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,
package/src/version.js ADDED
@@ -0,0 +1,6 @@
1
+ import { createRequire } from 'node:module'
2
+
3
+ const require = createRequire(import.meta.url)
4
+
5
+ /** The package version, read once from package.json. */
6
+ export const VERSION = require('../package.json').version