PharoInfer is a fully in-image inference engine for Pharo Smalltalk. It loads a GGUF model file directly from disk and drives llama.cpp through UFFI — there is no HTTP server, no Ollama bridge, and no subprocess. Talk to the model straight from the image.
libai_llama.so on Linux,
libai_llama.dylib on macOS, ai_llama.dll on Windows), linked to
a shared build of llama.cpp..gguf model file.model.gguf is the model weights only. The client machine also needs
the native llama.cpp runtime, either installed by the user or shipped
with your Pharo image/application.
Load the package in Pharo. If you are using this local checkout:
Metacello new
githubUser: 'pharo-llm' project: 'pharo-infer' commitish: 'main' path: 'src';
baseline: 'AIPharoInfer';
load.
Do these steps in order. The .gguf file alone is not enough; the
client machine must also have the native llama.cpp runtime built or
bundled.
In a terminal, from this repository:
cd /path/to/pharo-infer
sh scripts/build-native.sh
The script clones/builds llama.cpp as a shared library and compiles the
small PharoInfer shim into $HOME/pharo-infer-native/lib. It supports
macOS, Linux, and Windows. On Windows, run it from Git Bash or another
POSIX-compatible shell with CMake and a C/C++ compiler available.
On macOS this creates:
$HOME/pharo-infer-native/lib/libai_llama.dylib
On Linux this creates:
$HOME/pharo-infer-native/lib/libai_llama.so
On Windows this creates:
$HOME/pharo-infer-native/lib/ai_llama.dll
Keep the whole $HOME/pharo-infer-native/lib folder together. It
contains libai_llama plus the llama.cpp / ggml libraries it depends
on.
Use a GGUF model file:
mkdir -p "$HOME/pharo-models"
cp /path/to/model.gguf "$HOME/pharo-models/model.gguf"
Pharo will look for libai_llama.so, libai_llama.dylib, or
ai_llama.dll on the default library search path, and also under
FileLocator imageDirectory / 'pharo-infer-native' / 'lib' and
FileLocator home / 'pharo-infer-native' / 'lib'. To override, pin it
from the image:
macOS:
AILlamaLibrary libraryPath:
(FileLocator home / 'pharo-infer-native' / 'lib' / 'libai_llama.dylib') fullName.
Linux:
AILlamaLibrary libraryPath:
(FileLocator home / 'pharo-infer-native' / 'lib' / 'libai_llama.so') fullName.
Windows:
AILlamaLibrary libraryPath:
(FileLocator home / 'pharo-infer-native' / 'lib' / 'ai_llama.dll') fullName.
Run this in a Pharo Playground:
| backend manager engine model answer |
backend := AILocalBackend new
nThreads: 8;
contextSize: 2048;
batchSize: 512;
nGpuLayers: 0;
yourself.
manager := AIModelManager new.
manager currentBackend: backend.
model := manager loadModel:
(FileLocator home / 'pharo-models' / 'model.gguf') fullName.
engine := AIInferenceEngine new.
engine modelManager: manager.
answer := engine
complete: 'Say hello from Pharo in one short sentence.'
model: model name.
Transcript show: answer; cr.
answer
For Apple Silicon / Metal GPU offload, try this before loading the model:
backend nGpuLayers: 999.
engine
stream: 'Tell me a joke about Smalltalk'
model: model name
onToken: [ :piece | Transcript show: piece ].
| request |
request := AIChatCompletionRequest
model: model name
messages: {
AIChatMessage system: 'You are a helpful AI assistant.'.
AIChatMessage user: 'What is Smalltalk?' }.
AIChatAPI default complete: request.
AILocalBackend new
nGpuLayers: 999; "offload all layers"
nThreads: 8;
contextSize: 4096.
AILlamaLibrary — FFILibrary mapping the PharoInfer shim entry
points.AILlamaModelParams, AILlamaContextParams, AILlamaBatch,
AILlamaSamplerChainParams — legacy structure mirrors kept for
source compatibility; the production backend no longer passes them
through UFFI.AILocalBackend — drives llama.cpp: loads a model, runs
tokenization + decode + sampling, and detokenizes back to UTF-8.AILocalModelHandle — owns the opaque (model *, context *) pair
and frees it on unload.AIGGUFParser — optional pre-flight reader for GGUF metadata
(header, vocab, special tokens) without loading the model.AIInferenceEngine, AIChatAPI — high-level entry points.