list-models.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. /** Model-facing discovery of LLM routes available to child Agents. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import type LlmRuntime from '@deepseek-ai/dsh-llm'
  4. import type { LlmProviderInfo } from '@deepseek-ai/dsh-llm'
  5. import { defineTool } from '@deepseek-ai/dsh-tools'
  6. import type { ModelSelectionPolicy } from './model-selection.ts'
  7. interface ListSubagentModelsRequest {
  8. readonly provider?: string
  9. readonly model?: string
  10. }
  11. /** Resolve one registered provider with a model-correctable diagnostic. */
  12. function registeredProvider(
  13. llm: LlmRuntime,
  14. policy: ModelSelectionPolicy,
  15. providerId: string,
  16. ): LlmProviderInfo {
  17. const providers = llm.listProviders()
  18. const provider = providers.find(candidate => candidate.id === providerId)
  19. if (provider !== undefined) return provider
  20. const available = providers
  21. .filter(candidate => policy.routes.some(route => route.provider === candidate.id))
  22. .map(candidate => candidate.id)
  23. .join(', ') || '(none)'
  24. throw new Error(`LLM provider "${providerId}" is not registered; available providers: ${available}`)
  25. }
  26. /** Render one advertised or resolved model. */
  27. function modelLine(provider: string, model: { id: string; name: string; description?: string }): string {
  28. return `${provider}/${model.id} — ${model.name}${model.description === undefined ? '' : `: ${model.description}`}`
  29. }
  30. /** Read the requested provider, advertised models, or exact-model efforts. */
  31. async function listSubagentModels(
  32. ctx: Context,
  33. policy: ModelSelectionPolicy,
  34. request: ListSubagentModelsRequest,
  35. signal: AbortSignal,
  36. ): Promise<string> {
  37. const llm = ctx.get('llm')
  38. if (llm === undefined) {
  39. throw new Error('cannot discover child LLM routes because the `llm` service is unavailable')
  40. }
  41. if (request.model !== undefined && request.provider === undefined) {
  42. throw new Error('`model` requires `provider`')
  43. }
  44. if (request.provider === undefined) {
  45. const providers = llm.listProviders()
  46. .filter(provider => policy.routes.some(route => route.provider === provider.id))
  47. return providers.length === 0
  48. ? '(no LLM providers)'
  49. : providers.map(provider => `${provider.id} — ${provider.name}`).join('\n')
  50. }
  51. if (request.provider.length === 0) throw new Error('`provider` must be non-empty')
  52. const allowedRoutes = policy.routes.filter(route => route.provider === request.provider)
  53. if (allowedRoutes.length === 0) {
  54. throw new Error(`LLM provider "${request.provider}" is not allowed for this Session`)
  55. }
  56. const provider = registeredProvider(llm, policy, request.provider)
  57. if (request.model === undefined) {
  58. const models = (await llm.listModels(provider.id))
  59. .filter(model => allowedRoutes.some(route => route.model === model.id))
  60. return models.length === 0
  61. ? `(no advertised models for ${provider.id})`
  62. : models.map(model => modelLine(provider.id, model)).join('\n')
  63. }
  64. if (request.model.length === 0) throw new Error('`model` must be non-empty')
  65. if (!allowedRoutes.some(route => route.model === request.model)) {
  66. throw new Error(`child LLM route "${provider.id}/${request.model}" is not allowed for this Session`)
  67. }
  68. const model = await llm.resolveModelInfo(provider.id, request.model, signal)
  69. const efforts = model.reasoning?.efforts.map(effort => (
  70. `${effort.id}${model.reasoning?.defaultEffort === effort.id ? ' (default)' : ''} — ${effort.name}`
  71. + (effort.description === undefined ? '' : `: ${effort.description}`)
  72. )).join('\n') || '(no advertised reasoning efforts)'
  73. return `${modelLine(provider.id, model)}\nReasoning efforts:\n${efforts}`
  74. }
  75. /**
  76. * Register `list_subagent_models` for one owning delegation-tool instance.
  77. * @param ctx - Context whose tool registry owns the fixed discovery definition.
  78. * @param policy - Route policy captured for this Session.
  79. */
  80. export function registerListSubagentModels(ctx: Context, policy: ModelSelectionPolicy): void {
  81. ctx.tools.register(defineTool({
  82. name: 'list_subagent_models',
  83. description:
  84. 'Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list '
  85. + 'registered providers, with `provider` to list its advertised models, or with `provider` and `model` '
  86. + 'to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may '
  87. + 'accept an unlisted model id. Use the returned ids with a delegation tool\'s `provider`, `model`, and '
  88. + '`reasoning_effort` fields.',
  89. parameters: {
  90. provider: {
  91. type: 'string',
  92. description: 'Registered LLM provider id. Omit to list providers.',
  93. },
  94. model: {
  95. type: 'string',
  96. description: 'Exact model id to inspect. Requires provider; omit to list that provider\'s advertised models.',
  97. },
  98. },
  99. output: {
  100. schema: { type: 'string' },
  101. render: (_args, result) => [{ type: 'text', text: result }],
  102. },
  103. execute(args, exec) {
  104. return listSubagentModels(ctx, policy, args, exec.signal)
  105. },
  106. }))
  107. }