@stackstackstack/dsh-llm-deepseek 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +114 -0
- package/README.zh.md +114 -0
- package/lib/index.js +781 -0
- package/lib/invariant.js +23 -0
- package/lib/types/adapter.d.ts +102 -0
- package/lib/types/index.d.ts +76 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/serialize.d.ts +34 -0
- package/lib/types/sse.d.ts +24 -0
- package/lib/types/translate.d.ts +36 -0
- package/lib/types/types.d.ts +150 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
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.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
|
5
|
+
README.md: a1a03357455ce84ff5dd427144985a616b785502
|
|
6
|
+
README.zh.md: 89546189070df85b101ede656e0ea2c5319c0dba
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @stackstackstack/dsh-llm-deepseek
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
DeepSeek chat-completions adapter for the harness LLM seam: direct `fetch` + SSE (framed by `eventsource-parser`) translating the official wire format (source of truth: the API docs — guides/thinking_mode, guides/tool_calls, api/create-chat-completion) into the `StreamChunk` protocol.
|
|
6
|
+
|
|
7
|
+
A second, library-backed implementation of the same seam exists in `@stackstackstack/dsh-llm-pi-ai`. This package owns the `deepseek-official` provider route — deliberately distinct from pi-ai's catalog name `deepseek`, so one composition can mount both DeepSeek paths side by side; registering another adapter for `deepseek-official` itself still throws `LlmError('DUPLICATE_ADAPTER')`.
|
|
8
|
+
|
|
9
|
+
The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire serialization, SSE parsing, and chunk translation helpers are not part of that root contract.
|
|
10
|
+
|
|
11
|
+
## Config
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
- id: llm-deepseek
|
|
15
|
+
name: '@stackstackstack/dsh-llm-deepseek'
|
|
16
|
+
config:
|
|
17
|
+
apiKeyEnv: DEEPSEEK_API_KEY # default; resolved per request via ctx.credentials, then the environment
|
|
18
|
+
baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
|
|
19
|
+
thinking: enabled # optional; provider default is enabled
|
|
20
|
+
reasoningEffort: high # optional; off | high | max — omitted ⇒ high
|
|
21
|
+
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
|
22
|
+
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
|
23
|
+
retryPolicy: # optional; omission uses bounded normal defaults
|
|
24
|
+
mode: always # normal | always
|
|
25
|
+
backoff:
|
|
26
|
+
initialDelayMs: 500
|
|
27
|
+
maxDelayMs: 10000
|
|
28
|
+
jitterRatio: 0.1
|
|
29
|
+
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
|
30
|
+
models: # optional; defaults to V4 Flash and V4 Pro
|
|
31
|
+
- id: deepseek-v4-flash
|
|
32
|
+
name: DeepSeek-V4-Flash
|
|
33
|
+
- id: private-reasoner
|
|
34
|
+
description: Company-hosted reasoning model
|
|
35
|
+
contextWindow: 512000
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as `DeepSeek-V4-Flash` and `deepseek-v4-pro` as `DeepSeek-V4-Pro`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id.
|
|
39
|
+
|
|
40
|
+
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. The adapter default is 1,000,000; pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
|
41
|
+
|
|
42
|
+
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. A catalog entry may carry its own `maxTokens`, which wins for that model; an entry without one, and any unlisted pass-through id, resolve to the profile value, so adding a per-model cap changes one model rather than the route. Exact-model resolution exposes the winner as `defaultMaxTokens`; `LlmRuntime` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
|
43
|
+
|
|
44
|
+
The same exact-model result exposes ordered `off`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `high` and `max` enable thinking and serialize as the official top-level `reasoning_effort`; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O.
|
|
45
|
+
|
|
46
|
+
`thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `high` or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults.
|
|
47
|
+
|
|
48
|
+
`streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. DeepSeek SSE comments rearm an outstanding read as transport activity but never become `StreamChunk` values or session-log events. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers the configured policy as provider metadata, and `dsh-llm-retry` separately executes it at durable agent-step boundaries.
|
|
49
|
+
|
|
50
|
+
## Dynamic configuration (settings + credentials)
|
|
51
|
+
|
|
52
|
+
Connection facts are not frozen at load. `resolveAdapterOptions` is the one explicit resolve step from raw config to validated facts, and the adapter re-reads them through a thunk **once per operation**: base URL, catalog, request defaults, and idle budget all take effect on the next request, while an in-flight stream keeps the facts it started with. Two optional seams feed that thunk:
|
|
53
|
+
|
|
54
|
+
- **`ctx.settings`** — the plugin registers the `llm-deepseek` namespace with this same `Config` schema and its `cordis.yml` entry as the composition `base`, so a `llm-deepseek:` section in the user settings document overrides any field without a restart. Without a mounted settings service the entry config alone drives the adapter, unchanged. A live settings snapshot that passes the schema but fails a beyond-schema bound (a duplicate catalog id, a broken thinking/effort pair) keeps the last good facts and logs the failure; the entry config itself still fails plugin load.
|
|
55
|
+
- **`ctx.credentials`** — the API key resolves per stream call, from the *same* resolved snapshot that supplies the endpoint. Configuration carries only `apiKeyEnv`, never a literal key: the reference resolves through the credential seam, and without a mounted seam through the trusted environment layers. Because credential facts travel with the connection facts, a settings snapshot the resolver rejects contributes neither its endpoint nor its key: the whole previous generation keeps serving. Every resolved key is format-checked before use, so a value no HTTP header can carry is refused with `LlmError('INVALID_CREDENTIAL')` naming the failing entry point — never any part of the key — instead of surfacing as an opaque `fetch` `TypeError`. A request with no key anywhere fails with `MISSING_CREDENTIAL` naming every configuration entry point, while the route stays registered and the catalog stays browsable — first-run onboarding is "browse models, store the key, prompt again", with no restart between.
|
|
56
|
+
|
|
57
|
+
The one registration-captured fact is the retry policy: when its resolved value changes, the plugin re-registers the route in place (same adapter instance, one synchronous section), so `ctx.llm.providerRetryPolicy('deepseek-official')` always reports the current policy.
|
|
58
|
+
|
|
59
|
+
The plugin also declares its route in the configurable-provider directory (`ctx.llm.listConfigurableProviders()`): provider `deepseek-official`, settings namespace `llm-deepseek`, empty settings path — the whole section is the profile. Configuration surfaces use that entry to offer this adapter alongside dormant pi-ai providers.
|
|
60
|
+
|
|
61
|
+
## App attribution
|
|
62
|
+
|
|
63
|
+
Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compaction-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests.
|
|
64
|
+
|
|
65
|
+
DeepSeek request identity is separate from app attribution. After credential resolution, every provider request carries `x-deepseek-harness-user-id` with the stable anonymous id from [`@stackstackstack/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md); a request carrying `GenerateOptions.sessionId` also sends that exact value as `x-deepseek-harness-session-id`, while a direct call without a session omits the session header. Both headers go to the resolved `baseURL`, including a configured gateway, and remain outside the request body and model-visible content.
|
|
66
|
+
|
|
67
|
+
## Wire-format notes
|
|
68
|
+
|
|
69
|
+
- Streaming only (`stream_options.include_usage` always on). `usage` may arrive attached to the finish chunk or as a trailing usage-only chunk — the translator defers both to `[DONE]`, so `usage` always precedes `finish` and nothing follows `finish`.
|
|
70
|
+
- The adapter-owned `off` effort maps to `thinking: {type: 'disabled'}` and never crosses the wire as `reasoning_effort: 'off'`.
|
|
71
|
+
- The first thinking-mode chunk carries `reasoning_content: ""` — handled (no spurious reasoning block).
|
|
72
|
+
- **Reasoning passback rule**: on assistant turns that carried tool calls, `reasoning_content` is serialized back in history (required by the API in thinking mode); on tool-call-free turns it is dropped (ignored anyway — saves tokens).
|
|
73
|
+
- Cache accounting: `cacheReadTokens` ← `prompt_cache_hit_tokens` / `prompt_tokens_details.cached_tokens`; DeepSeek reports no cache-write metric.
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
Non-2xx responses throw `LlmError` with stable codes: `AUTH` (401/403), `QUOTA` (a response whose provider details identify exhausted quota, balance, or credits), `RATE_LIMIT` (other 429s), `CONTEXT_WINDOW_EXCEEDED` (a 400 whose provider code, type, or message identifies context overflow), `INVALID_REQUEST` (other 400s), `SERVER` (5xx), `HTTP_<status>` otherwise. Its serializable `failure` retains the HTTP status plus a valid positive `Retry-After` seconds/date delay and `x-request-id` / `x-deepseek-request-id` when present. A pre-response transport failure (DNS, refused connection, TLS, proxy) throws `TRANSPORT` naming the configured endpoint and chaining the original rejection as `cause`; caller aborts throw `ABORTED`, and the loop's cancellation signal remains authoritative. Protocol violations throw `STREAM_CLOSED` (no `[DONE]`) or `MALFORMED_RESPONSE` (bad JSON payload). Unknown wire `finish_reason`s (e.g. `content_filter`, `insufficient_system_resource`) become `finish {kind: 'error', failure}` chunks, and a completed stream whose `stop` (or absent) finish opened no content blocks becomes a `finish {kind: 'error'}` with code `EMPTY_RESPONSE` (retried by default policy).
|
|
78
|
+
|
|
79
|
+
## Model Experience
|
|
80
|
+
|
|
81
|
+
### DeepSeek request
|
|
82
|
+
|
|
83
|
+
#### What the model sees
|
|
84
|
+
|
|
85
|
+
The selected DeepSeek model receives the harness system prompt, message history, tool schemas, stop sequences, and call config without adapter-authored prompt prose. On a prior assistant turn with tool calls, its reasoning content is passed back as required; reasoning from tool-call-free turns is omitted.
|
|
86
|
+
|
|
87
|
+
#### Token effect
|
|
88
|
+
|
|
89
|
+
Provider tokenization governs exact input. Conditional reasoning passback increases tool-round-trip context, while dropping other reasoning avoids paying those tokens again; cache-read usage is reported when available.
|
|
90
|
+
|
|
91
|
+
#### KV Cache effect
|
|
92
|
+
|
|
93
|
+
An unchanged assembled prefix is eligible for DeepSeek cache reuse, which this adapter reports in usage. A model-route change or any upstream prompt, schema, prefix, or history change may prevent reuse from the first changed token; reasoning passback appends during tool round trips.
|
|
94
|
+
|
|
95
|
+
### DeepSeek response
|
|
96
|
+
|
|
97
|
+
#### What the model sees
|
|
98
|
+
|
|
99
|
+
Reasoning, text, and raw-string tool arguments are translated into harness chunks for the loop to log and assemble.
|
|
100
|
+
|
|
101
|
+
#### Token effect
|
|
102
|
+
|
|
103
|
+
Generated tokens follow the request's logged reasoning effort and `maxTokens`; only loop-retained blocks affect later input.
|
|
104
|
+
|
|
105
|
+
#### KV Cache effect
|
|
106
|
+
|
|
107
|
+
Loop-retained response blocks append to the next request and preserve its earlier reusable prefix; dropped blocks have no later cache effect. Changing the provider or model selects a different cache domain.
|
|
108
|
+
|
|
109
|
+
## Known Limitations and Deferred Work
|
|
110
|
+
|
|
111
|
+
- **A settings `models` list replaces the composition list wholesale** — settings-layer merging is per-field, and arrays are one field; per-entry catalog merging would need a keyed shape.
|
|
112
|
+
- **`tool_choice` is not mapped** — not part of the core vocabulary (MVP cut, shared with the pi-ai twin).
|
|
113
|
+
- **Requests use raw `fetch`, not `@cordisjs/plugin-http`** — no shared proxy/interception configuration; adoption is deferred until a second adapter wants it (`TODO(http)`).
|
|
114
|
+
- **Serialization flattens user and tool-result content to text blocks** — plugin-added block types are skipped, and empty tool output crosses the wire as the literal `(no output)`.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @stackstackstack/dsh-llm-deepseek
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE(Server-Sent Events,由 `eventsource-parser` 分帧),将官方协议格式(wire format;真源:API 文档 guides/thinking_mode、guides/tool_calls、api/create-chat-completion)转换为 `StreamChunk` 协议。
|
|
6
|
+
|
|
7
|
+
同一 seam 的第二个基于库的实现位于 `@stackstackstack/dsh-llm-pi-ai`。本包拥有 `deepseek-official` 提供方路由——刻意区别于 pi-ai 的 catalog 名称 `deepseek`,因此同一组合可以并排挂载两条 DeepSeek 路径;而为 `deepseek-official` 本身注册另一个适配器仍会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
|
8
|
+
|
|
9
|
+
包根入口导出 Cordis 插件约定与 `DeepSeekAdapter`;协议序列化、SSE 解析与分片转换 helper 不属于该根约定。
|
|
10
|
+
|
|
11
|
+
## 配置
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
- id: llm-deepseek
|
|
15
|
+
name: '@stackstackstack/dsh-llm-deepseek'
|
|
16
|
+
config:
|
|
17
|
+
apiKeyEnv: DEEPSEEK_API_KEY # default; resolved per request via ctx.credentials, then the environment
|
|
18
|
+
baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
|
|
19
|
+
thinking: enabled # optional; provider default is enabled
|
|
20
|
+
reasoningEffort: high # optional; off | high | max — omitted ⇒ high
|
|
21
|
+
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
|
22
|
+
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
|
23
|
+
retryPolicy: # optional; omission uses bounded normal defaults
|
|
24
|
+
mode: always # normal | always
|
|
25
|
+
backoff:
|
|
26
|
+
initialDelayMs: 500
|
|
27
|
+
maxDelayMs: 10000
|
|
28
|
+
jitterRatio: 0.1
|
|
29
|
+
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
|
30
|
+
models: # optional; defaults to V4 Flash and V4 Pro
|
|
31
|
+
- id: deepseek-v4-flash
|
|
32
|
+
name: DeepSeek-V4-Flash
|
|
33
|
+
- id: private-reasoner
|
|
34
|
+
description: Company-hosted reasoning model
|
|
35
|
+
contextWindow: 512000
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
该插件注册唯一提供方路由 `deepseek-official`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
|
|
39
|
+
|
|
40
|
+
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。适配器默认值为 1,000,000;因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
|
41
|
+
|
|
42
|
+
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。Catalog 配置项可以自带 `maxTokens`,它对该模型胜出;不含该上限的配置项以及任何未列出原样传递 id 都解析为 profile 值,因此新增按模型的上限只改变一个模型,而非整条路由。确切模型解析会将胜出值公开为 `defaultMaxTokens`;`LlmRuntime` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
|
43
|
+
|
|
44
|
+
同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`high` 和 `max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
|
45
|
+
|
|
46
|
+
`thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `high` 或 `max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。
|
|
47
|
+
|
|
48
|
+
`streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它把已配置策略注册为提供方元数据,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。
|
|
49
|
+
|
|
50
|
+
## 动态配置(settings + credentials)
|
|
51
|
+
|
|
52
|
+
连接事实不在加载时冻结。`resolveAdapterOptions` 是从原始配置到已校验事实的唯一显式 resolve 步骤,适配器经由一个 thunk **每操作重读一次**:base URL、catalog、请求默认值与 idle 预算都在下一次请求生效,进行中的流则保持其起始事实。两个可选 seam 供给该 thunk:
|
|
53
|
+
|
|
54
|
+
- **`ctx.settings`**——插件用同一份 `Config` schema 注册 `llm-deepseek` namespace,并以其 `cordis.yml` 条目为组合 `base`,因此用户设置文档中的 `llm-deepseek:` 分节可以免重启覆盖任何字段。未挂载 settings 服务时,仅由 entry 配置驱动适配器,行为不变。存活 settings 快照若通过 schema 却违反 schema 之外的约束(重复的 catalog id、无法成立的 thinking/推理强度组合),则保留最后可用事实并记录失败;entry 配置本身仍会使插件加载失败。
|
|
55
|
+
- **`ctx.credentials`**——API 密钥按每次 stream 调用解析,取自与端点*同一*份解析后的快照。配置只携带 `apiKeyEnv`,从不携带字面密钥:该引用经凭据 seam 解析,未挂载 seam 时则经受信环境层解析。由于凭据事实与连接事实同行,被 resolver 拒绝的 settings 快照既不贡献自己的端点,也不贡献自己的密钥:整个先前世代继续服务。每个解析出的密钥在使用前都会被校验格式,因此 HTTP 标头无法承载的值会以 `LlmError('INVALID_CREDENTIAL')` 被拒绝,点名失败的入口,但绝不透露密钥的任何部分,而不是以语义不明的 `fetch` `TypeError` 形式浮现。任何地方都没有密钥的请求以 `MISSING_CREDENTIAL` 失败,并点名每个配置入口,同时路由保持注册、catalog 保持可浏览——首次运行的上手流程就是「浏览模型、存入密钥、再次发起提示」,中间无需任何重启。
|
|
56
|
+
|
|
57
|
+
唯一在注册期捕获的事实是重试策略:其解析值变化时,插件原地重新注册该路由(同一适配器实例、一个同步区段),因此 `ctx.llm.providerRetryPolicy('deepseek-official')` 始终报告当前策略。
|
|
58
|
+
|
|
59
|
+
该插件还会在可配置提供方目录(`ctx.llm.listConfigurableProviders()`)中声明自己的路由:提供方为 `deepseek-official`,settings namespace 为 `llm-deepseek`,settings path 为空——整个分节就是 profile。配置界面借助该条目,把本适配器与休眠的 pi-ai 提供方一并呈现。
|
|
60
|
+
|
|
61
|
+
## 应用归因
|
|
62
|
+
|
|
63
|
+
每个请求都携带 dsh-llm `attributionHeaders()` 的共享归因标头,即用于识别 harness 的必需 `User-Agent` 基线(见 [dsh-llm § 应用归因](../llm/README.md#app-attribution-attributionts))。在该适配器约定(adapter contract)下,直接 DeepSeek 请求与 OpenAI 兼容 gateway 请求都不会获得提供方特定应用归因标头;OpenRouter 应用归因暂缓到未来的显式 OpenRouter 适配器或模式。`GenerateOptions.purpose` 为 `compaction` 的请求(dsh-compaction-basic 的辅助摘要调用)还会携带 `x-deepseek-harness-compact: 1`,让宿主可以将压缩流量与会话请求分开。
|
|
64
|
+
|
|
65
|
+
DeepSeek 请求身份独立于应用归因。凭据解析成功后,每个提供方请求都会通过 `x-deepseek-harness-user-id` 携带来自 [`@stackstackstack/dsh-anonymous-user-id`](../../identity/anonymous-user-id/README.md) 的稳定匿名 id;携带 `GenerateOptions.sessionId` 的请求还会通过 `x-deepseek-harness-session-id` 发送该确切值,缺少会话的直接调用则省略会话标头。两个标头都会发送至解析后的 `baseURL`(包括已配置的 gateway),且不会进入请求正文或模型可见内容。
|
|
66
|
+
|
|
67
|
+
## 协议格式说明
|
|
68
|
+
|
|
69
|
+
- 只支持流式输出(`stream_options.include_usage` 始终开启)。`usage` 可能附着在 finish 分片上,也可能作为尾随的纯 usage 分片到达;转换器会将两者都延迟到 `[DONE]`,因此 `usage` 始终位于 `finish` 之前,`finish` 之后不会出现任何内容。
|
|
70
|
+
- 适配器持有的 `off` 推理强度映射为 `thinking: {type: 'disabled'}`,绝不会以 `reasoning_effort: 'off'` 通过协议发送。
|
|
71
|
+
- 第一个思考模式分片携带 `reasoning_content: ""`,系统会处理它(不会产生多余 reasoning 块)。
|
|
72
|
+
- **推理回传规则**:对携带工具调用的 assistant 轮次,会将 `reasoning_content` 序列化回历史(思考模式 API 必需);对不含工具调用的轮次,它会被丢弃(不会使用,可节省 token)。
|
|
73
|
+
- Cache 计量:`cacheReadTokens` ← `prompt_cache_hit_tokens` / `prompt_tokens_details.cached_tokens`;DeepSeek 不报告 cache-write 指标。
|
|
74
|
+
|
|
75
|
+
## 错误
|
|
76
|
+
|
|
77
|
+
非 2xx 响应会抛出稳定 code 的 `LlmError`:`AUTH`(401/403)、`QUOTA`(提供方详细信息标识配额、余额或点数耗尽的响应)、`RATE_LIMIT`(其他 429)、`CONTEXT_WINDOW_EXCEEDED`(提供方 code、type 或 message 标识上下文溢出的 400)、`INVALID_REQUEST`(其他 400)、`SERVER`(5xx),其他情况为 `HTTP_<status>`。其可序列化 `failure` 保留 HTTP 状态,以及有效的正 `Retry-After` 秒数/日期延迟和存在时的 `x-request-id` / `x-deepseek-request-id`。响应前传输失败(DNS、连接被拒绝、TLS、proxy)会抛出命名已配置端点的 `TRANSPORT`,并将原始拒绝作为 `cause`;调用方 abort 抛出 `ABORTED`,仍以 loop 的取消信号为准。协议违例抛出 `STREAM_CLOSED`(没有 `[DONE]`)或 `MALFORMED_RESPONSE`(JSON payload 格式错误)。未知协议 `finish_reason`(例如 `content_filter`、`insufficient_system_resource`)会变为 `finish {kind: 'error', failure}` 分片;已完成流如果使用 `stop`(或缺失)finish 但没有开启内容块,就会变为 `finish {kind: 'error'}`,code 为 `EMPTY_RESPONSE`(默认策略会重试)。
|
|
78
|
+
|
|
79
|
+
## 模型体验
|
|
80
|
+
|
|
81
|
+
### DeepSeek 请求
|
|
82
|
+
|
|
83
|
+
#### 模型看到的内容
|
|
84
|
+
|
|
85
|
+
所选 DeepSeek 模型会收到 harness 系统提示词、消息历史、工具 schema、stop sequence 和调用配置,不含适配器撰写的提示词文本。当之前的 assistant 轮次包含工具调用时,会按要求回传其推理内容;不含工具调用的轮次会省略推理。
|
|
86
|
+
|
|
87
|
+
#### Token 影响
|
|
88
|
+
|
|
89
|
+
精确输入取决于提供方 tokenization。有条件推理回传会增加工具往返上下文,丢弃其他推理则避免再次支付这些 token;可用时会报告 cache-read 用量。
|
|
90
|
+
|
|
91
|
+
#### KV Cache 影响
|
|
92
|
+
|
|
93
|
+
未更改的已组装前缀可使用 DeepSeek cache 复用,适配器会在 usage 中报告它。模型路由变更,或任何上游提示词、schema、前缀或历史变更,都可能使从首个发生变化的 token 起的复用失效;推理回传会在工具往返期间追加。
|
|
94
|
+
|
|
95
|
+
### DeepSeek 响应
|
|
96
|
+
|
|
97
|
+
#### 模型看到的内容
|
|
98
|
+
|
|
99
|
+
推理、文本与原始字符串工具参数会转换为 harness 分片,供 loop 记录和组装。
|
|
100
|
+
|
|
101
|
+
#### Token 影响
|
|
102
|
+
|
|
103
|
+
生成 token 遵循请求中已记录的推理强度和 `maxTokens`;只有 loop 保留的块会影响后续输入。
|
|
104
|
+
|
|
105
|
+
#### KV Cache 影响
|
|
106
|
+
|
|
107
|
+
loop 保留的响应块会追加到下一个请求,并保留其较早可复用前缀;已丢弃块不会影响后续 cache。更改提供方或模型会选择不同 cache 域。
|
|
108
|
+
|
|
109
|
+
## 已知限制与暂缓事项
|
|
110
|
+
|
|
111
|
+
- **settings 的 `models` 列表会整体替换组合列表**:settings 层按字段合并,而数组是单个字段;按条目合并 catalog 需要带键的形状。
|
|
112
|
+
- **未映射 `tool_choice`**:它不属于核心词汇(MVP 取舍,与 pi-ai twin 共享)。
|
|
113
|
+
- **请求使用原始 `fetch`,而非 `@cordisjs/plugin-http`**:没有共享 proxy/拦截配置;采用暂缓到第二个适配器需要该功能时(`TODO(http)`)。
|
|
114
|
+
- **序列化会将 user 与工具结果内容展平为文本块**:会跳过插件添加的块类型,空工具输出会以字面 `(no output)` 通过协议发送。
|