Provider Backends Guide

Copy Markdown View Source

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:

  • 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.

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:

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.