| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- import { describe, expect, it } from 'vitest'
- import { Context } from '@deepseek-ai/cordis'
- import LlmRuntime, {
- CallId,
- 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 { 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() {
- const ctx = new Context()
- await ctx.plugin(LlmRuntime)
- await ctx.plugin(SystemPrompt)
- await ctx.plugin(ToolRuntime)
- await ctx.plugin(SubagentRuntime)
- const fiber = await ctx.plugin(tool, { provider: 'unused', enableModelSelection: true })
- return { ctx, fiber }
- }
- let counter = 0
- function call(ctx: Context, args: unknown) {
- return ctx.tools.execute({
- signal: testToolSignal,
- callId: CallId(`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)
- await ctx.plugin(tool, { provider: 'unused', enableModelSelection: true })
- 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()
- await expect(ctx.plugin(tool, {
- provider: 'another-unused',
- toolName: 'subagent_other',
- enableModelSelection: true,
- }).then(() => undefined)).rejects.toThrow('tool "list_subagent_models" is already registered')
- })
- it('lists registered providers and follows live registration changes', async () => {
- const { ctx, fiber } = 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')
- await fiber.dispose()
- expect(ctx.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('renders an empty advertised model list', async () => {
- const { ctx } = await setupListTool()
- 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'], 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: 'available providers: (none)' },
- ])('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()
- 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')
- })
- })
|