A small standalone Composer app that wraps Displace\Infer\Model::chat()
into an interactive Symfony Console command. Use it to:
- kick the tires on a freshly downloaded GGUF model
- demo
ext-inferto teammates without writing scaffolding - crib the multi-turn
Promptaccumulation pattern for your own apps
You'll need ext-infer itself installed first — either via pie install displace/ext-infer (once we publish releases; see RELEASE.md) or by
running make install from the repository root. Verify it's loaded:
php -r 'echo extension_loaded("infer") ? "yes\n" : "no\n";'Then install the Symfony Console dep:
cd examples/chat-interactive
composer installIf you're testing against a -d extension=… development build instead of
a system-installed ext-infer, composer install will complain that
the ext-infer PHP extension is missing. Bypass the check:
composer install --ignore-platform-req=ext-inferComposer's platform check reads php.ini, not -d overrides, so the flag
is only needed during dev. Once pie install displace/ext-infer lands
the extension into the global ini, it goes away.
./bin/chat /path/to/some-model.ggufOr with options:
./bin/chat ~/models/Qwen3-0.6B-Q8_0.gguf \
--system="You are a salty pirate. Speak only in metaphors about the sea." \
--temperature=0.9 \
--max-tokens=256Add -v to see the model's internal <think> reasoning alongside its
answer (Qwen3 / DeepSeek R1 / other reasoning models).
Inside the chat prompt:
| Command | Effect |
|---|---|
/reset |
Clear conversation history; keep the system prompt. |
/show |
Dump every message in the conversation as a table. |
/exit |
Quit. Ctrl+D also works. |
Usage:
chat [options] [--] <model>
Arguments:
model Path to a GGUF model file.
Options:
-s, --system=SYSTEM System prompt (default: helpful assistant).
-m, --max-tokens=MAX-TOKENS Maximum tokens per assistant turn (default: 512).
-t, --temperature=TEMPERATURE Sampling temperature (default: 0.7).
-v, --verbose Print the model's <think> reasoning.
src/ChatCommand.php is the meat. Three patterns worth copy-pasting:
$base = Prompt::system($system);
$conversation = $base;
// ... loop ...
if ($line === '/reset') {
$conversation = $base; // immutable; safe to share the original
}Prompt is immutable, so $base never changes no matter how many
->withUser(...) calls the conversation has gone through. That's the
key thing the DateTimeImmutable-style API buys you.
$conversation = $conversation->withAssistant($response->answer());
// ^^^^^^^^^^
// answer(), not text() — otherwise the model sees its own reasoning
// on the next turn and tends to derail.if ($response->finishReason() === 'length') {
$io->writeln('<comment>(truncated — bump --max-tokens)</comment>');
}Better to surface a hint than to leave the user wondering why the answer cut off mid-word.
The command lives in one class (ChatCommand) by design — drop it into a
Symfony app and it'll just work as a sub-command. To add a new command
(say, embed), add a src/EmbedCommand.php, register it in bin/chat,
and the Symfony Application will dispatch on argv.