Current section
Files
Jump to
Current section
Files
lib/asm/provider.ex
defmodule ASM.Provider do
@moduledoc """
Provider definition data used by the backend registry.
"""
alias ASM.Error
alias ASM.Provider.ExampleSupport
alias ASM.Provider.Profile
alias CliSubprocessCore.ProviderFeatures, as: CoreProviderFeatures
@enforce_keys [
:name,
:display_name,
:core_profile,
:example_support,
:options_schema,
:profile,
:feature_manifest
]
defstruct [
:name,
:display_name,
:core_profile,
:sdk_runtime,
:example_support,
:options_schema,
:profile,
:feature_manifest,
aliases: [],
metadata: %{}
]
@type provider_name ::
:amp | :antigravity | :claude | :codex | :codex_exec | :cursor | atom()
@provider_names_by_string %{
"amp" => :amp,
"antigravity" => :antigravity,
"claude" => :claude,
"codex" => :codex,
"codex_exec" => :codex,
"cursor" => :cursor
}
@type t :: %__MODULE__{
name: provider_name(),
display_name: String.t(),
core_profile: module(),
sdk_runtime: module() | nil,
example_support: ExampleSupport.t(),
options_schema: keyword(),
profile: Profile.t(),
feature_manifest: map(),
aliases: [provider_name()],
metadata: map()
}
@spec supported_providers() :: [provider_name()]
def supported_providers, do: Map.keys(providers())
@spec example_support(t() | provider_name()) :: {:ok, ExampleSupport.t()} | {:error, Error.t()}
def example_support(provider_or_name) do
with {:ok, %__MODULE__{} = provider} <- resolve(provider_or_name) do
{:ok, provider.example_support}
end
end
@spec example_support!(t() | provider_name()) :: ExampleSupport.t()
def example_support!(provider_or_name) do
case example_support(provider_or_name) do
{:ok, %ExampleSupport{} = example_support} ->
example_support
{:error, %Error{} = error} ->
raise ArgumentError, Exception.message(error)
end
end
@spec feature_manifest(t() | provider_name()) :: {:ok, map()} | {:error, Error.t()}
def feature_manifest(provider_or_name) do
with {:ok, %__MODULE__{} = provider} <- resolve(provider_or_name) do
{:ok, provider.feature_manifest}
end
end
@spec feature_manifest!(t() | provider_name()) :: map()
def feature_manifest!(provider_or_name) do
case feature_manifest(provider_or_name) do
{:ok, manifest} ->
manifest
{:error, %Error{} = error} ->
raise ArgumentError, Exception.message(error)
end
end
@spec supports_feature?(t() | provider_name(), atom()) :: boolean()
def supports_feature?(provider_or_name, feature) when is_atom(feature) do
case feature_manifest(provider_or_name) do
{:ok, manifest} ->
get_in(manifest, [feature, :supported?]) == true
{:error, _error} ->
false
end
end
@spec resolve(t() | provider_name() | String.t()) :: {:ok, t()} | {:error, Error.t()}
def resolve(%__MODULE__{} = provider), do: {:ok, provider}
def resolve(name) when is_atom(name) do
case find_provider(name) do
{:ok, provider} ->
{:ok, provider}
:error ->
{:error,
Error.new(
:config_invalid,
:config,
"Unknown provider: #{inspect(name)}",
cause: %{provider: name}
)}
end
end
def resolve(name) when is_binary(name) do
case provider_name_from_string(name) do
{:ok, provider_name} ->
resolve(provider_name)
:error ->
{:error,
Error.new(
:config_invalid,
:config,
"Unknown provider: #{inspect(name)}",
cause: %{provider: name}
)}
end
end
def resolve(other) do
{:error,
Error.new(
:config_invalid,
:config,
"Provider must be an atom or %ASM.Provider{}, got: #{inspect(other)}",
cause: %{provider: other}
)}
end
@spec resolve!(t() | provider_name()) :: t()
def resolve!(provider_or_name) do
case resolve(provider_or_name) do
{:ok, provider} ->
provider
{:error, %Error{} = error} ->
raise ArgumentError, Exception.message(error)
end
end
defp find_provider(name) do
case Map.fetch(providers(), normalize_name(name)) do
{:ok, provider} -> {:ok, provider}
:error -> find_alias(name)
end
end
defp find_alias(name) do
Enum.find_value(providers(), :error, fn {_id, provider} ->
if name in provider.aliases do
{:ok, provider}
else
false
end
end)
end
defp providers do
%{
claude: %__MODULE__{
name: :claude,
display_name: "Claude CLI",
core_profile: CliSubprocessCore.ProviderProfiles.Claude,
sdk_runtime: :"Elixir.ClaudeAgentSDK.Runtime.CLI",
example_support: %ExampleSupport{
cli_command: "claude",
cli_path_env: "CLAUDE_CLI_PATH",
install_hint: "npm install -g @anthropic-ai/claude-code",
model_env: "ASM_CLAUDE_MODEL",
example_default_model: nil,
sdk_app: :claude_agent_sdk,
sdk_repo_dir: "claude_agent_sdk",
sdk_root_env: "CLAUDE_AGENT_SDK_ROOT"
},
options_schema: ASM.Options.Claude.schema(),
feature_manifest: feature_manifest_for(:claude),
profile:
Profile.new!(
max_concurrent_runs: 1,
max_queued_runs: 10
)
},
codex: %__MODULE__{
name: :codex,
display_name: "Codex CLI",
core_profile: CliSubprocessCore.ProviderProfiles.Codex,
sdk_runtime: :"Elixir.Codex.Runtime.Exec",
example_support: %ExampleSupport{
cli_command: "codex",
cli_path_env: "CODEX_PATH",
install_hint: "npm install -g @openai/codex",
model_env: "ASM_CODEX_MODEL",
example_default_model: nil,
sdk_app: :codex_sdk,
sdk_repo_dir: "codex_sdk",
sdk_root_env: "CODEX_SDK_ROOT",
sdk_cli_env: "CODEX_PATH"
},
options_schema: ASM.Options.Codex.schema(),
feature_manifest: feature_manifest_for(:codex),
aliases: [:codex_exec],
profile:
Profile.new!(
max_concurrent_runs: 1,
max_queued_runs: 10
)
},
amp: %__MODULE__{
name: :amp,
display_name: "Amp CLI",
core_profile: CliSubprocessCore.ProviderProfiles.Amp,
sdk_runtime: :"Elixir.AmpSdk.Runtime.CLI",
example_support: %ExampleSupport{
cli_command: "amp",
cli_path_env: "AMP_CLI_PATH",
install_hint: "npm install -g @sourcegraph/amp",
model_env: "ASM_AMP_MODEL",
example_default_model: nil,
sdk_app: :amp_sdk,
sdk_repo_dir: "amp_sdk",
sdk_root_env: "AMP_SDK_ROOT",
sdk_cli_env: "AMP_CLI_PATH"
},
options_schema: ASM.Options.Amp.schema(),
feature_manifest: feature_manifest_for(:amp),
profile:
Profile.new!(
max_concurrent_runs: 1,
max_queued_runs: 10
)
},
cursor: %__MODULE__{
name: :cursor,
display_name: "Cursor Agent CLI",
core_profile: CliSubprocessCore.ProviderProfiles.Cursor,
sdk_runtime: CursorCliSdk.Runtime.CLI,
example_support: %ExampleSupport{
cli_command: "agent",
cli_path_env: "CURSOR_CLI_PATH",
install_hint: "https://cursor.com/docs/cli/overview",
model_env: "ASM_CURSOR_MODEL",
example_default_model: nil,
sdk_app: :cursor_cli_sdk,
sdk_repo_dir: "cursor_cli_sdk",
sdk_root_env: "CURSOR_CLI_SDK_ROOT",
sdk_cli_env: "CURSOR_CLI_PATH"
},
options_schema: ASM.Options.Cursor.schema(),
feature_manifest: feature_manifest_for(:cursor),
profile:
Profile.new!(
max_concurrent_runs: 1,
max_queued_runs: 10
)
},
antigravity: %__MODULE__{
name: :antigravity,
display_name: "Antigravity CLI",
core_profile: CliSubprocessCore.ProviderProfiles.Antigravity,
sdk_runtime: :"Elixir.AntigravityCliSdk.Runtime.CLI",
example_support: %ExampleSupport{
cli_command: "agy",
cli_path_env: "ANTIGRAVITY_CLI_PATH",
install_hint: "Install the Antigravity CLI agent binary",
model_env: "ASM_ANTIGRAVITY_MODEL",
example_default_model: nil,
sdk_app: :antigravity_cli_sdk,
sdk_repo_dir: "antigravity_cli_sdk",
sdk_root_env: "ANTIGRAVITY_CLI_SDK_ROOT",
sdk_cli_env: "ANTIGRAVITY_CLI_PATH"
},
options_schema: ASM.Options.Antigravity.schema(),
feature_manifest: feature_manifest_for(:antigravity),
profile:
Profile.new!(
max_concurrent_runs: 1,
max_queued_runs: 10
)
}
}
end
defp normalize_name(:codex_exec), do: :codex
defp normalize_name(name), do: name
defp provider_name_from_string(name) when is_binary(name) do
name
|> String.trim()
|> String.downcase()
|> then(&Map.fetch(@provider_names_by_string, &1))
end
defp feature_manifest_for(provider) when is_atom(provider) do
ollama =
provider
|> CoreProviderFeatures.partial_feature!(:ollama)
|> Map.merge(%{
common_surface: true,
common_opts: [:ollama, :ollama_model, :ollama_base_url, :ollama_http, :ollama_timeout_ms]
})
%{ollama: ollama}
end
end