Skip to content

Model selection decides, for one agent's next request, the provider route and the provider-owned model id (plus an optional reasoning effort). It is a resolution chain that runs at request time and sits at the meeting point of several packages: the default model provider, the agent loop's agent/request waterfall, the LLM adapter registry, the session's logged request header, and the UI's selection surface.

PackageOwns
@deepseek-ai/dsh-agent-default-modelctx.agentDefaultModel — the global default selection, persisted through settings
@deepseek-ai/dsh-agentModelSelection, installModelSelection, the agent/request waterfall
@deepseek-ai/dsh-agent-loopThe request-build path that resolves and freezes config
@deepseek-ai/dsh-llmThe LlmRuntime adapter registry, prepareCall, stream
@deepseek-ai/dsh-sessionThe logged request/header and request/context
@deepseek-ai/dsh-llm-retryRetry policy on agent/request-error
@deepseek-ai/dsh-client-ui-model-selectionThe browser half; selection surface

The resolution chain

The concrete wiring is visible in the headless bundle (packages/bundle/headless/src/index.ts), which is the smallest runnable composition:

session-specific selection (ModelSelectionRef, node half)
        │  installModelSelection(agentCtx, ref)

agent default  ←  agentDefaultModel.currentSelection()
        │  seeded into AgentOptions { provider, model } at creation

agent/request waterfall (agent loop buildRequest)
        │  frozen LlmCallConfig

LlmRuntime.prepareCall(config) → adapter + defaults


provider.stream(request)

Three tiers feed the resolved request:

  1. Global defaultctx.agentDefaultModel, the AgentDefaultModelConfig service. Its currentSelection() returns the { provider, model, reasoningEffort? } read from the composition entry or, when a settings provider is mounted, from the live settings document.
  2. Agent default — the default is copied into AgentOptions.provider/AgentOptions.model at agent creation, so ctx.agents.create({ agentOptions }) already carries a concrete route.
  3. Session/step selectionctx.agentDefaultModel seeds a ModelSelectionRef whose current value a session can replace; installModelSelection(agentCtx, ref) couples that mutable selection to prompt assembly and request routing.

The default model provider

The task's "default model provider" is, in the source, the AgentDefaultModelConfig service (packages/core/agent-default-model/src/index.ts), mounted as ctx.agentDefaultModel. It owns a mutable, settings-backed default:

ts
export class AgentDefaultModelConfig extends Service {
  private source: () => AgentDefaultModelSettings

  constructor(ctx, config: Config) {
    super(ctx, 'agentDefaultModel')
    const entry: AgentDefaultModelSettings = { provider: config.provider, model: config.model }
    this.source = () => entry
    installSettingsSection(ctx, AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE,
      AGENT_DEFAULT_MODEL_SETTINGS_SCHEMA, entry, { setSource: (current) => { this.source = current }, onChange: () => {} })
  }

  currentSelection(): ModelSelection {
    return selection(this.source())
  }

  async saveSelection(next: ModelSelection): Promise<void> {
    await this.ctx.get('settings')?.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
      provider: next.provider, model: next.model,
      ...next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) },
    })
  }
}

The settings namespace is agent-default-model, schema carrying provider (required), model (required), and optional reasoningEffort. currentSelection() always reads through this.source(), so a settings change is reflected live without any registration-level rebuild.

The injected consumers (agentDefaultModel, agents, sessions) seed a live agent:

ts
const selection = defaultModel.currentSelection()
const { agent } = await agents.create({
  sessionId: SessionId(`session-${randomUUID()}`),
  meta: { cwd: process.cwd() },
  agentOptions: { provider: selection.provider, model: selection.model },
  setup: (agentCtx) => {
    const selected: ModelSelectionRef = { current: selection, assembled: undefined }
    installModelSelection(agentCtx, selected)
  },
})

How a session's selection overrides

installModelSelection (in packages/core/agent/src/model-selection.ts) installs two scoped waterfall listeners. The first snapshots the selected model before prompt assembly and sets provider/model as prompt variables; the second forces provider/model (and effort) onto the LlmCallConfig at the agent/request boundary. The current/assembled split means a concurrent switch applies to a later step rather than splitting the two surfaces:

ts
const disposeRequest = agentCtx.on('agent/request', async (_payload, next): Promise<LlmCallConfig> => {
  const resolved = await next()
  const selected = selection.assembled
  if (selected === undefined) return resolved
  const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
  return {
    ...withoutInheritedEffort,
    provider: selected.provider,
    model: selected.model,
    ...selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort },
  }
})

How the loop freezes a request

buildRequest in the agent loop reads the persisted request header, runs the agent/request waterfall to a final LlmCallConfig, and requires both a provider and a model (or throws "has no provider/model"). It then logs the request/header (reason initial, resume, or change) and the request/context (provider/model/contextWindow) onto the session, and binds prepareCall so adapter defaults and retry policy are captured with the exact route:

ts
const proposedConfig = await this.dispatch.waterfall(
  'agent/request', { turn, step, signal }, () => Promise.resolve(seedConfig),
)
...
preparedCall = await this.loopCtx.llm.prepareCall(proposedConfig, signal)
config = preparedCall.config
...
const header = canonicalHeader({ config, ...adapterDefaults, ...system, ...tools })

Provider registration and models

ctx.llm.registerAdapter(providers, adapter) (the LlmRuntime in packages/llm/llm/src/index.ts) registers a provider route; it throws DUPLICATE_ADAPTER all-or-nothing and returns a disposer carrying AdapterRegistrationHandle.replace for atomic route replacement. Each adapter also exposes providerRetryPolicy(provider), listModels(provider), and resolveModel(info) metadata, so model ids are provider-owned opaque strings — the core never interprets them. prepareCall captures PreparedLlmCall (frozen config with materialized defaults, retryPolicy, and context.contextWindow). The request surface is GenerateOptions (provider, model, messages, system, tools, reasoningEffort, maxTokens); the request goes out through LlmAdapter.stream(options).

Retry hooks

Retry policy is registration-bound: llm-retry implements on agent/request-error. Each scheduled retry is durable before its cancellable wait, and the policy comes from the adapter registration's providerRetryPolicy. Model-request failures surface as structured LlmFailure facts through the same waterfall.

Where API keys come from

Model calls delegate credential resolution to the credential seam, not the core. The deepseek provider adapter (packages/llm/llm-deepseek/src/adapter.ts) resolves its API key per stream call through the optional ctx.credentials seam — a CredentialRef — and injects it as authorization: Bearer <apiKey> at its HTTP boundary (one resolution per call so a changed credential is respected). The core LlmRuntime is transport-agnostic; a deployment mounts the credential seam or supplies the key through configuration.

Further reading

  • The agent loop — the agent/request waterfall and buildRequest freeze step.
  • System prompt assembly — how the selected model becomes provider/model prompt variables.
  • Session management — the logged request/header and request/context.
  • Scope system — the scoped listeners installModelSelection registers.
  • Repo source: packages/core/agent-default-model/src/index.ts, packages/core/agent/src/model-selection.ts, packages/bundle/headless/src/index.ts, packages/llm/llm/src/index.ts.
  • Official scaffold: docs/config-catalog.md (model keys), docs/subsystems/credentials.md.