model-selection.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /** Child LLM route selection for the subagent tool. */
  2. import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
  3. import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
  4. import type { AgentOptions } from '@deepseek-ai/dsh-agent'
  5. /** Model-facing child LLM route fields. */
  6. export interface DelegationModelRequest {
  7. readonly provider?: string
  8. readonly model?: string
  9. readonly reasoning_effort?: string
  10. }
  11. /**
  12. * Whether a call explicitly selects any child LLM value.
  13. * @param request - Model-facing route fields from the tool call.
  14. * @returns Whether at least one route or effort field is present.
  15. */
  16. export function hasDelegationModelRequest(request: DelegationModelRequest): boolean {
  17. return request.provider !== undefined
  18. || request.model !== undefined
  19. || request.reasoning_effort !== undefined
  20. }
  21. /** Reject an empty model-facing route value at the tool JSON boundary. */
  22. function assertNonEmpty(value: string | undefined, field: keyof DelegationModelRequest): void {
  23. if (value !== undefined && value.length === 0) {
  24. throw new Error(`child LLM \`${field}\` must be non-empty`)
  25. }
  26. }
  27. /**
  28. * Merge model-supplied selection fields over configured child defaults.
  29. * Provider and model form one route and must be supplied together. Changing
  30. * that route without an effort clears the configured route-owned effort.
  31. * @param parentOptions - Current parent values that supply missing child values.
  32. * @param configured - Tool-instance child defaults.
  33. * @param request - Model-facing route override.
  34. * @param enabled - Whether this tool instance permits model-facing selection.
  35. * @returns Child Agent options, preserving omission when no layer contributes one.
  36. */
  37. export function requestedAgentOptions(
  38. parentOptions: AgentOptions,
  39. configured: AgentOptions | undefined,
  40. request: DelegationModelRequest,
  41. enabled: boolean,
  42. ): AgentOptions | undefined {
  43. if (!hasDelegationModelRequest(request)) return configured
  44. if (!enabled) {
  45. throw new Error('child model selection is disabled for this tool instance')
  46. }
  47. assertNonEmpty(request.provider, 'provider')
  48. assertNonEmpty(request.model, 'model')
  49. assertNonEmpty(request.reasoning_effort, 'reasoning_effort')
  50. if ((request.provider === undefined) !== (request.model === undefined)) {
  51. throw new Error('child LLM `provider` and `model` must be supplied together')
  52. }
  53. const baselineProvider = configured?.provider ?? parentOptions.provider
  54. const baselineModel = configured?.model ?? parentOptions.model
  55. const routeChanged = request.provider !== undefined
  56. && (request.provider !== baselineProvider || request.model !== baselineModel)
  57. const { reasoningEffort: _configuredReasoningEffort, ...configuredWithoutReasoning } = configured ?? {}
  58. return {
  59. ...routeChanged && request.reasoning_effort === undefined ? configuredWithoutReasoning : configured,
  60. ...request.provider === undefined ? {} : { provider: request.provider, model: request.model },
  61. ...request.reasoning_effort === undefined
  62. ? {}
  63. : { reasoningEffort: ReasoningEffortId(request.reasoning_effort) },
  64. }
  65. }
  66. /**
  67. * Whether configured Agent options require route validation before delegation.
  68. * @param options - Tool-instance child defaults.
  69. * @returns Whether configured provider, model, or effort values must be resolved.
  70. */
  71. export function hasConfiguredLlmSelection(options: AgentOptions | undefined): boolean {
  72. return options?.provider !== undefined
  73. || options?.model !== undefined
  74. || options?.reasoningEffort !== undefined
  75. }
  76. /**
  77. * Resolve an effective child route through its live adapter before the child is
  78. * created. The LLM runtime owns provider lookup, exact-model metadata, effort
  79. * validation, and adapter defaults.
  80. * @param llm - Live LLM runtime.
  81. * @param parentOptions - Current parent values whose compatible fields the child inherits.
  82. * @param requested - Per-child options after request/config merging.
  83. * @param signal - Tool-call cancellation signal.
  84. */
  85. export async function preflightChildLlmRoute(
  86. llm: LlmRuntime,
  87. parentOptions: AgentOptions,
  88. requested: AgentOptions | undefined,
  89. signal: AbortSignal,
  90. ): Promise<void> {
  91. const provider = requested?.provider ?? parentOptions.provider
  92. const model = requested?.model ?? parentOptions.model
  93. if (provider === undefined || model === undefined) {
  94. throw new Error('cannot select child LLM values without an effective provider and model')
  95. }
  96. const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model
  97. const reasoningEffort = requested?.reasoningEffort
  98. ?? (routeChanged ? undefined : parentOptions.reasoningEffort)
  99. await llm.resolveCallConfig({
  100. provider,
  101. model,
  102. ...reasoningEffort === undefined ? {} : { reasoningEffort },
  103. }, signal)
  104. }