LlmCore can wrap any command-line LLM tool as a first-class provider. No Elixir code needed — just TOML configuration.

How It Works

CLI providers are declared in TOML with type = "cli". At runtime, LlmCore.LLM.CLIProvider builds the correct command-line invocation from the config: binary, flags, prompt position, system prompt strategy, output capture, and error handling.

A CLI provider is considered available when:

  1. enabled = true in its config block
  2. The declared binary is found in PATH

No API key or module loading required.

Built-in CLI Providers

These ship in priv/config/llm_core.toml and work out of the box when the binary is installed:

NameBinaryHighlights
claude_codeclaude--print, system prompt via --append-system-prompt-file, --add-dir
droiddroidexec subcommand, --auto high, --cwd, --worktree
pi_clipi--print, --provider, --thinking, system prompt file
kimi_clikimi-cliAgent-file YAML transform, --final-message-only, output stripping
codex_clicodexexec subcommand, --full-auto, file capture via --output-last-message
gemini_cligeminiModel selection, standard CLI interface

To override a built-in, define a [providers.<name>] block with the same ID and type = "cli" in a project or global TOML override. Your definition replaces the default entirely.

Adding a Custom CLI Provider

Here is a complete example:

[providers.my_tool]
type = "cli"
enabled = true
aliases = ["my-tool", "mt"]
default_model = "v2"

[providers.my_tool.cli]
binary = "my-tool"                     # required — must be in PATH
subcommand = "exec"                    # optional subcommand prepended to args
default_timeout = 60000                # ms (default: 1_800_000 = 30 min)
prompt_position = "last"              # "last" or "flagged"
prompt_transport = "last"             # "last", "flagged", or "stdin"
system_prompt_transport = "file_flag"  # "flag", "file_flag", "inline_fallback", or "unsupported"
install_hint = "pip install my-tool"   # shown when binary is missing
prefix_args = ["--no-interactive"]     # always prepended
auto_approve_args = ["--yes"]          # appended when auto_approve: true
sandbox_bypass_args = ["--unsafe"]     # appended when sandbox_bypass: true

[providers.my_tool.cli.flags]
model = "--model"
temperature = "--temp"
system_prompt = "--system-prompt"
system_prompt_file = "--system-file"
cwd = "--cwd"

[providers.my_tool.cli.preflight]
help_args = ["--help"]
expect_in_help = ["--model"]

[providers.my_tool.capabilities]
streaming = true
passthrough = true

[providers.my_tool.metadata]
cost_tier = "cli"

Configuration Reference

Core Fields

FieldRequiredDefaultDescription
binaryYesExecutable name, must be in PATH
subcommandNonilSubcommand prepended before flags (e.g. "exec" for droid exec)
default_timeoutNo1800000 (30 min)Process timeout in milliseconds
default_modelNonilModel string passed to --model
model_resolutionNo"provider_runtime"How model selection works: "gc_default", "provider_runtime", or "explicit_only"
prompt_positionNo"last"Where the prompt goes: "last" (end of args) or "flagged" (after prompt_flag)
prompt_flagConditionalnilRequired when prompt_position = "flagged" (e.g. "-p" for Claude)
prompt_transportNofollows prompt_positionSemantic transport: "last", "flagged", or "stdin"
stdin_hackNofalseWrap with /bin/sh -c ... < /dev/null (needed by Claude CLI)
prefix_argsNo[]Arguments always prepended (e.g. ["--print"])
install_hintNonilHuman-readable message shown when binary is not found

Automation Args

These three profiles let callers control the CLI's autonomy level:

FieldPurpose
non_interactive_argsEnable batch mode (e.g. ["--batch"])
auto_approve_argsEnable unattended execution (e.g. ["--auto", "high"])
sandbox_bypass_argsDisable approval/sandbox entirely (e.g. ["--dangerously-bypass-approvals-and-sandbox"])

All are appended to the invocation only when the corresponding option is true in the dispatch call.

System Prompt Transport

How the system prompt reaches the CLI:

TransportBehavior
"flag"Passed via a flag like --system-prompt "text"
"file_flag"Written to a temp file, path passed via --system-file /tmp/...
"inline_fallback"Prepended to the user prompt as System instructions:\n...\nUser request:\n...
"unsupported"No system prompt mechanism available

System Prompt File Transforms

Some CLIs need the system prompt in a specific file format. Declare the transform:

[providers.kimi_cli.cli]
system_prompt_file_transform = "agent_spec_yaml"

[providers.kimi_cli.cli.file_transform_defaults]
version = 1
extend = "default"

Supported transforms:

  • "agent_spec_yaml" — Generates a YAML agent spec + sibling system.md. Used by Kimi CLI's --agent-file.

Output Capture

FieldPurpose
output_file_flagCLI flag that writes the final response to a file (e.g. "--output-last-message" for Codex)
output_strip_patternsRegex patterns stripped from stdout before building the response

Preflight Checks

Declarative validation that the CLI binary matches the configured contract:

[providers.my_tool.cli.preflight]
help_args = ["--help"]
expect_in_help = ["--model"]

Runs the binary with help_args, checks that every string in expect_in_help appears in the output. Fails fast if the CLI version doesn't support expected flags.

Flags Map

Maps semantic option keys to CLI flag strings:

[providers.my_tool.cli.flags]
model = "--model"
temperature = "--temp"

At dispatch time, these are resolved: model: "v2" becomes --model v2 in the invocation. Boolean flags work too: auto_approve: true becomes just --auto (no value).

Querying at Runtime

alias LlmCore.CLIProvider.Registry

# All known CLI providers (built-in + configured)
Registry.list()
#=> [%{id: :claude_code, aliases: ["claude-code"], binary: "claude", available?: true, ...}, ...]

# Only those with binary in PATH
Registry.available()

# Fetch by id or alias
{:ok, entry} = Registry.fetch(:droid)
{:ok, entry} = Registry.fetch("pi")

# Get a ready-to-use provider struct
{:ok, provider} = Registry.resolve(:claude_code)

# Inspect capabilities
{:ok, caps} = Registry.capabilities(:codex_cli)
#=> %{streaming: true, passthrough: true, ...}

Validation Rules

  • binary is required and must be a non-empty string
  • prompt_position = "flagged" requires prompt_flag to be set
  • Enum fields are validated: prompt_position, prompt_transport, system_prompt_transport, output_mode, system_prompt_file_transform
  • default_model must be a real model identifier, not a provider name or binary name
  • Invalid configs are skipped with a warning (same as module providers)