Files
deepseek-harness/packages/llm/llm/tests/service.spec.ts
Tianyi Cui be62ee14ba Merge remote-tracking branch 'origin/master' into codex/simp-prune-llm-contract
# Conflicts:
#	docs/cordis-catalog/services.md
#	packages/llm/llm-pi-ai/README.md
#	packages/support/llm-replay/tests/llm-replay.spec.ts
2026-07-18 11:55:12 +08:00

330 lines
13 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService, { GenerateOptions, LlmAdapter, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
class ScriptedAdapter extends LlmAdapter {
constructor(private script: StreamChunk[]) {
super()
}
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
yield * this.script
}
}
class RecordingAdapter extends ScriptedAdapter {
lastOptions: GenerateOptions | undefined
override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
this.lastOptions = options
yield * super.stream(options)
}
}
class CatalogAdapter extends ScriptedAdapter {
constructor(
private readonly provider: LlmProviderInfo,
private readonly models: readonly LlmModelInfo[],
) {
super(SCRIPT)
}
override providerInfo(_provider: string): LlmProviderInfo {
return this.provider
}
override listModels(_provider: string): Promise<readonly LlmModelInfo[]> {
return Promise.resolve(this.models)
}
}
const SCRIPT: StreamChunk[] = [
{ type: 'block-start', index: 0, blockType: 'text' },
{ type: 'text-delta', index: 0, text: 'hi' },
{ type: 'finish', reason: { kind: 'stop' } },
]
describe('LlmService', () => {
it('routes stream() to the registered adapter', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['test-provider'], new ScriptedAdapter(SCRIPT))
const chunks: StreamChunk[] = []
for await (const chunk of ctx.llm.stream({ provider: 'test-provider', model: 'test-model', messages: [] })) chunks.push(chunk)
expect(chunks).toEqual(SCRIPT)
})
it('throws NO_ADAPTER for unregistered providers', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await expect((async () => {
for await (const _ of ctx.llm.stream({ provider: 'nope', model: 'any-model', messages: [] })) { /* drain */ }
})()).rejects.toThrow('no adapter registered')
})
it('unregisters adapters when the owning fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
inner.llm.registerAdapter(['scoped-model'], new ScriptedAdapter(SCRIPT))
}, { inject: ['llm'] }))
expect(ctx.llm.listProviders()).toEqual([{ id: 'scoped-model', name: 'scoped-model' }])
await fiber.dispose()
expect(ctx.llm.listProviders()).toEqual([])
})
it('discovers detached provider and advisory model metadata', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const provider = { id: 'catalog', name: 'Catalog Provider' }
const model = { provider: 'catalog', id: 'fast', name: 'Fast', description: 'Low latency' }
ctx.llm.registerAdapter(['catalog'], new CatalogAdapter(provider, [model]))
const providers = ctx.llm.listProviders()
const models = await ctx.llm.listModels('catalog')
expect(providers).toEqual([provider])
expect(models).toEqual([model])
providers[0]!.name = 'mutated'
models[0]!.name = 'mutated'
provider.name = 'source mutated'
model.name = 'source mutated'
expect(ctx.llm.listProviders()).toEqual([{ id: 'catalog', name: 'Catalog Provider' }])
await expect(ctx.llm.listModels('catalog')).resolves.toEqual([{
provider: 'catalog', id: 'fast', name: 'source mutated', description: 'Low latency',
}])
})
it('defaults adapters to their route name and an empty advisory model list', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['plain'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'plain', name: 'plain' }])
await expect(ctx.llm.listModels('plain')).resolves.toEqual([])
await expect(ctx.llm.listModels('missing')).rejects.toMatchObject({ code: 'NO_ADAPTER' })
})
it.each([
[{ id: 1, name: 'Name' }, 'non-string id'],
[{ id: 'other', name: 'Name' }, 'mismatched id'],
[{ id: 'route', name: 1 }, 'non-string name'],
[{ id: 'route', name: '' }, 'empty name'],
] as const)('rejects invalid provider metadata atomically (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
const adapter = new CatalogAdapter(metadata as unknown as LlmProviderInfo, [])
expect(() => ctx.llm.registerAdapter(['route'], adapter)).toThrow(expect.objectContaining({ code: 'INVALID_ADAPTER' }))
expect(ctx.llm.listProviders()).toEqual([])
})
it.each([
[{ provider: 1, id: 'm', name: 'M' }, 'non-string provider'],
[{ provider: 'other', id: 'm', name: 'M' }, 'mismatched provider'],
[{ provider: 'route', id: 1, name: 'M' }, 'non-string id'],
[{ provider: 'route', id: '', name: 'M' }, 'empty id'],
[{ provider: 'route', id: 'm', name: 1 }, 'non-string name'],
[{ provider: 'route', id: 'm', name: '' }, 'empty name'],
[{ provider: 'route', id: 'm', name: 'M', description: 1 }, 'non-string description'],
] as const)('rejects invalid model metadata (%s: %s)', async (metadata, _label) => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
{ id: 'route', name: 'Route' },
[metadata as unknown as LlmModelInfo],
))
await expect(ctx.llm.listModels('route')).rejects.toMatchObject({ code: 'INVALID_CATALOG' })
})
it('rejects duplicate model ids in one provider catalog', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const model = { provider: 'route', id: 'same', name: 'Same' }
ctx.llm.registerAdapter(['route'], new CatalogAdapter({ id: 'route', name: 'Route' }, [model, model]))
await expect(ctx.llm.listModels('route')).rejects.toMatchObject({ code: 'INVALID_CATALOG' })
})
it('lets llm/stream waterfall listeners wrap the underlying stream', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['test-model'], new ScriptedAdapter(SCRIPT))
ctx.on('llm/stream', function (_options, next) {
const inner = next()
return (async function * () {
yield { type: 'block-start', index: 99, blockType: 'text' } satisfies StreamChunk
yield * inner
})()
})
const chunks: StreamChunk[] = []
for await (const chunk of ctx.llm.stream({ provider: 'test-model', model: 'dynamic-model', messages: [] })) chunks.push(chunk)
expect(chunks).toHaveLength(4)
expect(chunks[0]).toMatchObject({ index: 99 })
})
it('resolves the provider after llm/stream listeners have had a chance to route it', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const adapter = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['routed'], adapter)
ctx.on('llm/stream', (options, next) => {
options.provider = 'routed'
return next()
})
for await (const _chunk of ctx.llm.stream({ provider: 'initial', model: 'm', messages: [] })) { /* drain */ }
expect(adapter.lastOptions?.provider).toBe('routed')
})
it('keeps replay state when historical and target providers belong to the same adapter instance', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const adapter = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['historical', 'target'], adapter)
const replayState = { private: 'state' }
for await (const _chunk of ctx.llm.stream({
provider: 'target',
model: 'new-model',
messages: [{
role: 'assistant',
content: [{ type: 'text', text: 'old response' }],
provenance: { provider: 'historical', model: 'old-model', replayState },
}],
})) { /* drain */ }
expect(adapter.lastOptions?.messages[0]?.provenance).toEqual({
provider: 'historical', model: 'old-model', replayState,
})
})
it('strips replay state but preserves provenance when the target uses a different adapter instance', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['historical'], new RecordingAdapter(SCRIPT))
const target = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['target'], target)
for await (const _chunk of ctx.llm.stream({
provider: 'target',
model: 'new-model',
messages: [{
role: 'assistant',
content: [{ type: 'text', text: 'old response' }],
provenance: { provider: 'historical', model: 'old-model', replayState: { private: 'state' } },
}],
})) { /* drain */ }
expect(target.lastOptions?.messages[0]?.provenance).toEqual({ provider: 'historical', model: 'old-model' })
})
it('preserves immutability while stripping replay state from frozen requests', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['historical'], new RecordingAdapter(SCRIPT))
const target = new RecordingAdapter(SCRIPT)
ctx.llm.registerAdapter(['target'], target)
const options = Object.freeze({
provider: 'target',
model: 'new-model',
messages: [{
role: 'assistant' as const,
content: [{ type: 'text' as const, text: 'old response' }],
provenance: { provider: 'historical', model: 'old-model', replayState: { private: 'state' } },
}],
})
for await (const _chunk of ctx.llm.stream(options)) { /* drain */ }
expect(target.lastOptions).not.toBe(options)
expect(Object.isFrozen(target.lastOptions)).toBe(true)
expect(target.lastOptions?.messages[0]?.provenance).toEqual({ provider: 'historical', model: 'old-model' })
})
it('creates LlmError with a code for programmatic handling', () => {
const err = new LlmError('something went wrong', 'CUSTOM_CODE')
expect(err).toBeInstanceOf(Error)
expect(err.name).toBe('LlmError')
expect(err.message).toBe('something went wrong')
expect(err.code).toBe('CUSTOM_CODE')
})
it('LlmError extends the shared HarnessError base', async () => {
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
const cause = new Error('root cause')
const err = new LlmError('boom', 'AUTH', { cause })
expect(err).toBeInstanceOf(HarnessError)
expect(isHarnessError(err)).toBe(true)
expect(err.code).toBe('AUTH')
expect(err.cause).toBe(cause)
})
it('HarnessError carries a code, names itself by subclass, and chains cause', async () => {
const { HarnessError, isHarnessError } = await import('@deepseek-ai/dsh-llm')
const root = new Error('root cause')
const err = new HarnessError('wrapper', 'UNKNOWN', { cause: root })
expect(err).toBeInstanceOf(Error)
expect(err.name).toBe('HarnessError')
expect(err.code).toBe('UNKNOWN')
expect(err.cause).toBe(root)
expect(isHarnessError(err)).toBe(true)
expect(isHarnessError(root)).toBe(false)
expect(isHarnessError('nope')).toBe(false)
})
it('removes the adapter when the returned disposer is called', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const dispose = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'm1', name: 'm1' }])
dispose()
expect(ctx.llm.listProviders()).toEqual([])
})
it('rejects duplicate adapter registration with DUPLICATE_ADAPTER code', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
try {
ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect.fail('expected error')
} catch (error: unknown) {
expect(error).toBeInstanceOf(LlmError)
expect((error as LlmError).message).toContain('already registered')
expect((error as LlmError).code).toBe('DUPLICATE_ADAPTER')
}
})
it('rejects empty and internally duplicated provider registrations atomically', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const adapter = new ScriptedAdapter(SCRIPT)
expect(() => ctx.llm.registerAdapter([], adapter)).toThrow(expect.objectContaining({ code: 'INVALID_ADAPTER' }))
expect(() => ctx.llm.registerAdapter([''], adapter)).toThrow(expect.objectContaining({ code: 'INVALID_ADAPTER' }))
expect(() => ctx.llm.registerAdapter(['first', 'first'], adapter)).toThrow(expect.objectContaining({ code: 'DUPLICATE_ADAPTER' }))
expect(ctx.llm.listProviders()).toEqual([])
})
it('re-registers a model after its prior registration is disposed', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const dispose = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'm1', name: 'm1' }])
dispose()
expect(ctx.llm.listProviders()).toEqual([])
// The duplicate check is not wedged: the same model registers cleanly again.
const disposeAgain = ctx.llm.registerAdapter(['m1'], new ScriptedAdapter(SCRIPT))
expect(ctx.llm.listProviders()).toEqual([{ id: 'm1', name: 'm1' }])
disposeAgain()
expect(ctx.llm.listProviders()).toEqual([])
})
})