Talk to language models from MoonBit: one contract for chat, streaming, tools and embeddings, with a subpackage per wire format.
let provider : &@moonmuse.Provider = @openai.Client::new(api_key="sk-test")
let request = @moonmuse.Request::new(
"gpt-4o-mini",
[@moonmuse.Message::user("Why is the sky blue?")],
temperature=0.2,
max_tokens=256,
)
let reply = provider.chat(request)
let answer = reply.text()moon add moonbitstack/moonmuse| Contract | What it is | Who implements it |
|---|---|---|
| Provider | Ask for an answer, a stream, vectors, or the model list. | @openai, @anthropic, @gemini, @ollama, @fake, every layer, your own |
| Dialect | One vendor's wire format, read and written. | The four provider packages, your own |
| Package | What it adds |
|---|---|
| moonmuse | Messages, requests, replies, the two contracts, the server-sent-events reader |
| moonmuse/openai | OpenAI's format, plus every endpoint that speaks it — DeepSeek, Kimi, Qwen, vLLM, OpenRouter, DashScope, Mistral — and the Responses, moderation, image, file, batch, fine-tuning and audio calls |
| moonmuse/anthropic | The Messages API, its event stream, and its token counter |
| moonmuse/gemini | generateContent, streamGenerateContent, embedContent |
| moonmuse/ollama | The native /api endpoints, including the newline-delimited stream |
| moonmuse/layers | Retry, fallback, rate limiting, caching, routing — each a provider around a provider |
| moonmuse/toolbox | Tool execution and the tool loop |
| moonmuse/models | Context windows, output limits, list prices, capabilities |
| moonmuse/structured | Ask for a JSON answer that fits a schema, with a repair loop |
| moonmuse/prompt | Prompt templates, rendered by moonpug |
| moonmuse/fake | A scripted provider, for tests of everything above the wire |
| moonmuse/serve | Export any provider as an OpenAI, Anthropic, Gemini or Ollama API |
| moonmuse/local | A local model as a provider: chat template, tool-call parsing, an engine contract |
| moonmuse/count | Token counting over a moontoken vocabulary, for count_tokens |
| moonmuse/otel | Traces and metrics by the OpenTelemetry GenAI conventions |
let with_system = @moonmuse.Request::new(
"claude-sonnet-4-20250514",
[
@moonmuse.Message::system("Answer in one sentence."),
@moonmuse.Message::user("Why is the sky blue?"),
],
stop=["\n\n"],
)
let priced = @models.estimate_cost("claude-sonnet-4-20250514", reply.usage)let streamed = provider.stream(request, delta => {
match delta {
@moonmuse.Text(chunk) => println(chunk)
@moonmuse.CallStart(name~, ..) => println("call " + name)
@moonmuse.CallArgs(chunk~, ..) => println(chunk)
@moonmuse.Done(_) => println("")
}
})let tools = @toolbox.Toolbox::new()
tools.add(@moonmuse.Tool::new("add", "Add two integers"), args => {
let a = @moonmuse.json_int(args, "a").unwrap_or(0)
let b = @moonmuse.json_int(args, "b").unwrap_or(0)
(a + b).to_string()
})
let looped = @toolbox.run(provider, request, tools, limit=10)let secondary : &@moonmuse.Provider = @ollama.Client::new()
let gateway = @layers.cached(
@layers.retry(@layers.fallback([provider, secondary])),
@layers.Memory::new(limit=512),
)
let served = gateway.chat(request)let weather = @moonmuse.parse_json_lossy(
"{\"type\":\"object\",\"properties\":{\"celsius\":{\"type\":\"number\"}}}",
)
let celsius = @structured.ask(
provider,
request,
name="weather",
schema=weather,
decode=json => @moonmuse.json_int(json, "celsius"),
)let app = @serve.serve(
provider,
dialects=[@openai.dialect, @anthropic.dialect, @gemini.dialect, @ollama.dialect],
keys=@serve.StaticKeys::new(["sk-local"]),
)| Format | Endpoints |
|---|---|
| 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, :batchEmbedContents |
let ranks = @vocab.Ranks::from_tiktoken(vocabulary_text) // a tiktoken table, read once
let tokenizer = @moontoken.Tokenizer::cl100k(ranks~)
let counted = @serve.serve(provider, dialects=[@anthropic.dialect], counter=@count.Tokens::new(tokenizer~))struct Echo {
}
impl @local.Engine for Echo with fn generate(_self, prompt, _limit) {
prompt
}
pub extend Echo with @local.Engine::{generate}let local : &@moonmuse.Provider = @local.Local::new(
Echo::{},
template=@local.llama3(),
calls=@local.Tagged,
model="qwen2.5-7b",
)
let answered = local.chat(request)let recorder = @trace.Recorder::new()
let rand = width => Bytes::make(width, b'\x01')
let tracer = @trace.Tracer::new(
rand~,
sink=recorder,
sampler=@trace.parent_based(root=@trace.always),
)
let observed = @otel.traced(provider, tracer)
let measured = @otel.metered(provider, @moonp8s.Registry::new())let client = @openai.Client::new(api_key="sk-test")
let audio = client.speech("Read this aloud.")
let said = client.transcribe(audio, filename="out.mp3")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, Int) -> 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)
}pub(all) struct ModelInfo {
id : String
context : Int?
output : Int?
input_price : Double?
output_price : Double?
capabilities : Array[Capability]
} derive(Eq, Debug)fn ModelInfo::new(id : String, context? : Int, output? : Int, input_price? : Double, output_price? : Double, capabilities? : Array[Capability]) -> ModelInfofn parse_int_lossy(text : String) -> IntInstall
Download zipTalk to language models from MoonBit: one contract for chat, streaming, tools and embeddings, with a subpackage per wire format.