moonmuse

    Talk to language models from MoonBit: one contract for chat, streaming, tools and embeddings, with a subpackage per wire format.

    llm
    ai
    genai
    provider
    gateway
    openai
    anthropic
    gemini
    google
    claude
    ollama
    deepseek
    kimi
    qwen
    vllm
    openrouter
    mistral
    dashscope
    chat
    completion
    chat-completions
    responses
    streaming
    sse
    server-sent-events
    ndjson
    tool-calling
    function-calling
    structured-output
    json-schema
    embeddings
    retry
    fallback
    rate-limit
    cache
    routing
    prompt
    template
    client
    server
    asgi
    moonbit
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    7

    #moonmuse

    One contract for every language model: chat, streaming, tools and embeddings.

    mooncakes license

    A model endpoint is a request and an answer. Which vendor answers it, whether it runs in a data centre or on the laptop, and whether the caller speaks OpenAI's JSON or Anthropic's is a detail of the wire — not of the code that asks. This library puts that detail in one place and leaves one contract above it.

    let provider = @openai.Client::new(api_key="sk-...")
    let reply = provider.chat(
    @moonmuse.Request::new("gpt-4o-mini", [@moonmuse.Message::user("Why is the sky blue?")]),
    )
    println(reply.text())

    Swap the constructor for @anthropic.Client::new(...), @gemini.Client::new(...) or @ollama.Client::new() and nothing below that line changes.

    #Install

    moon add moonbitstack/moonmuse

    #The two contracts

    ContractWhat it isWho implements it
    ProviderAsk for an answer, a stream, vectors, or the model list.@openai, @anthropic, @gemini, @ollama, @fake, every layer, your own
    DialectOne vendor's wire format, read and written.The four provider packages, your own

    A Dialect is the reason a gateway and a client cannot disagree: the same value turns a Request into an HTTP call on the way out and a served body into a Request on the way in.

    #Packages

    PackageWhat it adds
    moonmuseMessages, requests, replies, the two contracts, the server-sent-events reader
    moonmuse/openaiOpenAI's format, plus every endpoint that speaks it — DeepSeek, Kimi, Qwen, vLLM, OpenRouter, DashScope, Mistral — and the Responses, moderation, image, file, batch and fine-tuning calls
    moonmuse/anthropicThe Messages API, its event stream, and its token counter
    moonmuse/geminigenerateContent, streamGenerateContent, embedContent
    moonmuse/ollamaThe native /api endpoints, including the newline-delimited stream
    moonmuse/layersRetry, fallback, rate limiting, caching, routing — each a provider around a provider
    moonmuse/toolboxTool execution and the tool loop
    moonmuse/modelsContext windows, output limits, list prices, capabilities
    moonmuse/structuredAsk for a JSON answer that fits a schema, with a repair loop
    moonmuse/promptPrompt templates, rendered by moonpug
    moonmuse/fakeA scripted provider, for tests of everything above the wire
    moonmuse/serveExport any provider as an OpenAI, Anthropic, Gemini or Ollama API
    moonmuse/localA local model as a provider: chat template, tool-call parsing, an engine contract

    #Chat

    let request = @moonmuse.Request::new(
    "claude-sonnet-4-20250514",
    [
    @moonmuse.Message::system("Answer in one sentence."),
    @moonmuse.Message::user("Why is the sky blue?"),
    ],
    temperature=0.2,
    max_tokens=256,
    stop=["\n\n"],
    )
    let reply = provider.chat(request)
    let (text, usage) = (reply.text(), reply.usage.total())

    Request.extra is merged into the body last, so a vendor's own field — reasoning_effort, a beta switch — reaches the wire without this library modelling it. Reply.raw keeps the body that came back for the fields the shared vocabulary does not hold.

    #Streaming

    let reply = provider.stream(request, delta => {
    match delta {
    Text(chunk) => print(chunk)
    CallStart(_, name~) => println("\ncall " + name)
    CallArgs(_, chunk~) => print(chunk)
    Done(_) => println("")
    }
    })

    The returned reply is the same value chat would have returned, so a caller that ignores the deltas still gets the whole answer.

    #Tools

    let tools = @toolbox.Toolbox::new()
    tools.add(@moonmuse.Tool::new("add", "Add two integers"), args => {
    (json_int(args, "a").unwrap_or(0) + json_int(args, "b").unwrap_or(0)).to_string()
    })
    let reply = @toolbox.run(provider, request, tools, limit=10)

    Toolbox::call is the only place a call becomes a result, so a framework with its own loop uses it directly and gets the same behaviour — including the rule that a failing tool produces an error result rather than an exception.

    #Layers

    let gateway = @layers.cached(
    @layers.retry(@layers.fallback([primary, secondary])),
    @layers.Memory::new(limit=512),
    )

    Every layer is a Provider, so they compose in any order. Retry honours retry-after and only retries failures that could pass on a second try; fallback switches on the same classification; the cache key covers every field that decides an answer.

    #Structured output

    let answer = @structured.ask(
    provider,
    request,
    name="weather",
    schema=my_schema,
    decode=json => json_int(json, "celsius"),
    )

    The schema is stated in the prompt and checked against the reply with moonschema; a reply that does not fit goes back with the reason, up to a budget, and the last failure is raised.

    #Serve

    let app = @serve.serve(
    provider,
    dialects=[@openai.dialect, @anthropic.dialect, @gemini.dialect, @ollama.dialect],
    keys=@serve.StaticKeys::new(["sk-local"]),
    )

    serve returns an AsgiApp: run it with any ASGI server, or mount it under a framework. The endpoints are the dialects' own, so a format added once is served, called and parsed by the same code.

    FormatEndpoints
    OpenAI/v1/chat/completions, /v1/completions, /v1/responses, /v1/embeddings, /v1/models
    Anthropic/v1/messages, /v1/messages/count_tokens, /v1/models
    Ollama/api/chat, /api/generate, /api/embed, /api/tags, /api/show, /api/version
    Gemini/v1beta/models/*:generateContent, :streamGenerateContent, :embedContent

    Two formats claim /v1/models; the anthropic-version header decides which answers.

    #Local models

    A local model is an inference engine plus the text format it was trained on. The engine is a contract here, so moonmuse/local does not know how weights are read; the format is data.

    let model = @local.Local::new(
    engine, // your inference engine
    template=@local.llama3(), // or @local.chatml()
    calls=@local.Tagged, // how it writes a tool call
    model="qwen2.5-7b",
    )
    let reply = model.chat(request) // a Provider like any other

    The answer is text, and a tool call is a JSON object inside it, so @local.parse finds the calls, removes them from what the user sees, and numbers them. A template that does not render is an error, not a silent empty prompt.

    #Model catalogue

    let info = @models.find("gpt-4o-mini")
    let cost = @models.estimate_cost("gpt-4o-mini", reply.usage)

    Prices are US dollars per million tokens as published by each vendor, and change without notice; the reply's own usage is what a call actually cost.

    #Consistency

    What is checked, and how:

    • Wire round trips. For each of the four formats, a request goes out through the dialect, is parsed back, and must equal the original — and a reply becomes a body and reads back the same. The fixtures are the shapes the vendors document.
    • Recorded streams. A recorded OpenAI chat stream, Anthropic event stream and Ollama newline stream are replayed through the reader; the assembled reply must match the non-streamed one. The OpenAI stream is also cut at every byte offset, to prove the reader never depends on where the transport splits a frame.
    • The stream writer against the reader. The frames a gateway writes are read back by the client reader, and must produce the same deltas.
    • A real server. The Ollama client is exercised against a live server in CI: chat, a tool declaration, and embeddings. Those tests run when MOONMUSE_LIVE is set, and fail loudly if it is set and the server is missing.
    • Transport. moonmuse/transport maps a Call onto moonfetch, including status and retry-after; a non-JSON error body is reported as a status, not as a decode failure.
    • Official client libraries. scripts/interop runs the gateway on port 12000 and drives it with openai, anthropic and ollama — the real SDKs, not hand-written requests. CI runs chat, streaming, tool calls, embeddings, model lists and an error case.
    • The official field description. scripts/openapi/gen_fields.py reads openai-openapi at a pinned commit and writes the field table the tests check bodies against (21 schemas, 182 fields). A body that carries a key the description does not have fails the build, and a stale table fails CI before it can pass a wrong body.
    • LangChain standard tests. The cases a provider can carry are transcribed in src/serve/langchain_wbtest.mbt, keeping the original names: conversation and double-message histories, usage with and without streaming, stop sequences, model override, tool calling with and without arguments, tool choice, tool-result and error histories, image inputs, Unicode tool calls, and sequential calls. The cases that test LangChain's own binding, event graph or timing layer are listed there with why they do not apply.

    Not yet checked, and known to be missing:

    • Streamed answers from a client. moonfetch has no streamed response yet, so Provider::stream on a client fetches the whole answer and replays it as deltas: the deltas are correct, the timing is not. The Dialect side — reading a real stream and writing one — is complete and tested.
    • /v1/messages/count_tokens answers Unsupported: counting tokens needs a tokenizer, and this library does not ship one.
    • The LangChain standard-tests transcription and a scripted comparison of @openai fields against openai-openapi are still owed.
    • moonmuse/local ships the template, the call parser and the engine contract; it does not ship an inference engine, so a model has to be wired in.

    #Targets

    moonmuse and its pure subpackages build for wasm, wasm-gc, js and native. The four provider clients need a transport, which moonfetch provides on native and wasm only; the other two targets still have the dialects, so a gateway can be written and tested without a socket.

    #License

    Apache-2.0.

    AnyProvider

    type AnyProvider = &Provider

    A provider reference that can be stored in a record or an array.

    Dialect

    pub(open) trait Dialect {
    fn name(Self) -> String
    fn request(Self, Request) -> Call raise LlmError
    fn reply(Self, status : Int, body :
    Json
    ) -> Reply raise LlmError
    fn delta(Self, Frame, Turn) -> Array[Delta] raise LlmError
    fn routes(Self) -> Array[Route]
    fn parse(Self, Route,
    Json
    ) -> Asked raise LlmError
    fn answer(Self, Reply) ->
    Json

    fn answer_embed(Self, String, Array[Array[Double]]) ->
    Json

    fn answer_count(Self, Reply) ->
    Json

    fn answer_models(Self, Array[ModelInfo]) ->
    Json

    fn answer_at(Self, RouteKind, Reply) ->
    Json

    fn frame(Self, Delta, Turn) -> Array[Frame]
    fn error(Self, LlmError) -> (Int,
    Json
    )
    }

    One vendor's wire format, read and written in the same place. The client half turns a Request into a Call and a body into a Reply; the server half turns a body into a Request and a Reply into a body. Because both halves live here, a gateway that speaks a format out cannot drift from the client that speaks it in.

    Provider

    pub(open) trait Provider {
    async fn chat(Self, Request) -> Reply raise LlmError
    async fn stream(Self, Request, (Delta) -> Unit) -> Reply raise LlmError
    async fn embed(Self, String, Array[String]) -> Array[Array[Double]] raise LlmError
    async fn models(Self) -> Array[ModelInfo] raise LlmError
    }

    The one contract every model endpoint implements: a remote API, a local server, a scripted fake, or a layer wrapped around another provider. Swapping providers is a change to the constructor line, not to the calling code.

    LlmError

    pub(all) suberror LlmError {
    Status(code~ : Int, body~ : String, retry_after~ : Int64?)
    Stream(String)
    Decode(String)
    Unsupported(String)
    } derive(Eq,
    Debug
    )

    Everything a provider call can fail with. The four cases are the only distinctions a caller can act on: retry, parse differently, or stop asking.

    LlmError::describe

    fn LlmError::describe(self : LlmError) -> String

    A one-line description, for logs and for retry classification.

    LlmError::equal

    fn LlmError::equal(LlmError, LlmError) -> Bool

    LlmError::is_retryable

    fn LlmError::is_retryable(self : LlmError) -> Bool

    Whether asking again could plausibly succeed. A rate limit, a server fault and a dropped stream qualify; a malformed body or a missing capability does not.

    LlmError::not_equal

    fn LlmError::not_equal(x : LlmError, y : LlmError) -> Bool

    LlmError::retry_after

    fn LlmError::retry_after(self : LlmError) -> Int64?

    The wait the endpoint asked for, when it named one.

    LlmError::to_repr

    Asked

    pub(all) enum Asked {
    Chat(Request)
    Embed(model~ : String, inputs~ : Array[String])
    Models
    Count(Request)
    } derive(Eq,
    Debug
    )

    What a server-side request asked for, decoded into the shared vocabulary.

    Asked::equal

    fn Asked::equal(Asked, Asked) -> Bool

    Asked::not_equal

    fn Asked::not_equal(x : Asked, y : Asked) -> Bool

    Asked::to_repr

    Assembly

    pub struct Assembly {
    text : String
    order : Array[String]
    names : Map[String, String]
    args : Map[String, String]
    done : Reply?
    }

    Assembles deltas into the reply they describe. Text accumulates in arrival order, argument fragments accumulate per call, and the Done reply wins when present.

    Assembly::new

    fn Assembly::new() -> Assembly

    An empty assembly.

    Assembly::push

    fn Assembly::push(self : Assembly, delta : Delta) -> Unit

    Feed one delta in.

    Assembly::reply

    fn Assembly::reply(self : Assembly) -> Reply

    The assembled reply: the Done one when the stream sent it, otherwise one built from what accumulated.

    Call

    pub(all) struct Call {
    verb : String
    path : String
    headers : Array[(String, String)]
    body :
    Json

    } derive(Eq,
    Debug
    )

    One HTTP request a dialect produced from a Request — verb, path, headers, body. A client only has to send it; nothing in it is provider-specific beyond what the dialect already decided.

    Call::equal

    fn Call::equal(Call, Call) -> Bool

    Call::header

    fn Call::header(self : Call, name : String, value : String) -> Call

    A copy with a header set, replacing any previous value of the same name.

    Call::new

    fn Call::new(verb : String, path : String, body :
    Json
    ) -> Call

    A call with the provider's JSON content type and no extra header.

    Call::not_equal

    fn Call::not_equal(x : Call, y : Call) -> Bool

    Call::to_repr

    Capability

    pub(all) enum Capability {
    Chat
    Tools
    Reasoning
    Vision
    Audio
    Embedding
    Json
    } derive(Eq,
    Debug
    )

    A capability a model may declare. The catalogue in moonmuse/models uses these to describe what an endpoint can actually do, so a caller can pick a model for a job instead of guessing from its name.

    Capability::equal

    fn Capability::equal(Capability, Capability) -> Bool

    Capability::not_equal

    fn Capability::not_equal(x : Capability, y : Capability) -> Bool

    Delta

    pub(all) enum Delta {
    Text(String)
    CallStart(id~ : String, name~ : String)
    CallArgs(id~ : String, chunk~ : String)
    Done(Reply)
    } derive(Eq,
    Debug
    )

    One event of a streamed answer. Done closes it with the assembled reply, so a caller that ignores the deltas still gets the same value as a non-streamed call.

    Delta::equal

    fn Delta::equal(Delta, Delta) -> Bool

    Delta::not_equal

    fn Delta::not_equal(x : Delta, y : Delta) -> Bool

    Delta::to_repr

    Finish

    pub(all) enum Finish {
    Stop
    Length
    Calls
    Filtered
    Other(String)
    } derive(Eq,
    Debug
    )

    Why generation stopped, in the four cases worth branching on plus a catch-all for a vendor's own reason string.

    Finish::equal

    fn Finish::equal(Finish, Finish) -> Bool

    Finish::not_equal

    fn Finish::not_equal(x : Finish, y : Finish) -> Bool

    Finish::of

    fn Finish::of(reason : String) -> Finish

    The Finish a vendor's reason string names. Shared by the four dialects, which differ in spelling but not in meaning.

    Finish::to_repr

    Frame

    pub(all) struct Frame {
    event : String?
    data : String
    } derive(Eq,
    Debug
    )

    One frame of a streamed response: the server-sent-events event name when the endpoint uses named events, and the payload. JSON endpoints that stream newline delimited objects leave event empty.

    Frame::equal

    fn Frame::equal(Frame, Frame) -> Bool

    Frame::json

    The frame's payload as JSON, or Decode when it is not JSON.

    Frame::named

    fn Frame::named(event : String, data : String) -> Frame

    A named frame.

    Frame::new

    fn Frame::new(data : String) -> Frame

    A frame with no event name.

    Frame::not_equal

    fn Frame::not_equal(x : Frame, y : Frame) -> Bool

    Frame::to_repr

    Message

    pub(all) struct Message {
    role : Role
    parts : Array[Part]
    } derive(Eq,
    Debug
    )

    A message: a role and its ordered parts.

    Message::assistant

    fn Message::assistant(text : String) -> Message

    An assistant message carrying one text part.

    Message::calls

    fn Message::calls(self : Message) -> Array[Part]

    The tool calls this message requests, in order. Empty on a plain text reply.

    Message::equal

    fn Message::equal(Message, Message) -> Bool

    Message::is_call

    fn Message::is_call(self : Message) -> Bool

    Whether this message asks for a tool.

    Message::new

    fn Message::new(role : Role, parts : Array[Part]) -> Message

    A message built from a role and its parts.

    Message::not_equal

    fn Message::not_equal(x : Message, y : Message) -> Bool

    Message::push

    fn Message::push(self : Message, part : Part) -> Message

    A copy with part appended. The tool loop grows the conversation this way.

    Message::results

    fn Message::results(self : Message) -> Array[Part]

    The tool results this message carries, in order.

    Message::system

    fn Message::system(text : String) -> Message

    A system message carrying one text part.

    Message::text_content

    fn Message::text_content(self : Message, separator? : String) -> String

    The text parts joined by separator. What a caller shows a human, and what a provider without structured content receives.

    Message::to_repr

    Message::tool

    fn Message::tool(content : String, id~ : String, error? : Bool) -> Message

    A tool message carrying one result, addressed to the call id.

    Message::user

    fn Message::user(text : String) -> Message

    A user message carrying one text part.

    ModelInfo

    pub(all) struct ModelInfo {
    id : String
    context : Int?
    output : Int?
    input_price : Double?
    output_price : Double?
    capabilities : Array[Capability]
    } derive(Eq,
    Debug
    )

    What a provider says about one model. Prices are US dollars per million tokens, because that is the unit every vendor publishes and the one a cost estimate wants.

    ModelInfo::equal

    fn ModelInfo::equal(ModelInfo, ModelInfo) -> Bool

    ModelInfo::new

    fn ModelInfo::new(id : String, context? : Int, output? : Int, input_price? : Double, output_price? : Double, capabilities? : Array[Capability]) -> ModelInfo

    A model with everything unknown except its id.

    ModelInfo::not_equal

    fn ModelInfo::not_equal(x : ModelInfo, y : ModelInfo) -> Bool

    ModelInfo::supports

    fn ModelInfo::supports(self : ModelInfo, capability : Capability) -> Bool

    Whether the model declares capability.

    Part

    pub(all) enum Part {
    Text(String)
    Image(url~ : String, media~ : String?)
    Call(id~ : String, name~ : String, args~ :
    Json
    )
    Result(id~ : String, content~ : String, error~ : Bool)
    } derive(Eq,
    Debug
    )

    One typed piece of a message. A message is an ordered list of these, so text and a tool call can share one assistant turn — which is what every provider returns when a model calls a tool and explains itself in the same breath.

    Part::call

    fn Part::call(id : String, name : String, args :
    Json
    ) -> Part

    A tool call part.

    Part::equal

    fn Part::equal(Part, Part) -> Bool

    Part::not_equal

    fn Part::not_equal(x : Part, y : Part) -> Bool

    Part::result

    fn Part::result(id : String, content : String, error? : Bool) -> Part

    A tool result part.

    Part::text

    fn Part::text(text : String) -> Part

    A text part.

    Part::text_of

    fn Part::text_of(self : Part) -> String

    The text this part carries: a text part's own text, a result's content, and nothing for a call or an image.

    Part::to_repr

    Reply

    pub(all) struct Reply {
    message : Message
    finish : Finish
    usage : Usage
    raw :
    Json

    } derive(Eq,
    Debug
    )

    A model's answer: the assistant message, why it stopped, what it cost, and the response body as it arrived, for fields the shared vocabulary does not model.

    Reply::calls

    fn Reply::calls(self : Reply) -> Array[Part]

    The reply's tool calls.

    Reply::equal

    fn Reply::equal(Reply, Reply) -> Bool

    Reply::new

    fn Reply::new(message : Message, finish? : Finish, usage? : Usage, raw? :
    Json
    ) -> Reply

    A reply from an assistant message.

    Reply::not_equal

    fn Reply::not_equal(x : Reply, y : Reply) -> Bool

    Reply::text

    fn Reply::text(self : Reply) -> String

    The reply's text.

    Reply::to_repr

    Request

    pub(all) struct Request {
    model : String
    messages : Array[Message]
    tools : Array[Tool]
    choice : ToolChoice
    temperature : Double?
    max_tokens : Int?
    stop : Array[String]
    extra : Map[String,
    Json
    ]
    } derive(Eq,
    Debug
    )

    One request, in the vocabulary every provider shares. extra is merged into the request body last of all, so a field this library does not model — a vendor's beta switch, a reasoning budget — still reaches the wire.

    Request::equal

    fn Request::equal(Request, Request) -> Bool

    Request::new

    fn Request::new(model : String, messages : Array[Message], tools? : Array[Tool], choice? : ToolChoice, temperature? : Double, max_tokens? : Int, stop? : Array[String], extra? : Map[String,
    Json
    ]) -> Request

    A request with the provider's defaults.

    Request::not_equal

    fn Request::not_equal(x : Request, y : Request) -> Bool

    Request::push

    fn Request::push(self : Request, message : Message) -> Request

    A copy with one more message — the tool loop appends the assistant's call and the tool's result this way.

    Request::to_repr

    Request::with_messages

    fn Request::with_messages(self : Request, messages : Array[Message]) -> Request

    A copy with the messages replaced.

    Role

    pub(all) enum Role {
    System
    User
    Assistant
    Tool
    } derive(Eq,
    Debug
    )

    Who authored a message. A tool exchange is a role of its own here rather than a part of a user message, because OpenAI, Anthropic, Gemini and Ollama all put it in a distinct place on the wire.

    Role::code

    fn Role::code(self : Role) -> String

    The lowercase name a role goes by on the wire, using the OpenAI spelling.

    Role::equal

    fn Role::equal(Role, Role) -> Bool

    Role::not_equal

    fn Role::not_equal(x : Role, y : Role) -> Bool

    Role::to_repr

    Route

    pub(all) struct Route {
    kind : RouteKind
    verb : String
    path : String
    } derive(Eq,
    Debug
    )

    One endpoint a dialect serves. path may contain a single *, which matches any run of characters — Gemini puts the model inside the path.

    Route::equal

    fn Route::equal(Route, Route) -> Bool

    Route::matches

    fn Route::matches(self : Route, verb : String, path : String) -> Bool

    Whether this route answers verb at path.

    Route::new

    fn Route::new(kind : RouteKind, verb : String, path : String) -> Route

    A route for a verb and a path pattern.

    Route::not_equal

    fn Route::not_equal(x : Route, y : Route) -> Bool

    Route::to_repr

    RouteKind

    pub(all) enum RouteKind {
    Chat
    Complete
    Responses
    Embed
    Models
    Count
    Show
    Version
    Generate
    } derive(Eq,
    Debug
    )

    The kinds of endpoint a dialect can both call and serve.

    RouteKind::equal

    fn RouteKind::equal(RouteKind, RouteKind) -> Bool

    RouteKind::not_equal

    fn RouteKind::not_equal(x : RouteKind, y : RouteKind) -> Bool

    Sse

    pub struct Sse {
    buf : String
    event : String?
    data : Array[String]
    started : Bool
    }

    A server-sent-events reader that takes the stream in whatever pieces the network delivers. A frame's fields may be split across two reads, and one read may carry several frames; the reader only dispatches at the blank line that ends a frame, so the boundaries of the transport never leak into the dialect.

    Sse::feed

    fn Sse::feed(self : Sse, chunk : StringView) -> Array[Frame]

    Take one piece of the stream and return the frames it completed.

    Sse::finish

    fn Sse::finish(self : Sse) -> Array[Frame]

    Dispatch whatever a truncated stream left behind. A server that closes without the final blank line has still sent a complete frame.

    Sse::is_idle

    fn Sse::is_idle(self : Sse) -> Bool

    Whether a frame is in progress.

    Sse::new

    fn Sse::new() -> Sse

    A reader with nothing buffered.

    Tool

    pub(all) struct Tool {
    name : String
    description : String
    schema :
    Json

    } derive(Eq,
    Debug
    )

    A tool the model may call: a name, a description, and a JSON Schema object for the arguments.

    Tool::equal

    fn Tool::equal(Tool, Tool) -> Bool

    Tool::new

    fn Tool::new(name : String, description : String, schema? :
    Json
    ) -> Tool

    A tool with an empty object schema, for a tool that takes no arguments.

    Tool::not_equal

    fn Tool::not_equal(x : Tool, y : Tool) -> Bool

    Tool::to_repr

    ToolChoice

    pub(all) enum ToolChoice {
    Auto
    NoTools
    Required
    Named(String)
    } derive(Eq,
    Debug
    )

    How a request should use tools.

    ToolChoice::equal

    fn ToolChoice::equal(ToolChoice, ToolChoice) -> Bool

    ToolChoice::not_equal

    fn ToolChoice::not_equal(x : ToolChoice, y : ToolChoice) -> Bool

    Turn

    pub struct Turn {
    seq : Int
    block : Int
    slots : Map[String, String]
    nums : Map[String, Int]
    }

    State carried across the frames of one streamed answer. Vendors number content blocks differently — OpenAI by tool-call index, Anthropic by block ordinal, Gemini by part ordinal — so the dialect keeps the counters here instead of in the client, and the same Turn serves the client reading a stream and the server writing one.

    Turn::add_num

    fn Turn::add_num(self : Turn, key : String, delta : Int) -> Int

    Add to a numeric scratch value and return the new total.

    Turn::add_slot

    fn Turn::add_slot(self : Turn, key : String, chunk : String) -> Unit

    Append to a scratch value.

    Turn::has_slot

    fn Turn::has_slot(self : Turn, key : String) -> Bool

    Whether a scratch value was recorded.

    Turn::new

    fn Turn::new() -> Turn

    A fresh stream state.

    Turn::next_block

    fn Turn::next_block(self : Turn) -> Int

    Take the next content-block ordinal.

    Turn::num

    fn Turn::num(self : Turn, key : String) -> Int

    A numeric scratch value, or zero.

    Turn::set_num

    fn Turn::set_num(self : Turn, key : String, value : Int) -> Unit

    Set a numeric scratch value.

    Turn::set_slot

    fn Turn::set_slot(self : Turn, key : String, value : String) -> Unit

    Record a scratch value.

    Turn::slot

    fn Turn::slot(self : Turn, key : String) -> String

    A scratch value, or the empty string.

    Turn::tick

    fn Turn::tick(self : Turn) -> Int

    Count one frame and return its ordinal.

    Usage

    pub(all) struct Usage {
    input : Int?
    output : Int?
    cached : Int?
    } derive(Eq,
    Debug
    )

    What a call cost. None means the provider reported nothing; a zero would claim the call was free.

    Usage::equal

    fn Usage::equal(Usage, Usage) -> Bool

    Usage::new

    fn Usage::new(input : Int, output : Int, cached? : Int) -> Usage

    Usage with both counts known and no cache report.

    Usage::not_equal

    fn Usage::not_equal(x : Usage, y : Usage) -> Bool

    Usage::to_repr

    Usage::total

    fn Usage::total(self : Usage) -> Int?

    Input plus output, when both were reported.

    Usage::zero

    fn Usage::zero() -> Usage

    Usage with nothing reported.

    check_vectors

    fn check_vectors(inputs : Array[String], vectors : Array[Array[Double]]) -> Array[Array[Double]] raise LlmError

    The embeddings of a batch, checked against the batch length. A short answer is a broken endpoint, not a partial success, because the caller indexes vectors by input position.

    encode_sse

    fn encode_sse(frames : Array[Frame]) -> String

    Encode frames as a server-sent-events body, with the event name when there is one.

    json_array

    The array at key, or an empty one.

    json_as_bool

    fn json_as_bool(json :
    Json
    ) -> Bool?

    The value of a JSON boolean.

    json_as_number

    fn json_as_number(json :
    Json
    ) -> Double?

    The value of a JSON number.

    json_as_string

    fn json_as_string(json :
    Json
    ) -> String?

    The text of a JSON string.

    json_bool

    fn json_bool(json :
    Json
    , key : String) -> Bool?

    The boolean at key, when it is a boolean.

    json_field

    The member at key, when this is an object that has it.

    json_int

    fn json_int(json :
    Json
    , key : String) -> Int?

    The integer at key, when it is a number.

    json_number

    fn json_number(json :
    Json
    , key : String) -> Double?

    The number at key, when it is a number.

    json_numbers

    fn json_numbers(value :
    Json
    ) -> Array[Double]

    The numbers in an array value.

    json_object

    fn json_object(json :
    Json
    , key : String) -> Map[String,
    Json
    ]?

    The object at key, or None.

    json_string

    fn json_string(json :
    Json
    , key : String) -> String?

    The string at key, when it is a string.

    json_string_or

    fn json_string_or(json :
    Json
    , key : String, fallback : String) -> String

    The string at key, or fallback.

    json_strings

    fn json_strings(value :
    Json
    ) -> Array[String]

    The strings in an array value, skipping anything that is not a string.

    merge_json

    fn merge_json(base : Map[String,
    Json
    ], overrides : Map[String,
    Json
    ]) -> Map[String,
    Json
    ]

    base with overrides merged over it, key by key. extra reaches the wire this way: a caller's field wins over the one this library wrote.

    numbers_json

    fn numbers_json(values : Array[Double]) ->
    Json

    A JSON array of the numbers in values.

    object_json

    A JSON object from key/value pairs, in order.

    parse_int_lossy

    fn parse_int_lossy(text : String) -> Int

    An integer from a decimal string, or zero. Used where a stream carries an index in a text field; a vendor that sends something else should not crash the reader.

    parse_json_lossy

    fn parse_json_lossy(text : String) ->
    Json

    A JSON document from a string, or the empty object when the string is not JSON. A streamed tool call whose arguments never complete is left as {} so the loop can report the tool's own error instead of failing the whole turn.

    parse_ndjson

    fn parse_ndjson(text : StringView) -> Array[Frame]

    Read a whole newline-delimited JSON body — Ollama's streaming shape and the fallback for endpoints that stream bare objects.

    parse_sse

    fn parse_sse(text : StringView) -> Array[Frame]

    Read a whole server-sent-events body at once.

    strings_json

    fn strings_json(items : Array[String]) ->
    Json

    A JSON array of the strings in items.