LanguageModel
The Prompt API: a general-purpose language model that runs on the device, with no server, no API key and no data leaving the machine.
@inject Bit.Butil.LanguageModel languageModelMDN reference
Unavailable. Build a
server-side or manual fallback and treat this as the fast path, not the only path.
Availability whether the options you want can be served, Create a
session from a user gesture (the first creation downloads the model - gigabytes, minutes),
then prompt it and dispose it. An undisposed session keeps hundreds of megabytes of model
state alive.
IsSupported answers whether the API exists; Availability answers whether it can actually serve you - Available, Downloadable (a session will download the model first), Downloading, or Unavailable. Probe with the options you intend to create with, since an option set the model can't serve answers Unavailable.
@inject Bit.Butil.LanguageModel languageModel
var availability = await languageModel.Availability();
if (availability == AiAvailability.Unavailable) UseServerFallback();Creates a conversation, optionally with a system prompt and sampling settings. Call it from a user gesture: the first creation on a device triggers the model download, which the browser will not start without one. The progress handler receives a 0-1 fraction while that happens.
_session = await languageModel.Create(
new LanguageModelOptions
{
SystemPrompt = "You answer in one short sentence.",
Temperature = 0.7,
TopK = 3,
},
onDownloadProgress: fraction =>
{
_progress = fraction;
InvokeAsync(StateHasChanged);
});Sends a turn and resolves with the whole answer. The session is stateful - every turn sees the ones before it - so this is a conversation, not a series of independent calls.
{
// The session from Create above. It carries the conversation, so the model sees everything asked
// through it - which is also what fills the context window up.
private LanguageModelSession? _session;
private async Task Ask()
{
var answer = await _session!.Prompt("Name three uses for a paperclip.");
}
}The same turn, reported as it is generated. Each chunk is the delta, not the text so far - append it. The handler runs on the interop dispatch, so a component has to call StateHasChanged itself.
<p>_answer</p>
{
private string _answer = "";
private LanguageModelSession? _session; // from Create
private async Task Stream(string prompt)
{
_answer = "";
await _session!.PromptStreaming(prompt, chunk =>
{
_answer += chunk;
InvokeAsync(StateHasChanged);
});
}
}Append adds turns the model should know about without asking for a reply. MeasureInputUsage says what a turn would cost before you spend it. GetUsage reports how much of the quota is gone - a session that runs out starts dropping the oldest turns. Clone forks the conversation, which is how 'regenerate this answer' keeps the original.
{
private LanguageModelSession? _session; // from Create
private async Task Budget(string draft)
{
var cost = await _session!.MeasureInputUsage(draft);
var usage = await _session.GetUsage();
if (cost > usage?.Remaining) TrimTheConversation();
// A fork shares everything said so far and diverges from here, which is how a branch of the
// conversation is explored without spending the original session's context on it.
var fork = await _session.Clone();
}
}API reference
ValueTask<bool> IsSupported()ValueTask<AiAvailability> Availability() / Availability(LanguageModelOptions options)ValueTask<AiModelParams?> GetParams()ValueTask<LanguageModelSession?> Create(LanguageModelOptions? options = null, Action<double>? onDownloadProgress = null)ValueTask<string?> Prompt(string input)Task<string> PromptStreaming(string input, Action<string>? onChunk = null)ValueTask<bool> Append(params AiPrompt[] prompts)ValueTask<double> MeasureInputUsage(string input)ValueTask<AiUsage?> GetUsage()ValueTask<LanguageModelSession?> Clone()ValueTask DisposeAsync()