OCaml-first SDK for LLM providers with an Eio native runtime and a zero-runtime-dependency JavaScript package generated by Melange. Chatoyant provides typed provider clients, structured outputs, tool calling, streaming, token/cost accounting, and root-only npm exports.
chatoyant /shuh-TOY-uhnt/ - having a changeable lustre.
.mli contracts, and typed tool definitions..d.ts and .d.cts declarations, and no runtime npm dependencies.Chat session API, one-shot text/data/stream shortcuts, tool calling, streaming accumulation, JSON roundtrip, and token/cost accounting.Provider | Env var | Detection |
|---|---|---|
OpenAI |
|
|
Anthropic |
|
|
xAI |
|
|
OpenRouter |
| Slash notation such as |
Local |
| Explicit |
Model presets are available for quick intent-based calls: fast, cheap, balanced, best, and reasoning.
open Chatoyant
let () =
Eio_main.run @@ fun env ->
let ai = Chatoyant.openai ~model:"gpt-5.6-luna" env in
match Chatoyant.gen_text ai "Say hello in three words." with
| Ok text -> print_endline text
| Error err -> prerr_endline (Chatoyant.Error.provider err)Typed tools are ordinary modules. Comments become schema descriptions, option means optional, and the generated tool value plugs into a chat.
module%tool Calculate = struct
type operation =
| Add
| Divide
type request = {
operation : operation; (** Operation to apply. *)
values : float list [@min_items 1]; (** Numbers to combine in order. *)
}
type answer = { result : float }
(** Combine numbers with a typed arithmetic operation. *)
let run : request -> (answer, string) result =
fun { operation; values } ->
match operation, values with
| _, [] -> Error "at least one value is required"
| Add, values -> Ok { result = List.fold_left ( +. ) 0. values }
| Divide, first :: rest ->
List.fold_left
(fun acc value -> Result.bind acc (fun n ->
if value = 0. then Error "division by zero" else Ok (n /. value)))
(Ok first) rest
|> Result.map (fun result -> { result })
end
let () =
Eio_main.run @@ fun env ->
let ai =
Chatoyant.openai ~model:"gpt-5.6-luna" ~tools:[ Calculate.tool ] env
in
ignore (Chatoyant.gen_text ai "Divide 83 by 3.")For the full native guide, see OCAML.md.
import { Chat, Schema, createTool, genData, genStream, genText } from "chatoyant";
const text = await genText("What is 2+2?", {
model: "fast",
system: "Return short answers.",
});
class Person extends Schema {
name = Schema.String({ description: "Person name" });
age = Schema.Integer({ minimum: 0 });
}
const person = await genData("Extract: Alice is 30 years old", Person);
for await (const chunk of genStream("Write a haiku about typed APIs.")) {
process.stdout.write(chunk);
}
const lookup = createTool({
name: "lookup",
description: "Lookup data",
parameters: { q: Schema.String({ minLength: 1 }) },
async execute({ args }) {
return { found: args.q };
},
});
const chat = new Chat({ model: "gpt-4o" });
chat.system("Use tools when useful.").user("Find needle").addTool(lookup);
console.log(await chat.generate());All JavaScript imports come from the package root:
import { Chat, OpenAI, OpenRouter, Tokens, JsonSchema, genText } from "chatoyant";Former subpath imports from the TypeScript package intentionally move to this root surface. See JAVASCRIPT.md.
From the repository root:
make build # OCaml + Melange build, then the esbuild npm bundle
make test # native tests, JS parity tests, tsc, and the JSON Schema suite
make check # the full release gate
make help # list all targetsmake check builds and tests the native OCaml package first, emits Melange ESM, bundles the npm package with esbuild, runs Node's native tests against dist/index.js, checks dist/index.d.ts with tsc, runs the pinned official JSON Schema suite through the bundled package, lints opam metadata, checks local documentation links, verifies package metadata, and dry-packs the npm artifact.
See CONTRIBUTING.md for setup, the project layout, and the release process, and CHANGELOG.md for release history.
If this package helps your project, consider sponsoring its maintenance: GitHub Sponsors.