|
|
|
|
@@ -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)
|
|
|
|
|
|