Asking a LLM¶
The llm_ask() function¶
Castor provides an llm_ask() function to ask a question to a Large Language
Model and get its answer as a string:
use Castor\Attribute\AsTask;
use function Castor\capture;
use function Castor\io;
use function Castor\llm_ask;
#[AsTask(description: 'Summarize the last commits with an LLM')]
function summarize_commits(): void
{
$commits = capture(['git', 'log', '--oneline', '--no-merges', '-n', '20']);
$summary = llm_ask(<<<PROMPT
Summarize the following git commits in a few bullet points, for a changelog. Use only the commit messages below:
{$commits}
PROMPT);
io()->writeln($summary);
}
Castor does not talk to any AI provider itself: it relies on the AI command line tools you may already have installed and logged in on your machine, and runs them in their non-interactive mode. Castor knows the following CLIs:
- Claude Code (
claude) - Codex CLI (
codex) - OpenCode (
opencode) - Gemini CLI (
gemini) - GitHub Copilot CLI (
copilot) - Cursor CLI (
cursor-agent) - Amp (
amp) - Crush (
crush) - Qwen Code (
qwen) - llm (
llm) - Mods (
mods) - Mistral Vibe (
vibe)
By default, the first time a task asks a LLM, Castor shows the CLI it is about to use and the beginning of the prompt, and asks for a confirmation. It then uses the first installed CLI that answers, for the whole run. The next section explains how to choose the CLI, and how to skip the confirmation.
Note
The CLI runs in the working directory of the current context, with its own
configuration, as when you use it directly: depending on the CLI, the model
may read the files of the project to answer. When the CLI has a read-only
mode, Castor uses it so that the model never modifies the project nor runs
commands: Claude Code runs without any tool, Codex in its read-only sandbox,
OpenCode and Mistral Vibe with their plan agent, Gemini CLI and Qwen Code
in their plan approval mode. The other CLIs keep their own behavior for a
non-interactive run, where the actions requiring an approval are usually
denied. Choosing another agent for OpenCode or Mistral Vibe replaces their
plan agent: what the model may do then depends on the agent you choose.
Choosing the CLI¶
How Castor chooses the CLI is described by a value, which is one of:
confirm: the default. Castor asks for a confirmation the first time a task asks a LLM, then uses the first installed CLI that answers.auto: Castor uses the first installed CLI that answers, without asking. A CLI that fails, because it is not logged in for instance, is skipped with a warning and the next one is tried. Once a CLI has answered, Castor keeps using it for the next questions of the run.choose: the first time a task asks a LLM, Castor lets you choose the CLI among the installed ones, then its model and its agent when the CLI supports them. The choice is kept for the run.- the name of a known CLI, like
claudeorcodex, to always use it. It can be followed by the--modeland--agentoptions, that Castor translates for the CLI, and by any other arguments, passed to the CLI as is:codex --model gpt-5.5,claude --agent reviewer,claude --effort high... - any other command, to use a CLI Castor does not know:
ollama run llama3.2. The prompt is written on the standard input of the command, and its standard output is the answer.
Castor takes the value from the first of these sources that defines one:
- the
cliargument of thellm_ask()function; - the
CASTOR_LLMenvironment variable; - the configuration of the project, made with the
castor:llm:configurecommand; - the global configuration, made with the
castor:llm:configure --globalcommand; - the default,
confirm.
Note
The confirm and choose values need someone to answer: in a
non-interactive run, like in CI or with the --no-interaction option, the
task fails with an error explaining how to configure Castor. Set the
CASTOR_LLM environment variable to auto or to a CLI in such environments.
The same goes for a job of parallel(), since its output is
captured: call llm_ask() once before parallel() to answer the question,
the jobs then use the same CLI.
The cli argument¶
The cli argument of the llm_ask() function takes a value, as a string or as an
array:
use function Castor\llm_ask;
$summary = llm_ask('Summarize this text: ...', cli: 'codex');
$summary = llm_ask('Summarize this text: ...', cli: 'claude --model opus');
$summary = llm_ask('Summarize this text: ...', cli: ['ollama', 'run', '--nowordwrap', 'llama3.2']);
When the value is an array, an argument equal to {prompt} is replaced by the
prompt, for the custom commands that do not read it on their standard input:
use function Castor\llm_ask;
$summary = llm_ask('Summarize this text: ...', cli: ['my-llm', '--prompt', '{prompt}']);
Since the task chooses the CLI, Castor does not ask for a confirmation.
The CASTOR_LLM environment variable¶
The CASTOR_LLM environment variable takes a value, for a single run or for
the environments where nobody can answer a question:
CASTOR_LLM=auto castor llm:summarize-commits
CASTOR_LLM="claude --model opus" castor llm:summarize-commits
CASTOR_LLM="ollama run --nowordwrap llama3.2" castor llm:summarize-commits
The castor:llm:configure command¶
The castor:llm:configure command saves a value, for the current project or,
with the --global option, for all your projects. Without argument, it asks
what to do, then lets you choose the CLI, its model and its agent:
castor castor:llm:configure
castor castor:llm:configure --global
The value can also be passed as an argument, and removed with the --reset
option:
castor castor:llm:configure auto
castor castor:llm:configure "claude --model opus" --global
castor castor:llm:configure --reset
The configuration is a preference of yours on this machine, not something to share with the project: it is stored in the cache directory of Castor, and removing the cache resets it.
The castor:llm:debug command¶
The castor:llm:debug command shows which CLIs are installed, the value of
each source and the one Castor uses, and the exact command it runs. With the
--test option, it also sends a prompt to check the CLI answers:
castor castor:llm:debug
castor castor:llm:debug --test
Errors¶
The llm_ask() function throws a Castor\Exception\LlmException when no known
CLI is installed, when the chosen CLI is not installed or fails, when none of
the installed CLIs could answer, or when the confirmation is refused or cannot
be asked. The message of the exception contains the error output of the CLIs,
to help you fix their setup.