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 default — ctx.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 selection — ctx.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.