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:
enabled = truein its config block- The declared
binaryis found inPATH
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:
| Name | Binary | Highlights |
|---|---|---|
claude_code | claude | --print, system prompt via --append-system-prompt-file, --add-dir |
droid | droid | exec subcommand, --auto high, --cwd, --worktree |
pi_cli | pi | --print, --provider, --thinking, system prompt file |
kimi_cli | kimi-cli | Agent-file YAML transform, --final-message-only, output stripping |
codex_cli | codex | exec subcommand, --full-auto, file capture via --output-last-message |
gemini_cli | gemini | Model 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
| Field | Required | Default | Description |
|---|---|---|---|
binary | Yes | — | Executable name, must be in PATH |
subcommand | No | nil | Subcommand prepended before flags (e.g. "exec" for droid exec) |
default_timeout | No | 1800000 (30 min) | Process timeout in milliseconds |
default_model | No | nil | Model string passed to --model |
model_resolution | No | "provider_runtime" | How model selection works: "gc_default", "provider_runtime", or "explicit_only" |
prompt_position | No | "last" | Where the prompt goes: "last" (end of args) or "flagged" (after prompt_flag) |
prompt_flag | Conditional | nil | Required when prompt_position = "flagged" (e.g. "-p" for Claude) |
prompt_transport | No | follows prompt_position | Semantic transport: "last", "flagged", or "stdin" |
stdin_hack | No | false | Wrap with /bin/sh -c ... < /dev/null (needed by Claude CLI) |
prefix_args | No | [] | Arguments always prepended (e.g. ["--print"]) |
install_hint | No | nil | Human-readable message shown when binary is not found |
Automation Args
These three profiles let callers control the CLI's autonomy level:
| Field | Purpose |
|---|---|
non_interactive_args | Enable batch mode (e.g. ["--batch"]) |
auto_approve_args | Enable unattended execution (e.g. ["--auto", "high"]) |
sandbox_bypass_args | Disable 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:
| Transport | Behavior |
|---|---|
"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 + siblingsystem.md. Used by Kimi CLI's--agent-file.
Output Capture
| Field | Purpose |
|---|---|
output_file_flag | CLI flag that writes the final response to a file (e.g. "--output-last-message" for Codex) |
output_strip_patterns | Regex 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
binaryis required and must be a non-empty stringprompt_position = "flagged"requiresprompt_flagto be set- Enum fields are validated:
prompt_position,prompt_transport,system_prompt_transport,output_mode,system_prompt_file_transform default_modelmust be a real model identifier, not a provider name or binary name- Invalid configs are skipped with a warning (same as module providers)