luv-ai
v1.1.0
Published
Canonical conversation type and provider morphisms for portable LLM apps. Forks, tool calls, streaming, and errors are first-class. Zero runtime dependencies.
Downloads
37
Maintainers
Readme
luv — TypeScript reference implementation
Hydration of the luv spec (spec/SPEC.md) in TypeScript. Runs in Bun
during development; the published package is a plain ESM library that
works in browsers, Node (≥18), Bun, and Deno. Zero runtime dependencies.
Install
npm install luv
# or
bun add luv
# or
pnpm add luvQuickstart
import { openaiClient, anthropicClient } from "luv";
const openai = openaiClient({ api_key: process.env.OPENAI_API_KEY! });
const anthropic = anthropicClient({ api_key: process.env.ANTHROPIC_API_KEY! });
const conv = {
spec_version: "1.0",
nodes: [
{
id: "n1",
parent_id: null,
message: {
role: "user",
content: [{ kind: "text", text: "Hello!" }],
},
},
],
};
// Same conversation, either provider, identical Reply shape.
const r1 = await openai.send(conv, { model: "gpt-4o-mini" });
const r2 = await anthropic.send(conv, {
model: "claude-haiku-4-5",
max_tokens: 1024,
});Streaming:
for await (const event of client.stream(conv, { model: "gpt-4o-mini" })) {
if (event.kind === "text_delta") process.stdout.write(event.text);
}Errors:
import { LuvError } from "luv";
try {
await client.send(conv, { model: "gpt-4o-mini" });
} catch (e) {
if (e instanceof LuvError) {
console.log(e.data.category); // "auth" | "rate_limit" | ...
console.log(e.data.message);
console.log(e.data.details); // canonical JSON string
}
}Configure per-error policy:
const client = openaiClient({
api_key,
on_error: {
rate_limit: "as_block", // surface as data instead of throwing
content_filter: "as_block",
},
});Layout
impl/typescript/
package.json
tsconfig.json
src/
index.ts — public exports
types.ts — canonical types + LuvError + ErrorCategory
encode.ts — canonical JSON encoders + stringify
stream.ts — consume_luv_stream_reply, produce_luv_stream_reply
validate.ts — five validators
morphisms/
openai_chat.ts — three morphism arrows
transport/
openai_chat.ts — three transport arrows + openaiClient
test/
bench.test.ts — walks spec/{cases,morphisms/*/cases}, byte-compares
scripts/
record.ts — refresh recorded fixtures against live API
smoke.ts — end-to-end live API smoke testScripts
| Command | What it does |
|---|---|
| bun test | Run the bench against on-disk fixtures (no network). |
| bun run build | Compile src/ to dist/ with type declarations (uses tsc). |
| bun run verify | Verify request-shape cases (luv→provider) against the live API; no file writes. |
| bun run record | Refresh recorded fixtures (input.json + regenerated expected.json) by hitting the live API. Reviewable via git diff. |
| bun run smoke | Live end-to-end smoke test of client.send + client.stream. |
All scripts that hit the live API expect OPENAI_API_KEY and/or
ANTHROPIC_API_KEY in either the environment or <repo-root>/.env.
Providers without a configured key are skipped.
Universal use
The src/ code uses only standard JavaScript and Web APIs (fetch,
ReadableStream, TextDecoder). It can be imported directly in a
browser, in Node, in Bun, or in any modern JS runtime. The bench runner
(test/) is Bun-specific because it walks the filesystem; everything
under src/ is universal.
Arrows registered with the bench
Universal (spec-level) arrows — spec/cases/:
consume_luv_stream_replyproduce_luv_stream_replyvalidate_luv_conversation
OpenAI morphism arrows — spec/morphisms/openai_chat/cases/:
luv_conversation_to_openai_requestopenai_response_to_luv_replyopenai_stream_to_luv_stream
OpenAI transport arrows — spec/morphisms/openai_chat/cases/:
luv_send_to_openai_http_requestopenai_http_response_to_luv_replyopenai_http_stream_to_luv_stream
Anthropic morphism arrows — spec/morphisms/anthropic_messages/cases/:
luv_conversation_to_anthropic_requestanthropic_response_to_luv_replyanthropic_stream_to_luv_stream
Anthropic transport arrows — spec/morphisms/anthropic_messages/cases/:
luv_send_to_anthropic_http_requestanthropic_http_response_to_luv_replyanthropic_http_stream_to_luv_stream
Also exported but not (yet) exercised by bench cases:
validate_luv_message, validate_luv_block, validate_luv_reply,
validate_luv_stream_reply.
OpenAI-compatible providers
openaiClient works with any provider that mirrors OpenAI's Chat
Completions wire format. Pass a base_url:
const togetherClient = openaiClient({
api_key: process.env.TOGETHER_API_KEY!,
base_url: "https://api.together.xyz/v1",
});See spec/morphisms/openai_chat/transport.md for the full list of
known-compatible providers.
Design notes
- Canonical JSON. Encoders construct plain objects with property
insertion in canonical key order;
JSON.stringifypreserves that order in ES2015+.stringify()walks the value tree to reject lone surrogates before serializing (Section 3 rule 3). - Validators. Single-pass walk, stable sort by JSON Pointer path at
the end. Path format matches the spec exactly (
/nodes/<i>/...). - Streaming.
openaiClient.stream()returnsAsyncIterable<StreamEventReply>— the natural shape for TS (for await). Internally it reads the Response body viaReadableStreamand emits luv events as bytes arrive; no buffering of the full response. - Recording.
bun run recordrefreshesinput.jsonfrom the live API and regeneratesexpected.jsonfrom the current arrow. Diffs surface ingit difffor human review before commit. Standard snapshot-test workflow. - Zero runtime dependencies. All shipped code is hand-written. The
transport layer uses
fetch,ReadableStream, andTextDecoder— all Web Standard APIs available in every modern runtime. The only dev dependency istypescript(for type-declaration emission during publish; seeDECISIONS.md). - Bun for development.
bun test,bun build,bun:test, and hand-rolled scripts. No bundlers, linters, or other tooling.
