local
js/ai/model/local.ts
fino:ai/model/local — optional llama.cpp adapter for local GGUF models.
This module lets Fino applications use the same provider-neutral Model
interface for local llama.cpp models that they use for remote providers.
Local support is optional: hasLlamaCpp is computed once when the module is
evaluated by probing the default libllama lookup paths, and importing the
module never throws when libllama is absent.
Design
The adapter loads system libllama through fino:ffi and uses FFI
StructType descriptors for llama.cpp's by-value parameter structs. Pass a
local .gguf path, or pass an explicit Hugging Face repository and file.
Hugging Face sources are downloaded into a cache before the model is opened.
The built-in direct libllama path tokenizes prompts, decodes batches,
samples tokens through llama.cpp sampler chains, detokenizes output pieces,
and cleans up native resources. v1 intentionally rejects native tool calls,
structured response formats, and embeddings because the local adapter does
not yet have portable support for those capabilities.
Library discovery honors the LLAMA_CPP_LIBRARY and FINO_LLAMA_LIBRARY
environment variables first, then platform-conventional install locations
(Homebrew and /usr/local/lib on macOS; /usr/local/lib, /usr/lib, and
multiarch directories on Linux). When a companion libggml is found next to
libllama or in the same conventional locations, its compute backends (BLAS,
CPU variants) are registered too. All llama.cpp and ggml logging is
silenced so model loading does not write to stderr.
Streaming is emulated: stream() runs the full generation and yields the
complete text as a single text_delta event, and reported token usage is
always zero. Concurrent generate() and stream() calls on one model are
serialized because they share a single llama.cpp context.
import { hasLlamaCpp, local } from 'fino:ai/model/local';
if (hasLlamaCpp) {
const model = await local({
model: './models/tinyllama.Q4_K_M.gguf',
contextSize: 4096,
threads: 8,
});
const result = await model.generate({
messages: [{ role: 'user', content: 'Write one sentence about local AI.' }],
});
console.log(result.text);
}
Types
type LocalModelSource = string | {
path: string;
} | {
repo: string;
file: string;
revision?: string;
}
Source accepted by the local llama.cpp adapter.
A plain string or the { path } form names a GGUF file on disk. The
{ repo, file, revision } form names an explicit GGUF file in a Hugging
Face repository, downloaded into the cache directory on first use
(revision defaults to main). Every form must end in .gguf — the
adapter refuses to guess quantizations or resolve non-GGUF artifacts and
throws for anything else.
Interfaces
interface LocalModelOptions extends ModelCreateOptions {
Options for creating a local llama.cpp-backed model.
Inherited ModelCreateOptions fields act as generation defaults: requests
that omit maxTokens or temperature fall back to the values given here,
then to the module defaults (512 tokens, temperature 0.8).
import { local } from 'fino:ai/model/local';
const model = await local({
model: {
repo: 'bartowski/SmolLM2-135M-Instruct-GGUF',
file: 'SmolLM2-135M-Instruct-Q2_K.gguf',
},
cacheDir: '/tmp/fino-models',
contextSize: 2048,
threads: 4,
maxTokens: 64,
temperature: 0,
stopSequences: ['\n'],
});
Properties
id?: string
Stable id exposed on the constructed Model.
Defaults to the local path or Hugging Face file identity. Providers use
this to preserve configured ids from ModelInfo.
model: LocalModelSource
Local GGUF path or explicit Hugging Face GGUF file.
libraryPath?: string
Explicit path to a system libllama.
When omitted, the module-level default libllama discovery is used.
cacheDir?: string
Cache directory for Hugging Face downloads. Defaults to .fino/models.
hfToken?: string
Hugging Face token for private repositories. Defaults to the HF_TOKEN
environment variable.
contextSize?: number
llama.cpp context size. Defaults to 4096 tokens.
threads?: number
CPU worker thread count. Defaults to 0, which lets libllama choose.
batchSize?: number
Prompt batch size. Defaults to 512.
gpuLayers?: number
Number of layers to place on GPU. Defaults to 0.
topP?: number
Sampling nucleus. Defaults to 0.95; values outside the exclusive (0, 1) range disable the top-p stage.
topK?: number
Sampling top-k. Defaults to 40; values of 0 or below disable the top-k stage.
stopSequences?: string[]
Default stop sequences for requests that do not supply their own.
Generation ends at the first match and the matched sequence is trimmed from the returned text.
client?: ClientLike
HTTP client used for Hugging Face downloads.
This is primarily useful for tests and custom network policy.
interface LocalModelProviderEntry {
Configured model entry exposed by localProvider().
Entries are static configuration: listModels() reports each one as a
ModelInfo row, and the model's GGUF source is only resolved and opened
when the entry is instantiated through createModel() or
ModelInfo.create().
import type { LocalModelProviderEntry } from 'fino:ai/model/local';
const entry: LocalModelProviderEntry = {
id: 'smol',
model: { repo: 'bartowski/SmolLM2-135M-Instruct-GGUF', file: 'SmolLM2-135M-Instruct-Q2_K.gguf' },
displayName: 'SmolLM2 135M',
};
Properties
id: string
Stable identifier used by listModels() and createModel(), and
preserved as the constructed model's id.
model: LocalModelSource
GGUF source opened when the entry is instantiated.
displayName?: string
Human-readable name surfaced on the listed ModelInfo.
metadata?: Record<string, unknown>
Extra ModelInfo metadata. Defaults to { source: model } when omitted.
interface LocalProviderOptions extends Omit<LocalModelOptions, 'model'> {
Options for a local provider.
Everything except models is shared LocalModelOptions configuration —
library path, cache directory, sampling defaults, and so on — applied to
every model the provider creates. Per-call ModelCreateOptions passed to
createModel() layer on top of these shared options.
import { localProvider } from 'fino:ai/model/local';
const provider = localProvider({
cacheDir: '/tmp/fino-models',
contextSize: 2048,
models: [{ id: 'tiny', model: '/models/tiny.Q4_K_M.gguf' }],
});
Properties
models?: LocalModelProviderEntry[]
Models returned by listModels().
Classes
class LocalModelLibraryError extends Error {
Error thrown when libllama cannot be loaded.
local() throws this when an explicit libraryPath fails to open, or when
no libraryPath is given and default discovery found no libllama (that is,
hasLlamaCpp is false).
import { local, LocalModelLibraryError } from 'fino:ai/model/local';
try {
await local({ model: '/tmp/model.gguf', libraryPath: '/opt/llama/lib/libllama.so' });
} catch (err) {
if (err instanceof LocalModelLibraryError) {
console.error(`libllama unavailable at ${err.libraryPath}`);
}
}
Properties
libraryPath?: string
The explicit library path that failed to load, when one was provided. Absent when default discovery found no candidate at all.
Constructors
constructor(message: string, opts: {
libraryPath?: string;
} = {})
Create the error, optionally recording the library path that failed.
class LocalModelUnsupportedError extends Error {
Error thrown for model request features that the local adapter does not support.
Raised for tool definitions or any explicit toolChoice, native
responseFormat requests, non-text content parts (images, documents), and
embed() calls. Catch it to fall back to a remote provider for requests
that need those capabilities.
import { local, LocalModelUnsupportedError } from 'fino:ai/model/local';
const model = await local({ model: './models/tiny.Q4_K_M.gguf' });
try {
await model.generate({
messages: [{ role: 'user', content: 'What is the weather?' }],
tools: [{ name: 'weather', description: 'Look up weather', parameters: {} }],
});
} catch (err) {
if (err instanceof LocalModelUnsupportedError) {
// route this request to a remote model instead
}
}
Constructors
constructor(message: string)
Create the error with a description of the unsupported capability.
Constants
const hasLlamaCpp
Whether the default libllama was found during module evaluation.
This boolean only reflects the default lookup paths. Passing
libraryPath to local() can still succeed when this is false.
Functions
async function local(opts: LocalModelOptions): Promise<Model>
Create a llama.cpp-backed local model.
Construction resolves the model source, downloads Hugging Face GGUF files
into the cache when necessary, loads the optional libllama, and opens the
model. Requests fall back to the option defaults given here, then to the
module defaults (512 max tokens, temperature 0.8, top-k 40, top-p 0.95); a
temperature of 0 or below switches to deterministic greedy sampling.
Concurrent generate() and stream() calls are serialized on the single
llama.cpp context. Close long-lived models with model.close?.() when your
application is done with them.
Throws LocalModelLibraryError when libllama cannot be loaded, and a plain
Error when the source is not a .gguf file, a Hugging Face download
fails, or the model itself fails to open.
import { local } from 'fino:ai/model/local';
const model = await local({
model: {
repo: 'bartowski/SmolLM2-135M-Instruct-GGUF',
file: 'SmolLM2-135M-Instruct-Q2_K.gguf',
},
contextSize: 2048,
maxTokens: 64,
});
for await (const event of model.stream({
messages: [{ role: 'user', content: 'Tell me a joke.' }],
})) {
if (event.type === 'text_delta') console.log(event.text);
}
function localProvider(opts: LocalProviderOptions = {}): ModelProvider
Create a local provider for configured GGUF models.
listModels() returns the static models entries supplied here. It does not
scan local directories or query Hugging Face. createModel(id) — or the
create() callback on a listed ModelInfo — opens the entry's GGUF source
through local() with the provider's shared options, and throws when id
was not configured.
import { localProvider } from 'fino:ai/model/local';
const provider = localProvider({
cacheDir: '/tmp/fino-models',
models: [
{
id: 'smol',
model: {
repo: 'bartowski/SmolLM2-135M-Instruct-GGUF',
file: 'SmolLM2-135M-Instruct-Q2_K.gguf',
},
displayName: 'SmolLM2 135M',
},
],
});
const [info] = await provider.listModels();
const model = await info.create({ maxTokens: 128 });