import { createServer } from 'node:http' import type { IncomingMessage, Server, ServerResponse } from 'node:http' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import LlmService, { CONTEXT_WINDOW_EXCEEDED_CODE, errorChain, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, userAgent, } from '@deepseek-ai/dsh-llm' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { SessionId } from '@deepseek-ai/dsh-session' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { DeepSeekAdapter } from '@deepseek-ai/dsh-llm-deepseek' import { httpErrorCode } from '../src/adapter.ts' import { assemble } from './assemble.ts' /** One scripted behavior for the next request the mock server receives. */ type Behavior = | { kind: 'sse'; events: string[]; delayMs?: number } | { kind: 'http-error'; status: number; body: string; contentType?: string; headers?: Record } | { kind: 'close-early'; events: string[] } interface MockServer { url: string /** Bodies of received requests, in order. */ requests: unknown[] /** Header bags of received requests, in order (parallel to `requests`). */ headers: IncomingMessage['headers'][] script: Behavior[] close(): Promise } const servers: Server[] = [] afterEach(async () => { await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve)))) vi.unstubAllEnvs() vi.useRealTimers() }) /** Local chat-completions stand-in: replays scripted behaviors per request. */ async function mockServer(script: Behavior[]): Promise { const requests: unknown[] = [] const headers: IncomingMessage['headers'][] = [] const server = createServer((request: IncomingMessage, response: ServerResponse) => { let body = '' request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8') }) request.on('end', () => { requests.push(JSON.parse(body)) headers.push(request.headers) const behavior = script.shift() if (!behavior) { response.writeHead(500).end('mock script exhausted') return } if (behavior.kind === 'http-error') { response.writeHead(behavior.status, { 'content-type': behavior.contentType ?? 'application/json', ...behavior.headers, }) response.end(behavior.body) return } response.writeHead(200, { 'content-type': 'text/event-stream' }) const write = (index: number): void => { if (index >= behavior.events.length) { if (behavior.kind === 'sse') response.end() else response.destroy() // close-early: drop the socket mid-stream return } response.write(`data: ${behavior.events[index]}\n\n`) setTimeout(() => { write(index + 1) }, behavior.kind === 'sse' ? behavior.delayMs ?? 0 : 5) } write(0) }) }) servers.push(server) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) const address = server.address() if (address === null || typeof address === 'string') throw new Error('no port') return { url: `http://127.0.0.1:${address.port}`, requests, headers, script, close: () => new Promise(resolve => server.close(() => { resolve() })), } } const textEvents = [ '{"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}', '{"choices":[{"delta":{"content":"hello"}}]}', '{"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}', '[DONE]', ] async function harness(baseURL: string, config: object = {}) { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'test-key', baseURL, ...config }) return ctx } describe('DeepSeekAdapter against a mock server', () => { it('streams a text generation end to end through the assembler', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url) const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], }) expect(result.message.content).toEqual([{ type: 'text', text: 'hello' }]) expect(result.finish).toEqual({ kind: 'stop' }) expect(result.usage).toEqual({ inputTokens: 3, outputTokens: 1 }) // The wire request carried the auth header contents we configured. expect(server.requests[0]).toMatchObject({ model: 'deepseek-v4-flash', reasoning_effort: 'high', stream: true, stream_options: { include_usage: true }, }) // Attribution reaches the wire: the exact shared User-Agent, and no // provider-specific headers under the User-Agent-only contract. expect(server.headers[0]?.['user-agent']).toBe(userAgent()) expect(server.headers[0]).not.toHaveProperty('http-referer') expect(server.headers[0]).not.toHaveProperty('x-openrouter-title') expect(server.headers[0]).not.toHaveProperty('x-openrouter-categories') expect(server.headers[0]).not.toHaveProperty('x-deepseek-harness-compact') }) it('streams raw chunks through ctx.llm.stream', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents, delayMs: 2 }]) const ctx = await harness(server.url) const kinds: string[] = [] for await (const chunk of ctx.llm.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], })) { kinds.push(chunk.type) } expect(kinds).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish']) }) it('forwards the harness session id for host-side trajectory routing', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url) await assemble(ctx, { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], sessionId: SessionId('child-session'), }) expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session') }) it('marks the auxiliary compaction call on the wire', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url) await assemble(ctx, { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], purpose: 'compaction', }) expect(server.headers[0]?.['x-deepseek-harness-compact']).toBe('1') }) it('switches dynamically from the configured high default through off to max', async () => { const server = await mockServer([ { kind: 'sse', events: textEvents }, { kind: 'sse', events: textEvents }, { kind: 'sse', events: textEvents }, ]) const ctx = await harness(server.url, { thinking: 'enabled', reasoningEffort: 'high' }) await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], }) await assemble(ctx,{ model: 'deepseek-v4-flash', reasoningEffort: ReasoningEffortId('off'), messages: [{ role: 'user', content: [{ type: 'text', text: 'hi again' }] }], }) await assemble(ctx,{ model: 'deepseek-v4-flash', reasoningEffort: ReasoningEffortId('max'), messages: [{ role: 'user', content: [{ type: 'text', text: 'one more time' }] }], }) expect(server.requests[0]).toMatchObject({ thinking: { type: 'enabled' }, reasoning_effort: 'high', }) expect(server.requests[1]).toMatchObject({ thinking: { type: 'disabled' }, }) expect(server.requests[1]).not.toHaveProperty('reasoning_effort') expect(server.requests[2]).toMatchObject({ thinking: { type: 'enabled' }, reasoning_effort: 'max', }) }) it('publishes only off and omits the wire effort when thinking is disabled', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url, { thinking: 'disabled' }) await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], }) expect(server.requests[0]).toMatchObject({ thinking: { type: 'disabled' }, }) expect(server.requests[0]).not.toHaveProperty('reasoning_effort') await expect(ctx.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash')) .resolves.toMatchObject({ reasoning: { efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], defaultEffort: ReasoningEffortId('off'), }, }) }) it('rejects a per-request effort before I/O when thinking is disabled', async () => { const server = await mockServer([]) const ctx = await harness(server.url, { thinking: 'disabled' }) await expect(assemble(ctx, { model: 'deepseek-v4-flash', reasoningEffort: ReasoningEffortId('high'), messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], })).rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_EFFORT' }) expect(server.requests).toHaveLength(0) }) it.each(['high', 'max'])( 'rejects direct adapter effort %s before I/O when thinking is disabled', async (effort) => { const server = await mockServer([]) const adapter = new DeepSeekAdapter({ apiKey: 'test-key', baseURL: server.url, defaults: { thinking: 'disabled' }, }) const stream = adapter.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', reasoningEffort: ReasoningEffortId(effort), messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], }) await expect(async () => { for await (const _chunk of stream) { /* drain */ } }).rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_EFFORT' }) expect(server.requests).toHaveLength(0) }, ) it.each([ [401, 'AUTH'], [403, 'AUTH'], [429, 'RATE_LIMIT'], [400, 'INVALID_REQUEST'], [500, 'SERVER'], [503, 'SERVER'], ])('maps HTTP %d to LlmError code %s with the body message', async (status, code) => { const behavior: Behavior = { kind: 'http-error', status, body: JSON.stringify({ error: { message: `failed with ${status}`, type: 't', code: 'c' } }), } const server = await mockServer([behavior, behavior]) const ctx = await harness(server.url) await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })) .rejects.toThrow(`failed with ${status}`) await expect( assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }) .catch((error: unknown) => (error as LlmError).code), ).resolves.toBe(code) }) it('classifies a thrown HTTP context-window rejection with the canonical code', async () => { const server = await mockServer([{ kind: 'http-error', status: 400, body: JSON.stringify({ error: { message: 'This model maximum context length is 128000 tokens; your input exceeds that limit.', type: 'invalid_request_error', code: 'context_length_exceeded', }, }), }]) const ctx = await harness(server.url) const code = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) .catch((error: unknown) => (error as LlmError).code) expect(code).toBe(CONTEXT_WINDOW_EXCEEDED_CODE) }) it('retains status, Retry-After seconds, and provider request id as structured facts', async () => { const server = await mockServer([{ kind: 'http-error', status: 429, body: JSON.stringify({ error: { message: 'slow down' } }), headers: { 'retry-after': '2', 'x-request-id': 'req-429' }, }]) const ctx = await harness(server.url) let thrown: unknown try { await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) } catch (error: unknown) { thrown = error } expect(thrown).toBeInstanceOf(LlmError) expect((thrown as LlmError).failure).toEqual({ message: 'slow down', code: 'RATE_LIMIT', status: 429, providerRetryAfterMs: 2_000, requestId: ProviderRequestId('req-429'), }) }) it('parses a future Retry-After HTTP date and the DeepSeek request-id fallback', async () => { const now = 1_800_000_000_000 const dateNow = vi.spyOn(Date, 'now').mockReturnValue(now) try { const server = await mockServer([{ kind: 'http-error', status: 503, body: JSON.stringify({ error: { message: 'come back later' } }), headers: { 'retry-after': new Date(now + 3_000).toUTCString(), 'x-deepseek-request-id': 'deepseek-503', }, }]) const ctx = await harness(server.url) await expect(assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })) .rejects.toMatchObject({ failure: { message: 'come back later', code: 'SERVER', status: 503, providerRetryAfterMs: 3_000, requestId: ProviderRequestId('deepseek-503'), }, }) } finally { dateNow.mockRestore() } }) it('omits zero, non-finite, invalid, and past Retry-After values', async () => { const values = [ '0', '9'.repeat(400), 'not-a-date', new Date(0).toUTCString(), ] for (const value of values) { const server = await mockServer([{ kind: 'http-error', status: 429, body: JSON.stringify({ error: { message: 'retry later' } }), headers: { 'retry-after': value }, }]) const ctx = await harness(server.url) let thrown: LlmError | undefined try { await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) } catch (error: unknown) { if (error instanceof LlmError) thrown = error } expect(thrown?.failure).toEqual({ message: 'retry later', code: 'RATE_LIMIT', status: 429 }) } }) it('classifies only context-capacity HTTP 400 details as context overflow', () => { expect(httpErrorCode(400, { message: 'request too large for model context' })) .toBe(CONTEXT_WINDOW_EXCEEDED_CODE) expect(httpErrorCode(400, { message: 'invalid input: temperature exceeds maximum allowed value' })) .toBe('INVALID_REQUEST') expect(httpErrorCode(413, { code: 'context_length_exceeded' })).toBe('HTTP_413') }) it('distinguishes terminal quota exhaustion from transient HTTP 429 throttling', () => { expect(httpErrorCode(429, { code: 'insufficient_quota', message: 'account credits exhausted' })) .toBe(QUOTA_EXCEEDED_CODE) expect(httpErrorCode(429, { message: 'request rate limit exceeded' })).toBe('RATE_LIMIT') }) it('keeps the status-line message for JSON error bodies without a message', async () => { const server = await mockServer([{ kind: 'http-error', status: 500, body: '{"error":{"type":"x"}}' }]) const ctx = await harness(server.url) await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })) .rejects.toThrow(/HTTP 500/) }) it('keeps the status-line message for non-JSON error bodies', async () => { const server = await mockServer([{ kind: 'http-error', status: 502, body: 'Bad Gateway', contentType: 'text/plain' }]) const ctx = await harness(server.url) await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })) .rejects.toThrow(/HTTP 502/) }) it('maps unusual statuses to HTTP_', () => { expect(httpErrorCode(418)).toBe('HTTP_418') }) it('wraps a transport failure in TRANSPORT with the fetch cause chain in the message', async () => { // Port 1 is reserved/unbound: fetch rejects with `TypeError: fetch failed` // whose actionable detail (ECONNREFUSED) lives on `cause`. const ctx = await harness('http://127.0.0.1:1') let caught: unknown try { await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) } catch (error: unknown) { caught = error } expect(caught).toBeInstanceOf(LlmError) const llmError = caught as LlmError expect(llmError.code).toBe('TRANSPORT') expect(llmError.message).toContain('http://127.0.0.1:1') expect(llmError.cause).toBeInstanceOf(TypeError) // The chain renderer reaches the transport diagnosis through the cause. expect(errorChain(llmError)).toMatch(/ECONNREFUSED|EADDRNOTAVAIL|bad port/) }) it('classifies an aborted request without losing the transport rejection', async () => { const controller = new AbortController() controller.abort() const ctx = await harness('http://127.0.0.1:1') let caught: unknown try { await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], signal: controller.signal }) } catch (error: unknown) { caught = error } expect(caught).toBeInstanceOf(LlmError) expect(caught).toMatchObject({ code: 'ABORTED' }) expect((caught as LlmError).cause).toMatchObject({ name: 'AbortError' }) }) it('throws EMPTY_RESPONSE when the response has no body', async () => { const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1' }) const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(null, { status: 200 }), ) try { const iterate = async (): Promise => { for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ } } await expect(iterate()).rejects.toThrow(/no response body/) } finally { fetchSpy.mockRestore() } }) it('classifies an abrupt body close as TRANSPORT and retains its cause', async () => { const server = await mockServer([{ kind: 'close-early', events: ['{"choices":[{"delta":{"content":"par"}}]}'], }]) const ctx = await harness(server.url) let caught: unknown try { await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }) } catch (error: unknown) { caught = error } expect(caught).toMatchObject({ code: 'TRANSPORT' }) expect(errorChain(caught)).toMatch(/terminated|socket|without \[DONE\]/) }) it('aborts mid-stream via the request signal', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents, delayMs: 50 }]) const ctx = await harness(server.url) const controller = new AbortController() const pending = (async () => { const chunks = [] for await (const chunk of ctx.llm.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], signal: controller.signal, })) { chunks.push(chunk) } return chunks })() setTimeout(() => { controller.abort() }, 30) await expect(pending).rejects.toMatchObject({ code: 'ABORTED' }) }) it('maps connection failures to TRANSPORT without losing the cause', async () => { const cause = new TypeError('connection refused') const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(cause) const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'https://example.invalid' }) try { const drain = async (): Promise => { for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ } } await expect(drain()).rejects.toMatchObject({ code: 'TRANSPORT', cause }) } finally { fetchSpy.mockRestore() } }) it('renders a non-Error transport rejection without losing its cause', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => { const failed = Promise.withResolvers() failed.reject('offline') return failed.promise }) const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'https://example.invalid' }) try { const drain = async (): Promise => { for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ } } await expect(drain()).rejects.toMatchObject({ message: 'DeepSeek API request to https://example.invalid failed', code: 'TRANSPORT', cause: 'offline', }) } finally { fetchSpy.mockRestore() } }) it('aborts the underlying body when the stream stays idle past its watchdog', async () => { vi.useFakeTimers() let stopped = false const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation((_input, init) => { const signal = init?.signal const body = new ReadableStream({ start(controller) { signal?.addEventListener('abort', () => { stopped = true controller.error(signal.reason) }, { once: true }) }, }) return Promise.resolve(new Response(body, { status: 200 })) }) const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'https://example.invalid', streamIdleTimeoutMs: 100, }) try { const drain = (async () => { for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ } })() const rejected = expect(drain).rejects.toMatchObject({ code: 'TIMEOUT' }) await vi.advanceTimersByTimeAsync(0) await vi.advanceTimersByTimeAsync(100) await rejected expect(stopped).toBe(true) } finally { fetchSpy.mockRestore() } }) }) describe('plugin registration and config', () => { it('keeps wire helpers off the package root', () => { for (const helper of [ 'httpErrorCode', 'serializeMessages', 'serializeRequest', 'DONE', 'parseSse', 'mapFinishReason', 'mapUsage', 'translate', ]) expect(LlmDeepSeek).not.toHaveProperty(helper) }) it('registers the deepseek provider and unregisters on dispose (HMR safety)', async () => { const server = await mockServer([]) const ctx = new Context() await ctx.plugin(LlmService) const fiber = await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: server.url, }) expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }]) await fiber.dispose() expect(ctx.llm.listProviders()).toEqual([]) }) it('owns the deepseek provider and advertises the default models', async () => { 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' }, ]) await expect(ctx.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash')) .resolves.toMatchObject({ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'deepseek-v4-flash', context: { contextWindow: 128_000 }, reasoning: { efforts: [ { id: ReasoningEffortId('off'), name: 'Off' }, { id: ReasoningEffortId('high'), name: 'High' }, { id: ReasoningEffortId('max'), name: 'Max' }, ], defaultEffort: ReasoningEffortId('high'), }, }) }) it.each(['off', 'max'] as const)('uses the configured %s reasoning default', async (effort) => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', reasoningEffort: effort, }) await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through')) .resolves.toMatchObject({ reasoning: { efforts: [ { id: ReasoningEffortId('off'), name: 'Off' }, { id: ReasoningEffortId('high'), name: 'High' }, { id: ReasoningEffortId('max'), name: 'Max' }, ], defaultEffort: ReasoningEffortId(effort), }, }) }) it('accepts off as the default when thinking is deployment-disabled', async () => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', thinking: 'disabled', reasoningEffort: 'off', }) await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through')) .resolves.toMatchObject({ reasoning: { efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], defaultEffort: ReasoningEffortId('off'), }, }) }) it.each(['high', 'max'] as const)( 'rejects configured reasoning effort %s when thinking is disabled', async (reasoningEffort) => { const ctx = new Context() await ctx.plugin(LlmService) await expect(ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', thinking: 'disabled', reasoningEffort, })).rejects.toThrow(/only reasoningEffort "off"/) expect(ctx.llm.listProviders()).toEqual([]) }, ) it.each(['high', 'max'] as const)( 'rejects disabled-thinking effort %s at the direct constructor boundary', (reasoningEffort) => { expect(() => new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1', defaults: { thinking: 'disabled', reasoningEffort }, })).toThrow(/only reasoningEffort "off"/) }, ) it('accepts disabled thinking with off at the direct constructor boundary', async () => { const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1', defaults: { thinking: 'disabled', reasoningEffort: 'off' }, }) await expect(adapter.resolveModel('deepseek', 'pass-through')).resolves.toMatchObject({ reasoning: { efforts: [{ id: ReasoningEffortId('off'), name: 'Off' }], defaultEffort: ReasoningEffortId('off'), }, }) }) it('uses the default model catalog when apply is called directly', async () => { 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' }, ]) }) it('advertises configured models without restricting arbitrary request ids', async () => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', models: [ { id: 'private-fast', contextWindow: 32_000 }, { id: 'private-reasoner', name: 'Private Reasoner', description: 'Higher reasoning budget', contextWindow: 64_000, }, ], }) 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.resolveModelInfo('deepseek', 'private-fast')) .resolves.toMatchObject({ context: { contextWindow: 32_000 } }) await expect(ctx.llm.resolveModelInfo('deepseek', 'private-reasoner')) .resolves.toMatchObject({ name: 'Private Reasoner', description: 'Higher reasoning budget', }) await expect(ctx.llm.resolveModelInfo('deepseek', 'arbitrary-unlisted')) .resolves.not.toHaveProperty('context') }) it('uses exact model capacity before the adapter-wide default', async () => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', defaultContextWindow: 256_000, models: [ { id: 'inherits-default' }, { id: 'exact-override', contextWindow: 64_000 }, ], }) await expect(ctx.llm.resolveModelInfo('deepseek', 'inherits-default')) .resolves.toMatchObject({ context: { contextWindow: 256_000 } }) await expect(ctx.llm.resolveModelInfo('deepseek', 'exact-override')) .resolves.toMatchObject({ context: { contextWindow: 64_000 } }) await expect(ctx.llm.resolveModelInfo('deepseek', 'unlisted-pass-through')) .resolves.toMatchObject({ context: { contextWindow: 256_000 } }) }) it('allows an explicit empty model catalog', async () => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', models: [], }) await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([]) }) it.each([ [[{ id: '' }], /ids must be non-empty/], [[{ id: 'm', name: '' }], /empty name/], [[{ id: 'm', contextWindow: 0 }], /contextWindow/], [[{ id: 'm', contextWindow: 1.5 }], /contextWindow/], [[{ id: 'm' }, { id: 'm' }], /duplicate catalog model/], ] as const)('rejects invalid advisory model config', async (models, message) => { const ctx = new Context() await ctx.plugin(LlmService) await expect(ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', models: [...models], })).rejects.toThrow(message) expect(ctx.llm.listProviders()).toEqual([]) }) it('rejects invalid context capacity when apply is called directly', async () => { const ctx = new Context() await ctx.plugin(LlmService) expect(() => { LlmDeepSeek.apply(ctx, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', models: [{ id: 'invalid-context', contextWindow: 0 }], }) }).toThrow(/contextWindow must be a positive integer/) expect(ctx.llm.listProviders()).toEqual([]) }) it.each([0, 1.5])( 'rejects invalid adapter-wide default context capacity %s', async (defaultContextWindow) => { expect(() => new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1', defaultContextWindow, })).toThrow(/defaultContextWindow must be a positive integer/) const ctx = new Context() await ctx.plugin(LlmService) await expect(ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', defaultContextWindow, })).rejects.toThrow(/defaultContextWindow/) expect(ctx.llm.listProviders()).toEqual([]) }, ) it('falls back to DEEPSEEK_API_KEY and DEEPSEEK_BASE_URL env vars', async () => { vi.stubEnv('DEEPSEEK_API_KEY', 'env-key') vi.stubEnv('DEEPSEEK_BASE_URL', 'http://127.0.0.1:1') const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, {}) expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }]) }) it('throws a clear error when no API key is available', async () => { vi.stubEnv('DEEPSEEK_API_KEY', '') const ctx = new Context() await ctx.plugin(LlmService) await expect(ctx.plugin(LlmDeepSeek, {})) .rejects.toThrow(/an API key is required/) expect(ctx.llm.listProviders()).toEqual([]) }) it('prefers explicit config over env for key and base URL', async () => { vi.stubEnv('DEEPSEEK_API_KEY', 'env-key') vi.stubEnv('DEEPSEEK_BASE_URL', 'http://env-host:1') const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url) // harness passes explicit config await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }) expect(server.requests).toHaveLength(1) // hit the explicit URL, not env }) it('uses DEEPSEEK_BASE_URL when config omits baseURL', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) vi.stubEnv('DEEPSEEK_BASE_URL', server.url) const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(LlmDeepSeek, { apiKey: 'k' }) await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }) expect(server.requests).toHaveLength(1) }) it('defaults to the public base URL without config or env', async () => { vi.stubEnv('DEEPSEEK_API_KEY', 'k') vi.stubEnv('DEEPSEEK_BASE_URL', undefined) const ctx = new Context() 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' }]) }) it('adapter is constructible directly for embedding', async () => { const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1' }) expect(adapter).toBeInstanceOf(DeepSeekAdapter) await expect(adapter.listModels('deepseek')).resolves.toEqual([]) }) it('rejects invalid idle watchdog bounds for direct and plugin composition', async () => { expect(() => new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1', streamIdleTimeoutMs: Number.POSITIVE_INFINITY, })).toThrow(/streamIdleTimeoutMs.*positive finite/) expect(() => new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1', streamIdleTimeoutMs: MAX_TIMER_DELAY_MS + 1, })).toThrow(/streamIdleTimeoutMs.*no greater/) const ctx = new Context() await ctx.plugin(LlmService) await expect(ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', streamIdleTimeoutMs: 0, })).rejects.toThrow(/streamIdleTimeoutMs/) await expect(ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1', streamIdleTimeoutMs: MAX_TIMER_DELAY_MS + 1, })).rejects.toThrow(/streamIdleTimeoutMs/) }) })