refactor(llm-deepseek)!: rename the provider route to deepseek-official

The native adapter's route was named deepseek, colliding with pi-ai's
catalog provider of the same name, so the two DeepSeek paths could never
be mounted side by side. The web settings page needs both configurable at
once. Compositions, fixtures, goldens, scaffolding defaults, and docs all
move together (pre-release, no shim); TUI/session-query-spill/
missing-credential goldens re-recorded through their keyless refresh
modes because provider-name length shifts box padding and spill
truncation points.
This commit is contained in:
Yichen Jiang
2026-07-29 16:36:07 +08:00
parent 7f1496c996
commit fee12f1af0
239 changed files with 823 additions and 820 deletions

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
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.
A second, library-backed implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai`. This package always owns the `deepseek` provider route; mounting a pi-ai profile with `provider: deepseek` in the same context throws `LlmError('DUPLICATE_ADAPTER')` by design.
A second, library-backed implementation of the same seam exists in `@deepseek-ai/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')`.
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.
@@ -35,9 +35,9 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
contextWindow: 64000
```
The plugin registers the single provider route `deepseek` together with its resolved `retryPolicy`. A request selects it with `provider: deepseek`; 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 256,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek')` 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.
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 256,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.
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. When neither value exists, `context` is absent without invalidating routing. Pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek` throws `LlmError('DUPLICATE_ADAPTER')`.
`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. When neither value exists, `context` is absent without invalidating routing. 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')`.
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.
@@ -52,7 +52,7 @@ Connection facts are not frozen at load. `resolveAdapterOptions` is the one expl
- **`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.
- **`ctx.credentials`** — the API key resolves per stream call: a non-empty literal `apiKey` wins, then `apiKeyEnv` through the credential seam (`$DSH_HOME/.env` under the live environment), then — only without a mounted seam — the raw environment variable. 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.
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')` always reports the current policy.
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.
## App attribution

View File

@@ -4,7 +4,7 @@
harness LLM seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE`eventsource-parser` 分帧将官方协议格式真源API 文档 guides/thinking_mode、guides/tool_calls、api/create-chat-completion转换为 `StreamChunk` 协议。
同一 seam 的第二个库支持实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包始终拥有 `deepseek` 提供方路由;在同一上下文中装载 `provider: deepseek` 的 pi-ai profile 会按设计抛出 `LlmError('DUPLICATE_ADAPTER')`
同一 seam 的第二个库支持实现位于 `@deepseek-ai/dsh-llm-pi-ai`。本包始终拥有 `deepseek` 提供方路由;在同一上下文中装载 `provider: deepseek-official` 的 pi-ai profile 会按设计抛出 `LlmError('DUPLICATE_ADAPTER')`
包根目录公开 Cordis 插件契约与 `DeepSeekAdapter`协议序列化、SSE 解析与 chunk 转换 helper 不属于该根契约。
@@ -35,9 +35,9 @@ harness LLM seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE
contextWindow: 64000
```
该插件注册唯一提供方路由 `deepseek`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 256,000 token显式列表会替换这些默认值`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek')` 公开给 ACPAgent Client Protocol编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
该插件注册唯一提供方路由 `deepseek-official`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 256,000 token显式列表会替换这些默认值`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACPAgent Client Protocol编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。两者都不存在时,`context` 字段缺失但不会使路由失效。因此,压力敏感插件可以获得部署拥有的容量,不会将模型 selector 视为权威。为 `deepseek` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。两者都不存在时,`context` 字段缺失但不会使路由失效。因此,压力敏感插件可以获得部署拥有的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`
同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off``high``max` 推理强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high``agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header``high``max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
@@ -52,7 +52,7 @@ harness LLM seam 的 DeepSeek chat-completions 适配器:直接 `fetch` + SSE
- **`ctx.settings`**——插件用同一份 `Config` schema 注册 `llm-deepseek` namespace并以其 `cordis.yml` 条目为组合 `base`,因此用户设置文档中的 `llm-deepseek:` 分节可以免重启覆盖任何字段。未挂载 settings 服务时,仅由 entry 配置驱动适配器,行为不变。存活 settings 快照若通过 schema 却违反 schema 之外的约束(重复的 catalog id、无法成立的 thinking推理强度组合则保留最后可用事实并记录失败entry 配置本身仍会使插件加载失败。
- **`ctx.credentials`**——API 密钥按每次 stream 调用解析:非空的字面 `apiKey` 优先,其次经凭据 seam 解析 `apiKeyEnv`(活跃环境之下的 `$DSH_HOME/.env`),最后——仅在未挂载 seam 时——读取原始环境变量。任何地方都没有密钥的请求以 `MISSING_CREDENTIAL` 失败并点名每个配置入口同时路由保持注册、catalog 保持可浏览——首次运行的上手流程就是「浏览模型、存入密钥、再次发起提示」,中间无需任何重启。
唯一在注册期捕获的事实是重试策略:其解析值变化时,插件原地重新注册该路由(同一适配器实例、一个同步区段),因此 `ctx.llm.providerRetryPolicy('deepseek')` 始终报告当前策略。
唯一在注册期捕获的事实是重试策略:其解析值变化时,插件原地重新注册该路由(同一适配器实例、一个同步区段),因此 `ctx.llm.providerRetryPolicy('deepseek-official')` 始终报告当前策略。
## 应用归因

View File

@@ -1,5 +1,5 @@
/**
* Register a {@link DeepSeekAdapter} for the `deepseek` provider route on
* Register a {@link DeepSeekAdapter} for the `deepseek-official` provider route on
* `ctx.llm`, with connection facts resolved per request instead of frozen at
* load: the plugin layers its `cordis.yml` entry config under the optional
* `llm-deepseek` user-settings section (`ctx.settings`) and resolves the API
@@ -202,7 +202,7 @@ export function apply(ctx: Context, config: Config): void {
if (ambient !== undefined && ambient.length > 0) return ambient
}
throw new LlmError(
'llm-deepseek: no API key for provider route "deepseek"; set the llm-deepseek "apiKey" setting,'
'llm-deepseek: no API key for provider route "deepseek-official"; set the llm-deepseek "apiKey" setting,'
+ ` store ${ref} with the credentials service, or export ${ref}`,
'MISSING_CREDENTIAL',
)
@@ -211,7 +211,7 @@ export function apply(ctx: Context, config: Config): void {
const adapter = new DeepSeekAdapter({ options, resolveApiKey })
// Route effects bind to this apply fiber via the stable `ctx` reference,
// even when a swap runs inside the scoped settings callback below.
let disposeRoute = ctx.llm.registerAdapter(['deepseek'], adapter)
let disposeRoute = ctx.llm.registerAdapter(['deepseek-official'], adapter)
let registeredPolicy = options().retryPolicy
const ensureRegistrationFacts = (): void => {
const policy = options().retryPolicy
@@ -220,7 +220,7 @@ export function apply(ctx: Context, config: Config): void {
// fact per-request resolution cannot refresh: swap the registration in one
// synchronous section (same adapter instance, no NO_ADAPTER window).
disposeRoute()
disposeRoute = ctx.llm.registerAdapter(['deepseek'], adapter)
disposeRoute = ctx.llm.registerAdapter(['deepseek-official'], adapter)
registeredPolicy = policy
}
@@ -228,7 +228,7 @@ export function apply(ctx: Context, config: Config): void {
// Expected on a first boot with dynamic sources: the route stays
// registered (the catalog is browsable) and each request fails with the
// actionable MISSING_CREDENTIAL message until a key arrives.
ctx.logger.warn('llm-deepseek: no API key resolved yet for route "deepseek"; requests will fail until one is configured')
ctx.logger.warn('llm-deepseek: no API key resolved yet for route "deepseek-official"; requests will fail until one is configured')
})
installSettingsSection(ctx, NS, Config, config, {

View File

@@ -172,7 +172,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
const ctx = await harness(FLASH, { thinking: 'disabled' })
const kinds: string[] = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
provider: 'deepseek-official',
model: FLASH,
messages: ask('Count from 1 to 5, digits only.'),
maxTokens: 50,

View File

@@ -78,7 +78,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const kinds: string[] = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
messages: [createUserMessage({
content: [{ type: 'text', text: 'hi' }],
@@ -182,7 +182,7 @@ describe('DeepSeekAdapter against a mock server', () => {
thinking: { type: 'disabled' },
})
expect(server.requests[0]).not.toHaveProperty('reasoning_effort')
await expect(ctx.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash'))
.resolves.toMatchObject({
reasoning: {
efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }],
@@ -213,7 +213,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const adapter = adapterOf({ apiKey: 'test-key', baseURL: server.url, thinking: 'disabled' })
const stream = adapter.stream({
provider: 'deepseek',
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
reasoningEffort: ReasoningEffortId(effort),
messages: [createUserMessage({
@@ -420,7 +420,7 @@ describe('DeepSeekAdapter against a mock server', () => {
)
try {
const iterate = async (): Promise<void> => {
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
}
await expect(iterate()).rejects.toThrow(/no response body/)
} finally {
@@ -452,7 +452,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const pending = (async () => {
const chunks = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
messages: [],
signal: controller.signal,
@@ -472,7 +472,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const adapter = adapterOf({ baseURL: 'https://example.invalid' })
try {
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
}
await expect(drain()).rejects.toMatchObject({ code: 'TRANSPORT', cause })
} finally {
@@ -489,7 +489,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const adapter = adapterOf({ baseURL: 'https://example.invalid' })
try {
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
}
await expect(drain()).rejects.toMatchObject({
message: 'DeepSeek API request to https://example.invalid failed',
@@ -519,7 +519,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const adapter = adapterOf({ baseURL: 'https://example.invalid', streamIdleTimeoutMs: 100 })
try {
const drain = (async () => {
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
})()
const rejected = expect(drain).rejects.toMatchObject({ code: 'TIMEOUT' })
await vi.advanceTimersByTimeAsync(0)
@@ -554,7 +554,7 @@ describe('plugin registration and config', () => {
apiKey: 'k',
baseURL: server.url,
})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
await fiber.dispose()
expect(ctx.llm.listProviders()).toEqual([])
})
@@ -571,7 +571,7 @@ describe('plugin registration and config', () => {
},
})
expect(ctx.llm.providerRetryPolicy('deepseek')).toEqual({
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toEqual({
mode: 'always',
initialDelayMs: 25,
maxDelayMs: 100,
@@ -583,14 +583,14 @@ describe('plugin registration and config', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
{ provider: 'deepseek', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
{ provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
])
await expect(ctx.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash'))
.resolves.toMatchObject({
provider: 'deepseek',
provider: 'deepseek-official',
id: 'deepseek-v4-flash',
name: 'DeepSeek-V4-Flash',
context: { contextWindow: 256_000 },
@@ -613,7 +613,7 @@ describe('plugin registration and config', () => {
baseURL: 'http://127.0.0.1:1',
reasoningEffort: effort,
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'unlisted-pass-through'))
.resolves.toMatchObject({
reasoning: {
efforts: [
@@ -635,7 +635,7 @@ describe('plugin registration and config', () => {
thinking: 'disabled',
reasoningEffort: 'off',
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'unlisted-pass-through'))
.resolves.toMatchObject({
reasoning: {
efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }],
@@ -669,7 +669,7 @@ describe('plugin registration and config', () => {
it('accepts disabled thinking with off at the resolver boundary', async () => {
const adapter = adapterOf({ thinking: 'disabled', reasoningEffort: 'off' })
await expect(adapter.resolveModel('deepseek', 'pass-through')).resolves.toMatchObject({
await expect(adapter.resolveModel('deepseek-official', 'pass-through')).resolves.toMatchObject({
reasoning: {
efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }],
defaultEffort: ReasoningEffortId('off'),
@@ -681,9 +681,9 @@ describe('plugin registration and config', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
LlmDeepSeek.apply(ctx, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
{ provider: 'deepseek', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash' },
{ provider: 'deepseek-official', id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro' },
])
})
@@ -703,18 +703,18 @@ describe('plugin registration and config', () => {
},
],
})
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'private-fast', name: 'private-fast' },
{ provider: 'deepseek', id: 'private-reasoner', name: 'Private Reasoner', description: 'Higher reasoning budget' },
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'private-fast', name: 'private-fast' },
{ provider: 'deepseek-official', id: 'private-reasoner', name: 'Private Reasoner', description: 'Higher reasoning budget' },
])
await expect(ctx.llm.resolveModelInfo('deepseek', 'private-fast'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'private-fast'))
.resolves.toMatchObject({ context: { contextWindow: 32_000 } })
await expect(ctx.llm.resolveModelInfo('deepseek', 'private-reasoner'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'private-reasoner'))
.resolves.toMatchObject({
name: 'Private Reasoner',
description: 'Higher reasoning budget',
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'arbitrary-unlisted'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'arbitrary-unlisted'))
.resolves.not.toHaveProperty('context')
})
@@ -731,11 +731,11 @@ describe('plugin registration and config', () => {
],
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'inherits-default'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'inherits-default'))
.resolves.toMatchObject({ context: { contextWindow: 256_000 } })
await expect(ctx.llm.resolveModelInfo('deepseek', 'exact-override'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'exact-override'))
.resolves.toMatchObject({ context: { contextWindow: 64_000 } })
await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through'))
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'unlisted-pass-through'))
.resolves.toMatchObject({ context: { contextWindow: 256_000 } })
})
@@ -747,7 +747,7 @@ describe('plugin registration and config', () => {
baseURL: 'http://127.0.0.1:1',
models: [],
})
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([])
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([])
})
it.each([
@@ -803,7 +803,7 @@ describe('plugin registration and config', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
})
it('loads keyless, keeps the catalog browsable, and fails the request actionably', async () => {
@@ -813,8 +813,8 @@ describe('plugin registration and config', () => {
await ctx.plugin(LlmDeepSeek, { baseURL: 'http://127.0.0.1:1' })
// First-boot onboarding: the route registers so models stay discoverable;
// only the request itself needs a key.
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek')).resolves.toHaveLength(2)
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
await expect(assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }))
.rejects.toMatchObject({ code: 'MISSING_CREDENTIAL' })
await expect(assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }))
@@ -847,7 +847,7 @@ describe('plugin registration and config', () => {
await ctx.plugin(LlmService)
// Registration succeeds; no call is made (would hit api.deepseek.com).
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
})
it('adapter is constructible directly for embedding over the shared resolver', async () => {
@@ -855,7 +855,7 @@ describe('plugin registration and config', () => {
expect(adapter).toBeInstanceOf(DeepSeekAdapter)
// Direct embedding shares the plugin's one resolve step, so it advertises
// the same default catalog instead of a divergent empty one.
await expect(adapter.listModels('deepseek')).resolves.toHaveLength(2)
await expect(adapter.listModels('deepseek-official')).resolves.toHaveLength(2)
})
it('resolves connection facts and the credential exactly once per stream call', async () => {
@@ -864,7 +864,7 @@ describe('plugin registration and config', () => {
const resolveApiKey = vi.fn(() => Promise.resolve('per-request-key'))
const adapter = new DeepSeekAdapter({ options, resolveApiKey })
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { /* drain */ }
expect(options).toHaveBeenCalledTimes(1)
expect(resolveApiKey).toHaveBeenCalledTimes(1)

View File

@@ -17,7 +17,7 @@ export interface AssembledResult {
export async function assemble(ctx: Context, options: Omit<GenerateOptions, 'provider'> & { provider?: string }): Promise<AssembledResult> {
const assembler = new BlockAssembler()
const request = { provider: 'deepseek', ...options }
const request = { provider: 'deepseek-official', ...options }
for await (const chunk of ctx.llm.stream(request)) assembler.push(chunk)
return {
message: assembler.message({

View File

@@ -106,10 +106,10 @@ describe('request-level dynamic configuration', () => {
const dir = await home()
const { ctx } = await boot(dir, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
await expect(ctx.llm.listModels('deepseek')).resolves.toHaveLength(2)
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
await ctx.settings.update(NS, { models: [{ id: 'settings-model', name: 'From Settings' }] })
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'settings-model', name: 'From Settings' },
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'settings-model', name: 'From Settings' },
])
})
@@ -120,13 +120,13 @@ describe('request-level dynamic configuration', () => {
await ctx.settings.update(NS, {
retryPolicy: { mode: 'always', backoff: { initialDelayMs: 25, maxDelayMs: 100, jitterRatio: 0.2 } },
})
expect(ctx.llm.providerRetryPolicy('deepseek')).toEqual({
expect(ctx.llm.providerRetryPolicy('deepseek-official')).toEqual({
mode: 'always',
initialDelayMs: 25,
maxDelayMs: 100,
jitterRatio: 0.2,
})
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek-official', name: 'DeepSeek' }])
})
it('keeps the last good options when a settings snapshot fails beyond-schema validation', async () => {
@@ -136,10 +136,10 @@ describe('request-level dynamic configuration', () => {
// Schema-valid but resolver-invalid: duplicate catalog ids pass the array
// schema and fail the explicit resolve step.
await ctx.settings.update(NS, { models: [{ id: 'dup' }, { id: 'dup' }] })
await expect(ctx.llm.listModels('deepseek')).resolves.toHaveLength(2)
await expect(ctx.llm.listModels('deepseek-official')).resolves.toHaveLength(2)
await ctx.settings.update(NS, { models: [{ id: 'recovered' }] })
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'recovered', name: 'recovered' },
await expect(ctx.llm.listModels('deepseek-official')).resolves.toEqual([
{ provider: 'deepseek-official', id: 'recovered', name: 'recovered' },
])
})

View File

@@ -4,7 +4,7 @@ import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-ll
import { serializeMessages, serializeRequest } from '../src/serialize.ts'
function request(overrides: Partial<GenerateOptions> = {}): GenerateOptions {
return { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], ...overrides }
return { provider: 'deepseek-official', model: 'deepseek-v4-flash', messages: [], ...overrides }
}
describe('serializeMessages', () => {

View File

@@ -93,7 +93,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
const port = await unusedPort()
context = await harness(`http://127.0.0.1:${port}`, { initialDelayMs: 100 })
const agent = context.agentLoop.create(SessionId('wire-refused'), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})
let recoveryServer: Promise<MockLlmServer> | undefined
@@ -128,7 +128,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
})
context = await harness(server.baseURL)
const agent = context.agentLoop.create(SessionId(`wire-${behavior}`), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})
@@ -154,7 +154,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
})
context = await harness(server.baseURL)
const agent = context.agentLoop.create(SessionId('wire-empty'), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})
@@ -182,7 +182,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
})
context = await harness(server.baseURL)
const agent = context.agentLoop.create(SessionId('wire-partial-eof'), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})
@@ -209,7 +209,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
// the stalled attempt and the mock server's immediate successful response.
context = await harness(server.baseURL, { streamIdleTimeoutMs: 1_000 })
const agent = context.agentLoop.create(SessionId('wire-stall'), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})
@@ -227,7 +227,7 @@ describe('bounded retry through the real DeepSeek HTTP/SSE adapter', () => {
})
context = await harness(server.baseURL)
const agent = context.agentLoop.create(SessionId('wire-exhausted'), {
provider: 'deepseek',
provider: 'deepseek-official',
model: 'mock-model',
})

View File

@@ -67,7 +67,7 @@ Every product adapter sends application identity on provider HTTP requests. `att
### Real adapters
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses direct fetch with `eventsource-parser` SSE framing for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses direct fetch with `eventsource-parser` SSE framing for the `deepseek-official` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale.
## Model Experience

View File

@@ -67,7 +67,7 @@
### 真实适配器
两个适配器使用不同内部机制实现 `LlmAdapter`[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek` 路由使用直接 fetch 加 `eventsource-parser` SSE 分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `StreamChunk` 约定,定义见 `types.ts`usage 先于 finish工具参数保持原始字符串错误使用两种已批准路径之一。设计理由见 [双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)。
两个适配器使用不同内部机制实现 `LlmAdapter`[`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) 针对 `deepseek-official` 路由使用直接 fetch 加 `eventsource-parser` SSE 分帧,[`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) 则通过 `@earendil-works/pi-ai` 动态解析已配置提供方/模型对。两者都遵循 `StreamChunk` 约定,定义见 `types.ts`usage 先于 finish工具参数保持原始字符串错误使用两种已批准路径之一。设计理由见 [双 LLM 适配器](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md)。
## 模型体验