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 });