llm-dialect 0.1.1

Pure sans-I/O LLM dialect translation: Anthropic Messages / OpenAI Chat / OpenAI Responses wire formats ↔ a canonical model, plus SSE framer state machines. No runtime, no I/O, no wall clock.
Documentation
# llm-dialect

[![ci](https://github.com/df49b9cd/llm-dialect/actions/workflows/ci.yml/badge.svg)](https://github.com/df49b9cd/llm-dialect/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/llm-dialect.svg)](https://crates.io/crates/llm-dialect)
[![docs.rs](https://docs.rs/llm-dialect/badge.svg)](https://docs.rs/llm-dialect)

```toml
llm-dialect = "0.1" # MSRV 1.88
```

Pure sans-I/O LLM dialect translation: Anthropic Messages, OpenAI Chat
Completions, and OpenAI Responses wire formats ↔ a canonical request/response
model, plus SSE framer state machines for streamed turns. No async runtime, no
HTTP client/server types in the API surface, no wall clock, no randomness —
id minting and timestamps are supplied by the caller.

The mapping is the translation core of an LLM gateway (a LiteLLM-proxy-class
service): parse whatever dialect your client speaks into one `ItemRequest`,
translate to the `ChatRequest` your engine consumes, render the engine's
canonical chunks back to the caller's dialect.

## What it gives an embedder

Put an Anthropic `/v1/messages` (or OpenAI Responses) surface in front of any
chat/completions-class backend without rewriting the translation yourself:

```rust
use llm_dialect::{
    canonical::ChatRequest,
    dialect::{anthropic::{req, stream::{chunk_to_sse_events, finalize_stream, StreamState}}, deflate},
    items::ItemRequest,
};

// 1. parse an Anthropic Messages wire body into the canonical model
let items: ItemRequest = req::from_anthropic(&body)?;

// 2. flatten to the internal chat request your engine speaks
let req: ChatRequest = deflate::items_to_chat_request(&items)?;

// 3. per-chunk: canonical chunks back to Anthropic SSE frames
let mut st = StreamState::with_stop_sequences(stops);
for chunk in engine_chunks {
    for (event, data) in chunk_to_sse_events(&chunk, &model, &mut st, &msg_id) {
        write!(out, "event: {event}\ndata: {data}\n\n")?;
    }
}
// one guaranteed terminal pair (message_delta + message_stop)
for (event, data) in finalize_stream(&mut st) {
    write!(out, "event: {event}\ndata: {data}\n\n")?;
}
```

Enable the `axum` feature for the shared SSE pump and per-dialect
`*_stream_response` shells (`anthropic_stream_response(&stream, …) -> Response`),
if you're already an axum service and want drop-in handlers.

## Crate shape

| module | contents |
|---|---|
| `items` | the canonical request model (`ItemRequest`, `Item`, `ContentItem`, …) |
| `canonical` | the flattened `ChatRequest`/`ChatResponse`/`CanonChunk` the engine speaks |
| `dialect/{anthropic,openai_chat,openai_responses}` | wire parsers (`*/req.rs`), response renderers (`*/out.rs`), SSE framers (`*/stream.rs`) |
| `dialect::deflate` | `ItemRequest``ChatRequest` |
| `dialect::sse` *(feature `axum`)* | the pump that drives a framer over a stream |
| `error` | the shared `ProxyError` and its HTTP error envelope |

## Guarantees the crate holds

The dialect translation files are pure data transformation. A lint in the
integration tests enforces no `tokio`/`reqwest`/`axum`/`sqlx`/`rand`/
clock calls outside the sanctioned shell/factory regions, so the boundary
above is enforced rather than aspirational.

## License

MIT.