moonmuse

One request, four API specs

A system prompt, one user message, temperature 0.2, a 256-token cap. Sent to OpenAI, Anthropic, Gemini and Ollama, that conversation is four different documents. moonmuse gives MoonBit one contract above all four, and reads and writes each of them with the same code.

  • 4wire formats
  • 1request type
  • 17packages
  • 0vendor names in code

POST /v1/chat/completions

{
  "model": "gpt-4o-mini",
  "messages": [
    {"role": "system", "content": "Answer in one sentence."},
    {"role": "user", "content": "Why is the sky blue?"}
  ],
  "temperature": 0.2,
  "max_tokens": 256
}

The bodies in these tabs are written by this library, compiled to JavaScript and running on this page.


01 · The idea

Name the vendor once

A provider is anything that can answer a request. Which vendor, whether it runs in a data centre or on the laptop, and which JSON it speaks are details behind that one value.

MoonBit
let provider : &@moonmuse.Provider =
  @openai.Client::new(api_key="sk-…")

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)
println(reply.text())
Provider

Ask for an answer, a stream, vectors, or the model list. Six implementations ship here, and a layer is a provider too — so retry, fallback, routing and caching wrap in any order.

Dialect

One vendor's wire format, read and written. The client and the gateway use the same value, which is why they cannot disagree about a field.

02 · What it does

Everything around one call

The request is the small part. These are the parts a second integration usually has to write again.

Streaming

One callback

Text, a tool call starting, its arguments arriving in pieces, then done — the same four deltas from a JSON chunk, seven named events, a candidate array or a line of NDJSON.

Tools

A schema and a function

Toolbox::call is the only place a call becomes a result, and a tool that fails produces an error result rather than an exception.

Layers

Retry, fall back, cache

Only failures another attempt could avoid are retried. Fallback switches on the same classification; the cache key covers every field that decides an answer.

Structure

An answer that fits

Ask for JSON against a schema, check the reply, and send it back with the reason when it does not fit — up to a budget.

Serving

Four APIs out of one

Point serve at any provider and it answers as OpenAI, Anthropic, Gemini or Ollama. The official client libraries cannot tell it from the vendor.

Observation

Traces under the conventions

One client span per call and token metrics, under the OpenTelemetry GenAI names. Message bodies stay out unless asked for.

03 · The module

One package per concern

A package that is not imported is not compiled or linked, so the local-model and serving halves cost nothing to a program that only asks for a completion.

moonmuseMessages, requests, replies, the two contracts, the event-stream reader
openaiThe chat format and every endpoint that speaks it
anthropicThe Messages API, its event stream, its token counter
geminigenerateContent, streamGenerateContent, embeddings
ollamaThe native /api endpoints and the newline-delimited stream
bedrockSigV4 signing and the Converse API, event-stream framing included
layersRetry, fallback, rate limiting, caching, routing
toolboxTool execution and the tool loop
structuredA JSON answer that fits a schema, with a repair loop
promptPrompt templates, rendered against a value map
modelsContext windows, output limits, prices, capabilities
countToken counting over a vocabulary
serveAny provider exported as four APIs
transportThe one place a call becomes bytes on a socket
localA local engine behind the same contract
otelTraces and metrics by the GenAI conventions
fakeA scripted provider, for tests above the wire

04 · Where to go

Read it in the order you write it