Prism is an LLM protocol conversion engine. It converts between provider formats (OpenAI, Anthropic, Gemini, Azure, Vertex, vLLM) via a neutral intermediate representation called Lucent IR. The WASM build exposes 15 pure functions — all String → String, no state, no side effects — making it safe and easy to embed in any language.
Architecture Overview
Your App ──String JSON──▸ prism.wasm ──String JSON──▸ Your App
(provider (convert via (provider
format A) Lucent IR) format B)
Two usage patterns:
- SDK mode — your app speaks one provider format, Prism encodes/decodes for you
- Transit mode — convert provider A's format directly to provider B's format in one call
Critical: The String ABI
Prism uses MoonBit's classic wasm target. Strings are passed via linear memory with this layout:
┌──────────────────────────────────────────────────────┐
│ ptr - 4: u32 length (UTF-16 code units, LE) │
│ ptr: UTF-16LE payload (length × 2 bytes) │
└──────────────────────────────────────────────────────┘
- Arguments: write strings to linear memory, pass the
ptr(i32) to the function - Return value: the function returns an i32
ptrwith the same layout - Scratch region: use addresses below
0x1000for argument strings (the MoonBit GC heap starts above ~0x1000). Use a stride of 512+ bytes between arguments to avoid overlap.
Encoding a string argument
1. Encode your string as UTF-16LE code units
2. Write u32 length (number of code units, NOT bytes) at ptr - 4 in little-endian
3. Write UTF-16LE payload at ptr
4. Pass ptr (as i32) to the WASM function
Decoding a result string
1. Read u32 length from resultPtr - 4 (little-endian)
2. Read length × 2 bytes from resultPtr
3. Decode as UTF-16LE code units → your native string
Gotcha: Many languages use UTF-8 internally. You must convert to/from UTF-16LE for the WASM boundary. JavaScript/TypeScript natively uses UTF-16 so this is simpler there.
Exported Functions (15 total)
All functions take and return strings. Call them by name from the WASM module's exports.
Low-level IR conversion (6)
| Export | Args | Description |
|---|---|---|
wasm_to_lux_req |
(provider, json) |
Provider request JSON → LucentRequest JSON |
wasm_lux_req_to_provider |
(provider, luxJson) |
LucentRequest JSON → Provider request JSON |
wasm_to_lux_resp |
(provider, json) |
Provider response JSON → LucentResponse JSON |
wasm_lux_resp_to_provider |
(provider, luxJson) |
LucentResponse JSON → Provider response JSON |
wasm_sse_to_events |
(provider, sseText) |
Provider SSE text → StreamEvent JSON array |
wasm_events_to_sse |
(provider, eventsJson) |
StreamEvent JSON array → Provider SSE text |
High-level SDK (5)
| Export | Args | Description |
|---|---|---|
wasm_sdk_encode_req |
(provider, text) |
Plain text → Provider request JSON |
wasm_sdk_decode_resp |
(provider, json) |
Provider response JSON → Plain text |
wasm_sdk_encode_stream |
(provider, text) |
Plain text → Streaming provider request JSON |
wasm_sdk_decode_sse |
(provider, sseText) |
Provider SSE → PrismEvent JSON array |
wasm_sdk_capability |
(provider) |
Query provider capability declaration |
Transit conversion (3)
| Export | Args | Description |
|---|---|---|
wasm_convert_req |
(source, json, target) |
Convert request: source provider → target provider |
wasm_convert_resp |
(source, json, target) |
Convert response: source provider → target provider |
wasm_convert_stream |
(source, sseText, target) |
Convert SSE stream: source → target |
Query (1)
| Export | Args | Description |
|---|---|---|
wasm_list_providers |
() |
JSON array of all registered provider names |
Envelope Contract
Every function (except wasm_list_providers) returns an envelope:
{"value": "...", "diagnostics": [{"field": "...", "status": "exact|degraded|unsupported|invalid", "detail": "..."}]}
value— the result payload (object or JSON string depending on direction)diagnostics— conversion fidelity notes per field
Errors return:
{"error": "message", "diagnostics": []}
Always check for the error key first before accessing value.
Building prism.wasm
moon build --target wasm
# Output: _build/wasm/release/build/cmd/main/main.wasm
The cmd/main package is the executable entry point that defines the WASM exports via options(link: { "wasm": { "exports": [...], "export-memory-name": "memory" } }) in its moon.pkg.
Language-Specific Guides
Read the reference file for your target language:
- TypeScript/JavaScript →
references/typescript.md(also: existing wrapper atwrappers/ts/) - Go →
references/go.md(also: existing wrapper atwrappers/go/) - Rust →
references/rust.md - Python →
references/python.md
Each reference covers: loading the WASM module, implementing the string ABI, a working client class, and error handling.
Reference Implementations
The project includes production-quality wrappers you can study or use directly:
- Go wrapper (
wrappers/go/): Uses wazero runtime. Install:go get github.com/morning-start/prism/wrappers/go - TypeScript wrapper (
wrappers/ts/): Uses WebAssembly API. Install:npm install @morning-start/prism-wasm
These are the canonical implementations. When writing a new language wrapper, mirror their structure:
- Runtime layer — load WASM, implement
writeString/readStringfor the UTF-16 ABI - Client layer — wrap each export with typed methods, parse envelopes
- Types layer — define Envelope, Diagnostic, PrismEvent types natively
Common Pitfalls
-
UTF-8 vs UTF-16: The #1 mistake. Prism uses UTF-16LE in linear memory, not UTF-8. Most languages (Rust, Go, Python) use UTF-8 internally — you must transcode.
-
Length is code units, not bytes: The length header at
ptr - 4counts UTF-16 code units (each 2 bytes), not bytes. For BMP characters it equals the character count; for supplementary characters (>U+FFFF) one character becomes two code units. -
Scratch collision with GC heap: Don't write argument strings above
0x1000— the MoonBit garbage collector uses that region. Reset your scratch pointer on each call. -
Memory buffer detachment: In JS/TS,
WebAssembly.Memory.bufferdetaches when memory grows. Always get a freshDataView/Uint8Arrayper call, not once at load time. -
_start must run: The WASM module has a
_startexport that initializes the MoonBit runtime. Call it once after instantiation before any conversion function. -
Error envelope parsing: Don't assume the result is valid JSON on error. Check for
{"error":prefix first, then parse. -
wasm_list_providers takes no args: It's the only function with zero string arguments. Just call it directly — no need to write anything to linear memory.