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:
@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:
providerlanebackendruntimecapabilitiessessionobservability
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: :coreresolves toASM.ProviderBackend.Corelane: :sdkresolves toASM.ProviderBackend.SDKonly when the runtime kit is available locallylane: :autoprefers 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
:coreand local:sdkboth preserve the same normalizedexecution_surfacecontract and itsExecutionSurfacemetadata :remote_noderemains a separate ASM execution mode, not another execution surface
Observability
Backend choice is visible in run/event metadata:
requested_lanepreferred_lanelanebackendexecution_modelane_reasonlane_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/1ASM.Extensions.ProviderSDK.provider_extensions/1ASM.Extensions.ProviderSDK.available_provider_extensions/1ASM.Extensions.ProviderSDK.provider_capabilities/1ASM.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: :ossoss_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_payloadonly
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.