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

@@ -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', () => {