model

js/ai/model.ts

fino:ai/model — provider-neutral messages, streams, and model adapters.

This module defines the narrow contract that the rest of fino:ai builds on. Providers adapt remote APIs into Model, agents consume Model without provider-specific branches, tools use the shared message and content-part shapes, and evals can run against any compatible implementation, including optional local llama.cpp models.

Design

Model.stream() is the canonical path for agent execution. Provider adapters emit normalized StreamEvent values for text, tool-call deltas, usage, stop reasons, and errors; assembleResult() folds those events into the same GenerateResult shape returned by Model.generate(). Embeddings use the separate EmbeddingModel contract so chat-only providers and tests do not need fake embedding methods.

This module does not hide provider capabilities. Adapters expose id, provider, and optional capabilities so higher layers can make explicit choices, such as using native structured-output transport only when a model declares support for it. Provider registries can also discover available model ids from OpenAI-compatible and Anthropic endpoints before constructing a concrete Model.

import { assembleResult, modelRegistry, openaiProvider } from 'fino:ai/model';

const registry = modelRegistry([
  openaiProvider({ baseUrl: 'https://api.openai.com/v1' }),
]);
const [info] = await registry.list();
const model = await info.create({ temperature: 0.2 });
const stream = model.stream({
  messages: [{ role: 'user', content: 'Say hello in one sentence.' }],
});

const result = await assembleResult(stream);
console.log(result.text, result.usage);

Types

type Role = 'user' | 'assistant' | 'system'

Chat message role understood by all providers.

user and assistant carry the conversation turns. A system message placed in GenerateRequest.messages is routed to the provider's native system-prompt channel and takes precedence over GenerateRequest.system.

type ContentPart = TextPart | ImagePart | ToolUsePart | ToolResultPart | DocumentPart

Content part accepted in a model message.

Message content is either a plain string or an ordered list of these parts; the string form is shorthand for a single TextPart.

type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error' | 'refusal' | 'content_filter'

Reason a model stopped generating.

end_turn is a normal completion. tool_use means the model is waiting on tool results. max_tokens and stop_sequence indicate the output was cut by a limit from the request. error, refusal, and content_filter are abnormal terminations surfaced by the provider.

type StreamEvent = { type: 'text_delta'; index: number; text: string; } | { type: 'tool_call_start'; index: number; id: string; name: string; } | { type: 'tool_call_delta'; index: number; json: string; } | { type: 'tool_call_end'; index: number; } | { type: 'usage'; usage: Usage; } | { type: 'stop'; reason: StopReason; } | { type: 'error'; message: string; }

Streaming event emitted by provider adapters.

text_delta carries an index so parallel content blocks concatenate in the right order. Tool calls arrive as a tool_call_start naming the tool, tool_call_delta events carrying argument-JSON fragments, and a tool_call_end. usage may be emitted more than once; the latest values win. stop reports the stop reason, and error signals an abnormal termination — assembleResult() converts it into a thrown Error.

for await (const event of model.stream({ messages })) {
  if (event.type === 'text_delta') write(event.text);
  else if (event.type === 'stop') console.log('done:', event.reason);
}

type Model = ChatModel

Provider-neutral model adapter used by chat and agent APIs.

Providers may also implement EmbeddingModel; memory and semantic eval APIs depend on that narrower embedding contract instead of requiring every chat model to expose embeddings.

Interfaces

interface TextPart {

Text content part.

The most common part; a plain-string message body is shorthand for a single text part. Use the part form when a message mixes text with images, documents, or tool traffic, or when a span of text should be a prompt-cache breakpoint.

import type { ModelMessage } from 'fino:ai/model';

const message: ModelMessage = {
  role: 'user',
  content: [
    { type: 'text', text: manualText, cache: true },
    { type: 'text', text: 'Which chapter covers installation?' },
  ],
};

Properties

type: 'text'

Part discriminant.

text: string

The text carried by this part.

cache?: true

Marks this part as a provider prompt-cache breakpoint.

The Anthropic adapter maps it to cache_control: { type: 'ephemeral' }. Providers without explicit breakpoints ignore the flag; cache hits still surface through Usage.cacheReadInputTokens when reported.

interface ImagePart {

Inline image content part.

data is base64-encoded bytes without a data URI prefix. Send images only to models whose capabilities.input.image is true; providers reject or silently drop modalities they do not accept.

import type { ModelMessage } from 'fino:ai/model';

const message: ModelMessage = {
  role: 'user',
  content: [
    { type: 'image', mediaType: 'image/png', data: screenshotBase64 },
    { type: 'text', text: 'What error does this screenshot show?' },
  ],
};

Properties

type: 'image'

Part discriminant.

mediaType: string

MIME type of the encoded bytes, such as image/png or image/jpeg.

data: string

Base64-encoded image bytes without a data: URI prefix.

interface ToolUsePart {

Model-requested tool call content part.

Appears in assistant messages when replaying conversation history that included tool calls: echo the model's request back as a tool_use part, then answer it with a matching tool_result part in the following user message.

import type { ModelMessage } from 'fino:ai/model';

const history: ModelMessage[] = [
  { role: 'user', content: 'What is the weather in Lisbon?' },
  {
    role: 'assistant',
    content: [{ type: 'tool_use', id: 'call_1', name: 'weather', args: { city: 'Lisbon' } }],
  },
  {
    role: 'user',
    content: [{ type: 'tool_result', toolCallId: 'call_1', content: '19C and sunny' }],
  },
];

Properties

type: 'tool_use'

Part discriminant.

id: string

Provider-assigned call id, answered via ToolResultPart.toolCallId.

name: string

Name of the tool from the request's ToolDefinition list.

args: unknown

Parsed tool arguments produced by the model.

interface ToolResultPart {

Tool result content part sent back to the model.

Sent in a user message after the assistant requested a tool call. Set isError when the tool run failed so the model can recover instead of treating the output as a successful result.

import type { ModelMessage } from 'fino:ai/model';

const reply: ModelMessage = {
  role: 'user',
  content: [{
    type: 'tool_result',
    toolCallId: call.id,
    content: JSON.stringify(rows),
  }],
};

Properties

type: 'tool_result'

Part discriminant.

toolCallId: string

Id of the ToolUsePart (or ToolCall) this result answers.

content: string | ContentPart[]

Tool output as plain text, or as parts when the result includes media.

isError?: boolean

Marks the result as a tool failure rather than a successful output.

interface DocumentPart {

Inline document content part.

data is base64-encoded bytes without a data URI prefix. Providers may map this to files, documents, or other native document upload fields. Send documents only to models whose capabilities.input.document is true.

import type { ModelMessage } from 'fino:ai/model';

const message: ModelMessage = {
  role: 'user',
  content: [
    { type: 'document', mediaType: 'application/pdf', data: contractBase64, name: 'contract.pdf' },
    { type: 'text', text: 'List the termination clauses.' },
  ],
};

Properties

type: 'document'

Part discriminant.

mediaType: string

MIME type of the encoded bytes, such as application/pdf.

data: string

Base64-encoded document bytes without a data: URI prefix.

name?: string

Display filename hint passed to providers that accept one.

interface ResponseFormat {

Native structured response format request.

Adapters translate this to the provider's native structured-output transport: response_format on OpenAI-compatible APIs and output_config on Anthropic. Check capabilities.structuredOutput.native before relying on it; higher layers fall back to prompt-based JSON extraction for models without native support.

const result = await model.generate({
  messages: [{ role: 'user', content: 'Extract the invoice fields.' }],
  responseFormat: {
    type: 'json_schema',
    name: 'invoice',
    schema: {
      type: 'object',
      properties: { total: { type: 'number' }, currency: { type: 'string' } },
      required: ['total', 'currency'],
    },
  },
});
const invoice = JSON.parse(result.text);

Properties

type: 'json_schema'

Format discriminant; only JSON Schema output is defined.

name?: string

Schema name sent to the provider. Defaults to response.

schema: Record<string, unknown>

JSON Schema object the model output must conform to.

strict?: boolean

Request strict provider-side schema enforcement.

The OpenAI adapter defaults this to true; the Anthropic adapter passes it through only when set.

interface ModelMessage {

Provider-neutral chat message.

content is either plain text or an ordered list of content parts for multimodal and tool-carrying turns.

import type { ModelMessage } from 'fino:ai/model';

const messages: ModelMessage[] = [
  { role: 'user', content: 'Summarize the release notes.' },
  { role: 'assistant', content: 'The release adds SSE routes and fixes two loader bugs.' },
  { role: 'user', content: 'Shorter, one sentence.' },
];

Properties

role: Role

Who produced the message.

content: string | ContentPart[]

Plain text, or content parts for multimodal and tool traffic.

interface ToolDefinition {

Provider-neutral tool definition sent with model requests.

This is the wire shape adapters transmit; higher-level tool registration and dispatch live in fino:ai/tool. parameters is a JSON Schema object describing the tool's arguments.

import type { ToolDefinition } from 'fino:ai/model';

const weather: ToolDefinition = {
  name: 'weather',
  description: 'Look up current weather for a city.',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string' } },
    required: ['city'],
  },
};

Properties

name: string

Tool name the model uses in tool_use parts and tool-call events.

description: string

Natural-language description that tells the model when to call it.

parameters: Record<string, unknown>

JSON Schema object describing the tool's arguments.

interface GenerateRequest {

Provider-neutral generation request.

The same request shape is accepted by Model.generate() and Model.stream(). Sampling fields set here override any defaults configured on the model at construction time.

const controller = new AbortController();
const result = await model.generate({
  system: 'You are a terse release-notes editor.',
  messages: [{ role: 'user', content: draft }],
  maxTokens: 512,
  temperature: 0,
  signal: controller.signal,
});

Properties

messages: ModelMessage[]

Conversation turns, oldest first.

system?: string | TextPart[]

System prompt for the request.

The TextPart[] form allows prompt-cache breakpoints via cache: true. Ignored when a system-role message already appears in messages.

tools?: ToolDefinition[]

Tool definitions offered to the model for this request.

toolChoice?: 'auto' | 'any' | 'none' | { name: string; }

How the model may use the offered tools.

auto lets the model decide, any requires some tool call, none forbids tool calls, and { name } forces one specific tool. Consult capabilities.toolChoice for which modes a model honors.

maxTokens?: number

Maximum output tokens; overrides the model's configured default.

temperature?: number

Sampling temperature; overrides the model's configured default.

topP?: number

Nucleus sampling cutoff; overrides the model's configured default.

seed?: number

Sampling seed for providers that support reproducible output.

stopSequences?: string[]

Sequences that end generation with stopReason: 'stop_sequence'.

responseFormat?: ResponseFormat

Native structured-output request.

providerOptions?: Record<string, unknown>

Raw request-body extras keyed by provider name.

The entry matching the model's provider (for example { anthropic: { thinking: { type: 'enabled' } } }) is object-merged into the outgoing request body; other keys are ignored.

signal?: AbortSignal

Aborts the underlying HTTP request and stream when signaled.

interface Usage {

Token usage reported by a provider.

The cache* fields describe the provider's native prompt cache; the localCache* fields describe requests answered by a local Fino response cache (fino:ai/cache) without touching the provider at all. Optional fields are present only when the provider or cache reported them.

const result = await model.generate({ messages });
const { inputTokens, outputTokens, cacheReadInputTokens = 0 } = result.usage;
recordCost(inputTokens - cacheReadInputTokens, cacheReadInputTokens, outputTokens);

Properties

inputTokens: number

Provider-billed input tokens for the request.

outputTokens: number

Provider-billed output tokens for the request.

cacheReadInputTokens?: number

Provider-native cache-read input tokens, when reported by the provider.

cacheCreationInputTokens?: number

Provider-native cache-creation input tokens, when reported by the provider.

localCacheReadInputTokens?: number

Input tokens avoided by a local Fino cache hit.

localCacheReadOutputTokens?: number

Output tokens avoided by a local Fino cache hit.

interface ToolCall {

Assembled tool call returned by generate() or ModelStream.result().

This is the folded form of the incremental tool_call_start / tool_call_delta / tool_call_end stream events, with args already JSON-parsed. Answer each call with a tool_result part and continue the conversation.

const result = await model.generate({ messages, tools });
if (result.stopReason === 'tool_use') {
  for (const call of result.toolCalls) {
    const output = await dispatch(call.name, call.args);
    messages.push(
      { role: 'assistant', content: [{ type: 'tool_use', ...call }] },
      { role: 'user', content: [{ type: 'tool_result', toolCallId: call.id, content: output }] },
    );
  }
}

Properties

id: string

Provider-assigned call id, echoed back via ToolResultPart.toolCallId.

name: string

Name of the requested tool.

args: unknown

Parsed tool arguments.

Falls back to {} when the streamed argument JSON was empty or failed to parse.

interface GenerateResult {

Complete non-streamed model result.

Returned by Model.generate() and produced by folding a stream through ModelStream.result() or assembleResult() — all three paths yield this same shape.

const result = await model.generate({ messages, maxTokens: 256 });
if (result.stopReason === 'max_tokens') {
  console.warn('Output truncated after', result.usage.outputTokens, 'tokens');
}
console.log(result.text);

Properties

text: string

Generated text, concatenated across content blocks in index order.

toolCalls: ToolCall[]

Tool calls requested by the model, empty when there were none.

usage: Usage

Token usage for the request.

stopReason: StopReason

Why generation stopped.

warnings?: string[]

Non-fatal provider notes about how the request was handled.

providerMetadata?: Record<string, unknown>

Raw provider response data keyed by provider name, such as { anthropic: rawResponse }. Set by non-streamed generation paths that have the full provider response in hand.

interface ModelStreamState {

Current retained view of a model stream.

This is a lossy read model for UIs and progress meters. Use the stream itself when every provider event must be processed. The state advances only as events are pulled from the stream, so something must be consuming it.

const stream = model.stream({ messages });
const unsubscribe = stream.state.subscribe(({ text }) => render(text));
const result = await stream.result();
unsubscribe();

Properties

text: string

Text folded from text_delta events, ordered by content index.

usage: Usage

Latest usage values reported by the provider.

stopReason: StopReason

Latest stop reason, or end_turn before a stop event arrives.

interface ModelCapabilities {

Feature metadata exposed by chat model adapters.

Capabilities are semantic flags, not provider names. Agent code uses these fields to decide whether to request tools, native JSON Schema output, multimodal input, and optional sampling controls. Every field is optional; an absent field means the adapter made no claim, so treat it as unknown rather than unsupported.

const nativeJson = model.capabilities?.structuredOutput?.native === true;
const result = await model.generate({
  messages,
  ...(nativeJson ? { responseFormat: { type: 'json_schema', schema } } : {}),
});

Properties

streaming?: boolean

Whether the adapter implements incremental streaming.

toolCalling?: boolean

Whether the model can call tools at all.

toolChoice?: { auto?: boolean; any?: boolean; none?: boolean; named?: boolean; }

Which GenerateRequest.toolChoice modes the model honors.

structuredOutput?: { jsonSchema?: boolean; strictJsonSchema?: boolean; native?: boolean; }

Structured-output support: whether JSON Schema requests are accepted at all (jsonSchema), whether strict enforcement is available (strictJsonSchema), and whether a native transport carries the schema (native).

input?: { text?: boolean; image?: boolean; document?: boolean; }

Input modalities the model accepts.

sampling?: { temperature?: boolean; topP?: boolean; seed?: boolean; stopSequences?: boolean; }

Which sampling controls from GenerateRequest take effect.

local?: boolean

True when inference runs in-process without network access.

interface EmbeddingCapabilities {

Feature metadata exposed by embedding model adapters.

const batchSize = embedder.capabilities?.maxBatchSize ?? 64;
for (const batch of chunk(texts, batchSize)) {
  vectors.push(...await embedder.embed(batch));
}

Properties

dimensions?: number

Width of the vectors returned by embed().

maxBatchSize?: number

Maximum number of texts accepted per embed() call.

maxInputTokens?: number

Maximum tokens per input text.

interface ModelStream extends AsyncIterable<StreamEvent> {

Async stream of model events.

Iterate it to process every provider event, or call result() to drain the remaining events and receive the folded GenerateResult. The state signal is a retained, lossy view for UIs; it only advances while events are being pulled.

const stream = model.stream({ messages });
for await (const event of stream) {
  if (event.type === 'text_delta') write(event.text);
}
const { toolCalls, usage } = await stream.result();

Readonly Properties

readonly state: ReadonlySignal<ModelStreamState>

Retained state folded from events observed so far.

Methods

result(): Promise<GenerateResult>

Drain the remaining events and fold them into a GenerateResult.

Rejects when the provider emits an error event or the request fails.

interface ChatModel {

Provider-neutral chat model adapter.

This is the contract agents and evals consume; anthropic(), openai(), and local() all return implementations of it. Provider HTTP failures surface as ModelError.

import { anthropic } from 'fino:ai/model';

const model = anthropic();
const result = await model.generate({
  messages: [{ role: 'user', content: 'Name three prime numbers.' }],
  maxTokens: 128,
});
console.log(result.text);

Readonly Properties

readonly id?: string

Stable model identifier used for provider requests and telemetry.

Provider implementations set this explicitly. name remains available as a compatibility alias for local test doubles and older call sites.

readonly name: string

Model name; historically the same value as id.

readonly provider?: string

Provider identifier such as openai or anthropic.

Agent code uses this for telemetry and feature decisions instead of guessing from the model id.

readonly capabilities?: ModelCapabilities

Optional provider features that affect request construction.

Methods

stream(req: GenerateRequest): ModelStream

Start a streaming generation.

This is the canonical execution path; adapters implement streaming first and derive generate() from it.

generate(req: GenerateRequest): Promise<GenerateResult>

Run a generation to completion and return the folded result.

Equivalent to stream(req).result().

interface EmbeddingModel {

Provider-neutral embedding model adapter.

Kept separate from ChatModel so chat-only providers and test doubles do not need fake embedding methods. Memory and semantic-eval APIs accept this narrower contract.

import type { EmbeddingModel } from 'fino:ai/model';

async function indexChunks(embedder: EmbeddingModel, chunks: string[]) {
  const vectors = await embedder.embed(chunks);
  for (let i = 0; i < chunks.length; i++) {
    await store.put(chunks[i], vectors[i]);
  }
}

Readonly Properties

readonly id: string

Stable embedding model identifier.

readonly name: string

Model name; historically the same value as id.

readonly provider: string

Provider identifier such as openai.

readonly capabilities?: EmbeddingCapabilities

Optional embedding feature metadata.

readonly dimensions: number

Width of the vectors returned by embed().

Methods

embed(texts: string[]): Promise<Float32Array[]>

Embed a batch of texts into vectors, one per input in the same order.

interface ModelCreateOptions {

Options applied when constructing a Model from a provider or registry.

These become per-model defaults: each GenerateRequest can still override the sampling values for an individual call.

const model = await registry.create('gpt-4o-mini', {
  maxTokens: 1024,
  temperature: 0,
});

Properties

maxTokens?: number

Default maximum output token count for models created from the provider.

temperature?: number

Default sampling temperature for models created from the provider.

topP?: number

Default nucleus sampling cutoff for the constructed model.

seed?: number

Default sampling seed for providers that support reproducible output.

providerOptions?: Record<string, unknown>

Default raw request-body extras keyed by provider name, merged into every request unless a request supplies its own providerOptions.

dimensions?: number

Embedding vector dimensions for providers that support embeddings.

headers?: Record<string, string>

Additional provider headers merged with the provider's configured headers.

interface ModelInfo {

Discovered model entry returned by a ModelProvider.

create() constructs a provider-neutral Model for this exact model id using the provider configuration that discovered it.

const models = await registry.list({ provider: 'anthropic' });
const info = models.find((m) => m.capabilities?.toolCalling);
if (!info) throw new Error('No tool-calling model available');
const model = await info.create({ temperature: 0.2 });

Readonly Properties

readonly id: string

Provider model id used in generation requests.

readonly provider: string

Provider identifier such as openai or anthropic.

readonly displayName?: string

Human-readable name returned by the provider, when available.

readonly createdAt?: number

Provider-reported creation timestamp, when available.

readonly ownedBy?: string

Owner or organization returned by OpenAI-compatible endpoints.

readonly capabilities?: Model['capabilities']

Capabilities inferred by the provider adapter for this model.

readonly metadata?: Record<string, unknown>

Raw provider metadata for callers that need provider-specific fields.

Methods

create(opts?: ModelCreateOptions): Promise<Model>

Construct a Model for this discovered model id.

Model construction is always async so remote and local providers share one call shape. Remote providers usually resolve immediately, while local providers may need filesystem or cache work.

interface ModelProvider {

Provider that can discover available model ids and construct Model adapters for those ids.

Providers are usually consumed through a ModelRegistry, but they can be used directly when only one provider is in play.

import { anthropicProvider } from 'fino:ai/model';

const provider = anthropicProvider();
const [info] = await provider.listModels();
const model = await provider.createModel(info.id, { maxTokens: 2048 });

Readonly Properties

readonly provider: string

Stable provider name used in ModelInfo.provider and registry filters.

Methods

listModels(opts?: { signal?: AbortSignal; }): Promise<ModelInfo[]>

Fetch available models from the provider endpoint.

Throws ModelListingUnsupportedError when the endpoint has no model discovery, and ModelError for other HTTP failures.

createModel(id: string, opts?: ModelCreateOptions): Promise<Model>

Construct a model by provider model id.

interface ProviderOptions {

Common provider options accepted by bundled providers.

All fields are optional: API keys fall back to environment variables and baseUrl defaults to the provider's public endpoint. Point baseUrl at any compatible server — the OpenAI adapter works against OpenAI-compatible endpoints such as vLLM or llama-server.

import { openai } from 'fino:ai/model';

const model = openai({
  baseUrl: 'http://localhost:8000/v1',
  apiKey: 'local',
  model: 'llama-3.1-8b-instruct',
  maxTokens: 2048,
});

Properties

apiKey?: string

Provider API key.

Falls back to the provider's environment variable (ANTHROPIC_API_KEY or OPENAI_API_KEY); factories throw when neither is available.

baseUrl?: string

Endpoint base URL. Defaults to the provider's public API.

headers?: Record<string, string>

Extra headers sent with every provider request.

client?: { request(url: string | URL, init?: { method?: string; headers?: Record<string, string>; body?: string | null; signal?: AbortSignal | null; }): Promise<{ status: number; body: AsyncIterable<Uint8Array> | null; text(): Promise<string>; json(): Promise<unknown>; }>; }

Custom HTTP client used instead of the built-in one.

Primarily for tests and custom network policy; the shape matches the built-in HttpClient.request().

model?: string

Model id for the single-model factories.

anthropic() defaults to claude-opus-4-8 and openai() defaults to gpt-4o. Ignored by the provider factories, which take ids per call.

maxTokens?: number

Default maximum output tokens for requests to the model.

temperature?: number

Default sampling temperature for requests to the model.

topP?: number

Default nucleus sampling cutoff for requests to the model.

seed?: number

Default sampling seed for providers that support reproducible output.

providerOptions?: Record<string, unknown>

Default raw request-body extras keyed by provider name.

dimensions?: number

Embedding vector dimensions for providers that support embeddings.

Classes

class ModelRegistry {

Registry for discovering and constructing provider-neutral models.

A registry aggregates several ModelProvider instances behind one lookup surface so application code can list, find, and construct models without caring which provider serves them. listModels() results are cached per provider instance; pass refresh: true to bypass the cache for a call.

import { anthropicProvider, modelRegistry, openaiProvider } from 'fino:ai/model';

const registry = modelRegistry([openaiProvider(), anthropicProvider()]);
for (const info of await registry.list()) {
  console.log(info.provider, info.id);
}
const model = await registry.create('claude-opus-4-8', { temperature: 0.2 });

Constructors

constructor(providers: ModelProvider[] = [])

Create a registry with an optional initial provider list.

Methods

add(provider: ModelProvider): this

Add a provider and return this registry for chaining.

Any cached listing for the same provider instance is discarded so the next list() fetches fresh results from it.

async list(opts: { provider?: string; refresh?: boolean; signal?: AbortSignal; } = {}): Promise<ModelInfo[]>

List discovered models across all providers or one provider.

Results are served from the per-provider cache unless refresh is set or a provider has not been listed yet. Throws when provider names a provider that is not registered.

async get(id: string, opts: { provider?: string; refresh?: boolean; } = {}): Promise<ModelInfo | null>

Find a model by id, or return null when no provider reports it.

Throws when more than one provider reports the same id; pass provider to disambiguate.

async create(id: string, opts: ModelCreateOptions & { provider?: string; refresh?: boolean; } = {}): Promise<Model>

Construct a Model by discovered id.

Looks the id up with get() and forwards the remaining options to ModelInfo.create(). Throws when the id is unknown or ambiguous across providers.

Functions

function modelRegistry(providers: ModelProvider[] = []): ModelRegistry

Create a model registry from one or more providers.

Convenience wrapper around new ModelRegistry(providers).

import { modelRegistry, openaiProvider } from 'fino:ai/model';

const registry = modelRegistry([openaiProvider()]);
const model = await registry.create('gpt-4o-mini');

function assembleResult(events: AsyncIterable<StreamEvent>): Promise<GenerateResult>

Assemble streamed provider events into a complete GenerateResult.

Text deltas are concatenated per content index and joined in index order, tool-call argument fragments are JSON-parsed (falling back to {} when the JSON is empty or malformed), and the latest usage and stop values win. Throws when the stream emits an error event. ModelStream.result() uses this internally, so calling it directly is only needed for bare event iterables such as recorded or transformed streams.

import { assembleResult } from 'fino:ai/model';

const result = await assembleResult(recordedEvents());
console.log(result.text, result.stopReason);

function anthropic(opts: ProviderOptions = {}): Model

Create an Anthropic-backed Model.

apiKey defaults to ANTHROPIC_API_KEY (throws when neither is set), baseUrl defaults to the public Anthropic API, and model defaults to claude-opus-4-8.

import { anthropic } from 'fino:ai/model';

const model = anthropic({ model: 'claude-opus-4-8', maxTokens: 4096 });
const result = await model.generate({
  messages: [{ role: 'user', content: 'Explain kqueue in two sentences.' }],
});

function anthropicProvider(opts: ProviderOptions = {}): ModelProvider

Create an Anthropic provider that can list and construct models.

apiKey defaults to ANTHROPIC_API_KEY. listModels() calls GET /v1/models on baseUrl and infers capabilities per model id.

import { anthropicProvider } from 'fino:ai/model';

const provider = anthropicProvider();
const models = await provider.listModels();
const model = await provider.createModel(models[0].id);

function openai(opts: ProviderOptions = {}): Model

Create an OpenAI-backed Model.

apiKey defaults to OPENAI_API_KEY (throws when neither is set), baseUrl defaults to the public OpenAI API, and model defaults to gpt-4o. Point baseUrl at any OpenAI-compatible server such as vLLM or llama-server. The returned model also implements EmbeddingModel backed by the OpenAI embeddings endpoint.

import { openai } from 'fino:ai/model';

const model = openai({ model: 'gpt-4o-mini' });
const stream = model.stream({
  messages: [{ role: 'user', content: 'Stream a limerick about io_uring.' }],
});
for await (const event of stream) {
  if (event.type === 'text_delta') write(event.text);
}

function openaiProvider(opts: ProviderOptions = {}): ModelProvider

Create an OpenAI-compatible provider that can list and construct models.

apiKey defaults to OPENAI_API_KEY. listModels() calls GET /models on baseUrl; servers without a models endpoint make it throw ModelListingUnsupportedError.

import { openaiProvider } from 'fino:ai/model';

const provider = openaiProvider({ baseUrl: 'http://localhost:8000/v1', apiKey: 'local' });
const models = await provider.listModels();
const model = await provider.createModel(models[0].id);

Constants

const ModelError

Error thrown for provider HTTP failures.

Carries the HTTP status, the raw response body when available, and retryAfterMs parsed from a Retry-After header — useful for backoff on 429 and 5xx responses.

import { ModelError } from 'fino:ai/model';

try {
  await model.generate({ messages });
} catch (err) {
  if (err instanceof ModelError && err.status === 429) {
    await delay(err.retryAfterMs ?? 1000);
  } else {
    throw err;
  }
}

const ModelListingUnsupportedError

Error thrown when a provider endpoint does not support model listing.

Raised by ModelProvider.listModels() when the models endpoint responds with 404 — common for OpenAI-compatible servers that implement chat but not discovery. Carries the provider name, HTTP status, and response body.

const hasLlamaCpp

Whether a default system libllama was found when this module was evaluated.

This is a capability gate for optional local model support. It reflects only default lookup paths such as LLAMA_CPP_LIBRARY and FINO_LLAMA_LIBRARY; callers can still pass an explicit libraryPath to local().

import { anthropic, hasLlamaCpp, local } from 'fino:ai/model';

const model = hasLlamaCpp
  ? await local({ model: './models/qwen2.5-0.5b-instruct-q4_k_m.gguf' })
  : anthropic();

const LocalModelLibraryError

Error thrown when local llama.cpp support is requested but no usable libllama library is available.

Carries the libraryPath that failed to load, when one was given. Check hasLlamaCpp first to avoid the throw on default lookup paths.

const LocalModelUnsupportedError

Error thrown for local model request features that the llama.cpp adapter does not implement.

Currently raised for requests that use tools, toolChoice, or responseFormat, and for embed() calls on local models.

const local

Create a local llama.cpp-backed Model.

The model source can be a local GGUF path or an explicit Hugging Face GGUF file. Construction is async because Hugging Face sources may need to be downloaded into the local cache before llama.cpp opens them. Throws LocalModelLibraryError when no usable libllama can be loaded. Close long-lived models with model.close?.() to release the llama.cpp handle.

import { local } from 'fino:ai/model';

const model = await local({
  model: { repo: 'Qwen/Qwen2.5-0.5B-Instruct-GGUF', file: 'qwen2.5-0.5b-instruct-q4_k_m.gguf' },
  contextSize: 8192,
  gpuLayers: 99,
});
const result = await model.generate({
  messages: [{ role: 'user', content: 'Write a haiku about rivers.' }],
});

const localProvider

Create a local provider for configured GGUF models.

Local providers do not discover remote model catalogs. listModels() returns the configured entries so registries can present local and remote models through one interface.

import { localProvider, modelRegistry } from 'fino:ai/model';

const registry = modelRegistry([
  localProvider({
    models: [
      { id: 'qwen-0.5b', model: './models/qwen2.5-0.5b-instruct-q4_k_m.gguf' },
    ],
  }),
]);
const model = await registry.create('qwen-0.5b');