# Provider Backends Guide
ASM runs providers through a small backend contract instead of provider-specific
driver/parser stacks.
## Backend Roles
`ASM.ProviderBackend.Core` is the required baseline backend:
- starts `CliSubprocessCore.Session`
- works in `execution_mode: :local`
- works in `execution_mode: :remote_node`
- uses the built-in provider profiles from `cli_subprocess_core`
`ASM.ProviderBackend.SDK` is optional and additive:
- starts provider runtime kits when they are installed locally
- works only in `execution_mode: :local`
- preserves the same session/run/event model as the core backend
- preserves the same normalized execution-surface contract as the core backend
for local subprocess and SSH lanes
- never becomes a required dependency for ASM itself
- may exist without any separate ASM provider-native namespace, as with Gemini
and Amp today
## Common Contract
Both backends satisfy the same `ASM.ProviderBackend` behaviour:
```elixir
@callback start_run(map()) :: {:ok, pid(), ASM.ProviderBackend.Info.t()} | {:error, term()}
@callback send_input(pid(), iodata(), keyword()) :: :ok | {:error, term()}
@callback end_input(pid()) :: :ok | {:error, term()}
@callback interrupt(pid()) :: :ok | {:error, term()}
@callback close(pid()) :: :ok
@callback subscribe(pid(), pid(), reference()) :: :ok | {:error, term()}
@callback info(pid()) :: ASM.ProviderBackend.Info.t()
```
That keeps `ASM.Run.Server` lane-agnostic after resolution.
After `subscribe/3`, the kernel receives `%ASM.ProviderBackend.Event{}` messages
instead of matching provider or transport mailbox tags directly.
`ASM.ProviderBackend.Info` is the ASM-owned metadata contract consumed by the
kernel:
- `provider`
- `lane`
- `backend`
- `runtime`
- `capabilities`
- `session`
- `observability`
The `session` field may still contain backend/runtime details, but backend
adapters must strip raw delivery tags such as `session_event_tag` before that
data crosses into ASM kernel state.
## Backend Selection
`ASM.ProviderRegistry.resolve/2` chooses which backend module to use.
- `lane: :core` resolves to `ASM.ProviderBackend.Core`
- `lane: :sdk` resolves to `ASM.ProviderBackend.SDK` only when the runtime kit is available locally
- `lane: :auto` prefers the SDK lane when available and otherwise falls back to the core lane
`execution_mode` is applied after lane discovery. In the landed Phase 3
boundary, remote execution always uses the core backend even if `:auto`
preferred `:sdk`.
That split is intentional:
- local `:core` and local `:sdk` both preserve the same normalized
`execution_surface` contract and its `ExecutionSurface` metadata
- `:remote_node` remains a separate ASM execution mode, not another execution
surface
## Observability
Backend choice is visible in run/event metadata:
- `requested_lane`
- `preferred_lane`
- `lane`
- `backend`
- `execution_mode`
- `lane_reason`
- `lane_fallback_reason`
That metadata is merged into both streamed `%ASM.Event{}` values and the final
`%ASM.Result.metadata` projection.
## Provider-Native Extensions Stay Above The Backend Boundary
`ASM.ProviderRegistry` and the backend modules stop at normalized lane/runtime
selection.
Provider-native capability reporting now lives under
`ASM.Extensions.ProviderSDK`:
- `ASM.Extensions.ProviderSDK.extension/1`
- `ASM.Extensions.ProviderSDK.provider_extensions/1`
- `ASM.Extensions.ProviderSDK.available_provider_extensions/1`
- `ASM.Extensions.ProviderSDK.provider_capabilities/1`
- `ASM.Extensions.ProviderSDK.capability_report/0`
That keeps backend discovery focused on `:core` versus `:sdk`, while
provider-native surfaces such as Claude control semantics and Codex app-server,
MCP, realtime, and voice remain explicit optional seams above the kernel.
Gemini and Amp still affect backend lane availability through their optional
runtime kits, but they do not add separate ASM provider-native namespaces in
the current catalog.
For Claude specifically, `ASM.Extensions.ProviderSDK.Claude` can bridge ASM
config into `ClaudeAgentSDK.Client`, but the resulting control calls still live
on `ClaudeAgentSDK.Client.*` rather than the backend contract.
## Claude Backend-Specific Model Inputs
Claude is the first backend where ASM now forwards backend-specific model
inputs into the shared core model registry.
The relevant Claude provider fields are:
- `:provider_backend`
- `:external_model_overrides`
- `:anthropic_base_url`
- `:anthropic_auth_token`
- `:model`
Those values are still treated as value carriers only.
ASM does not validate Ollama models itself and does not build Ollama CLI env
itself. It forwards those values to
`CliSubprocessCore.ModelRegistry.build_arg_payload/3`, then passes the resolved
payload to either the core Claude profile or `ClaudeAgentSDK.Options`.
## Codex Backend-Specific Model Inputs
Codex now follows the same pattern for backend-aware model resolution.
Relevant Codex provider fields:
- `:provider_backend`
- `:model_provider`
- `:oss_provider`
- `:ollama_base_url`
- `:ollama_http`
- `:ollama_timeout_ms`
- `:model`
- `:reasoning_effort`
For the current local Ollama path, ASM callers should use:
- `provider_backend: :oss`
- `oss_provider: "ollama"`
- `model: "<local model id>"`
ASM still does not invent Codex backend flags locally. It forwards those inputs to
`CliSubprocessCore.ModelInput.normalize/3`, which in turn resolves through
`CliSubprocessCore.ModelRegistry.build_arg_payload/3` when the caller supplied
raw knobs instead of a payload. ASM then passes the finalized payload into
either the core Codex profile or `Codex.Options` / `Codex.Thread.Options` on
the SDK lane.
For Codex/Ollama, the shared core keeps `gpt-oss:20b` as the default validated
example model, but it also accepts other installed local model ids such as
`llama3.2`. The degraded-mode distinction for those non-default models is
metadata-driven rather than a hard rejection in ASM.
If a custom `ollama_base_url` is supplied, the finalized payload carries it in
payload-owned runtime data (`CODEX_OSS_BASE_URL`). Raw Ollama roots are
normalized to the OpenAI-compatible `/v1` base for Codex, so downstream core
and SDK transports can consume the payload alone after normalization.
## Gemini Backend-Specific Model Inputs
Gemini has a narrower surface than Claude or Codex.
Relevant Gemini provider fields:
- `:model`
When ASM bridges into `gemini_cli_sdk`, the Gemini SDK now consumes the shared
normalized payload instead of re-resolving over an explicit payload. Repo-local
`GEMINI_MODEL` defaults remain fallback inputs only when the caller did not
supply a payload.
## Amp Backend-Specific Model Inputs
Amp is intentionally payload-only for model input in the current stack.
Relevant Amp provider fields:
- `:model_payload` only
`amp_sdk` does not expose a second raw model/backend surface. ASM finalizes any
shared-core model selection before the Amp SDK boundary, and `AmpSdk.Types.Options.validate!/1`
only canonicalizes a supplied payload rather than inventing another resolution
path inside the Amp repo.