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