Files
deepseek-harness/packages/subagent/tool-subagent/tests/list-models.spec.ts
T

249 lines
9.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmRuntime, {
ToolCallId,
LlmAdapter,
ReasoningEffortId,
} from '@deepseek-ai/dsh-llm'
import type {
GenerateOptions,
LlmModelInfo,
LlmResolvedModelInfo,
StreamChunk,
} from '@deepseek-ai/dsh-llm'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as tool from '../src/index.ts'
import { registerListSubagentModels } from '../src/list-models.ts'
import { testToolSignal, text } from './harness.ts'
class CatalogAdapter extends LlmAdapter {
constructor(private readonly empty = false) {
super()
}
override providerInfo(provider: string) {
return { id: provider, name: `${provider.toUpperCase()} API` }
}
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
if (this.empty) return Promise.resolve([])
return Promise.resolve([
{ provider, id: 'fast', name: 'Fast', description: 'Focused work.' },
{ provider, id: 'plain', name: 'Plain' },
])
}
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
if (model === 'plain') return Promise.resolve({ provider, id: model, name: 'Plain' })
return Promise.resolve({
provider,
id: model,
name: 'Fast',
description: 'Focused work.',
reasoning: {
efforts: [
{ id: ReasoningEffortId('low'), name: 'Low' },
{ id: ReasoningEffortId('high'), name: 'High', description: 'Quality first.' },
],
defaultEffort: ReasoningEffortId('high'),
},
})
}
stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
return (async function* () { yield { type: 'finish' as const, reason: { kind: 'stop' as const } } })()
}
}
async function setupListTool(routes = [
{ provider: 'alpha', model: 'fast' },
{ provider: 'alpha', model: 'plain' },
{ provider: 'beta', model: 'fast' },
{ provider: 'beta', model: 'plain' },
]) {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
registerListSubagentModels(ctx, { routes })
return ctx
}
async function setupAllowedListTool() {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
registerListSubagentModels(ctx, {
routes: [
{ provider: 'alpha', model: 'fast' },
{ provider: 'alpha', model: 'unlisted' },
{ provider: 'missing', model: 'hidden' },
],
})
return ctx
}
let counter = 0
function call(ctx: Context, args: unknown) {
return ctx.tools.execute({
signal: testToolSignal,
callId: ToolCallId(`list-models-${++counter}`),
name: 'list_subagent_models',
arguments: args,
})
}
describe('list_subagent_models', () => {
it('is omitted unless its delegation-tool instance owns discovery', async () => {
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(tool, { provider: 'unused' })
expect(ctx.tools.get('list_subagent_models')).toBeUndefined()
})
it('stays registered without the optional LLM service and rejects discovery calls', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
registerListSubagentModels(ctx, { routes: [{ provider: 'alpha', model: 'fast' }] })
const result = await call(ctx, {})
expect(result.isError).toBe(true)
expect(text(result)).toContain('`llm` service is unavailable')
})
it('rejects two discovery-owning instances in one tool scope', async () => {
const ctx = await setupListTool()
expect(() => {
registerListSubagentModels(ctx, { routes: [{ provider: 'alpha', model: 'fast' }] })
}).toThrow('tool "list_subagent_models" is already registered')
})
it('lists registered providers and follows live registration changes', async () => {
const ctx = await setupListTool()
const empty = await call(ctx, {})
expect(empty.isError).toBe(false)
expect(text(empty)).toBe('(no LLM providers)')
const registration = ctx.llm.registerAdapter(['alpha'], new CatalogAdapter())
const providers = await call(ctx, {})
expect(providers.isError).toBe(false)
expect(text(providers)).toBe('alpha — ALPHA API')
registration.replace(['beta'])
const changed = await call(ctx, {})
expect(text(changed)).toBe('beta — BETA API')
const tools = ctx.tools
await ctx.fiber.dispose()
expect(tools.get('list_subagent_models')).toBeUndefined()
})
it('lists one provider\'s advertised models without treating the catalog as a whitelist', async () => {
const ctx = await setupListTool()
ctx.llm.registerAdapter(['alpha'], new CatalogAdapter())
const result = await call(ctx, { provider: 'alpha' })
expect(result.isError).toBe(false)
expect(text(result)).toBe('alpha/fast — Fast: Focused work.\nalpha/plain — Plain')
})
it('intersects provider and model discovery with the Session allowlist', async () => {
const ctx = await setupAllowedListTool()
ctx.llm.registerAdapter(['alpha', 'beta'], new CatalogAdapter())
expect(text(await call(ctx, {}))).toBe('alpha — ALPHA API')
expect(text(await call(ctx, { provider: 'alpha' }))).toBe('alpha/fast — Fast: Focused work.')
expect(text(await call(ctx, { provider: 'alpha', model: 'unlisted' })))
.toContain('alpha/unlisted — Fast')
const denied = await call(ctx, { provider: 'alpha', model: 'plain' })
expect(denied.isError).toBe(true)
expect(text(denied)).toContain('is not allowed for this Session')
})
it('rejects an unauthorized provider before calling its adapter catalog', async () => {
const ctx = await setupListTool([{ provider: 'alpha', model: 'fast' }])
const adapter = new CatalogAdapter()
const listModels = vi.spyOn(adapter, 'listModels')
ctx.llm.registerAdapter(['alpha', 'secret'], adapter)
const result = await call(ctx, { provider: 'secret' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('provider "secret" is not allowed for this Session')
expect(listModels).not.toHaveBeenCalled()
})
it('renders an empty advertised model list', async () => {
const ctx = await setupListTool([{ provider: 'alpha', model: 'fast' }])
ctx.llm.registerAdapter(['alpha'], new CatalogAdapter(true))
const result = await call(ctx, { provider: 'alpha' })
expect(result.isError).toBe(false)
expect(text(result)).toBe('(no advertised models for alpha)')
})
it('inspects exact-model efforts, descriptions, and defaults', async () => {
const ctx = await setupListTool()
ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter())
const result = await call(ctx, { provider: 'alpha', model: 'fast' })
expect(result.isError).toBe(false)
expect(text(result)).toBe(
'alpha/fast — Fast: Focused work.\nReasoning efforts:\n'
+ 'low — Low\nhigh (default) — High: Quality first.',
)
})
it('renders exact models without reasoning metadata', async () => {
const ctx = await setupListTool()
ctx.llm.registerAdapter(['alpha'], new CatalogAdapter())
const result = await call(ctx, { provider: 'alpha', model: 'plain' })
expect(result.isError).toBe(false)
expect(text(result)).toBe('alpha/plain — Plain\nReasoning efforts:\n(no advertised reasoning efforts)')
})
it.each([
{ args: { model: 'fast' }, expected: '`model` requires `provider`' },
{ args: { provider: '' }, expected: '`provider` must be non-empty' },
{ args: { provider: 'missing' }, expected: 'is not allowed for this Session' },
])('rejects incomplete or unavailable provider requests', async ({ args, expected }) => {
const ctx = await setupListTool()
const result = await call(ctx, args)
expect(result.isError).toBe(true)
expect(text(result)).toContain(expected)
})
it('rejects an empty exact model after resolving the provider', async () => {
const ctx = await setupListTool()
ctx.llm.registerAdapter(['alpha'], new CatalogAdapter())
const result = await call(ctx, { provider: 'alpha', model: '' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('`model` must be non-empty')
})
it('reports registered alternatives for an unavailable provider', async () => {
const ctx = await setupListTool([
{ provider: 'alpha', model: 'fast' },
{ provider: 'missing', model: 'fast' },
])
ctx.llm.registerAdapter(['alpha'], new CatalogAdapter())
const result = await call(ctx, { provider: 'missing' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('available providers: alpha')
expect(text(result)).not.toContain('secret')
})
it('reports no available provider when the authorized registry intersection is empty', async () => {
const ctx = await setupListTool([{ provider: 'missing', model: 'fast' }])
const result = await call(ctx, { provider: 'missing' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('available providers: (none)')
})
})