caret/v4 · reference implementation
The public reference backend.
Two complete caret/v4 backends and two conformance
checkers live in this repository under
reference/:
one in Go, one in Python. Both are dependency-free, both run in one
command, and each language's checker is run against the other
language's server, so their agreement about the protocol is
demonstrated rather than asserted.
Clone the repository, pick Go or Python, run it, and wire its
lanes to the recognizer, model and image tool you already use.
That is a backend. Go is the recommended default: the one to read
first and the one to deploy. Python is the supported alternative;
it implements the same protocol, passes the same checks, and
composes the same caret-cleanup/1 digest. Neither
imports the other, and neither imports anything that is not
shipped with its language. Writing your own backend against
the protocol page is for runtimes these
two cannot run on. If you would rather hand the whole job to a
coding agent, give it the agent instructions.
Run it
# the recommended path
git clone https://github.com/kballenegger/caret-docs
cd caret-docs/reference/go
go run ./cmd/caret-v4-backend -addr 127.0.0.1:8080 -keys dev-key
# in another terminal
go run ./cmd/caret-v4-conform -url http://127.0.0.1:8080 -key dev-key -insecure
That is a conforming backend. With no lane flags it serves
/dictate with built-in loopback providers: no model, no
network, no API keys of anyone's. Enough to point a client at, to run
the checker against, and to develop against on a plane. The Python
alternative is the same two commands:
cd caret-docs/reference/python
python3 -m caret_v4 serve --addr 127.0.0.1:8080 --keys dev-key
python3 -m caret_v4 conform --url http://127.0.0.1:8080 --key dev-key --insecure
Python 3.11 or newer, no virtualenv and no pip install,
because there is nothing to install. Full setup, the lane grammar for
wiring real recognizers and agents, TLS, and an honest list of
limitations are in
reference/README.md.
Wiring real providers
A lane is a string, and the same grammar works for every provider in
both languages: loopback for the built-in deterministic
provider, command:<argv> to run a program,
an https:// URL to POST JSON, or none to
turn the lane off.
go run ./cmd/caret-v4-backend \
-keys dev-key \
-stt 'command:whisper-cli --model base.en --file {audio} --output-txt -' \
-agent 'command:my-agent --prompt-stdin' \
-cleanup 'command:my-agent --prompt-stdin'
Capabilities follow the wiring, never a config flag: a route with no
provider is advertised false on /health and
answers not_supported. A backend with no speech lane
reports status: not_ready with a no_stt
blocker, because /dictate is mandatory and pretending
otherwise is the failure mode the protocol exists to prevent.
A command: speech lane receives a WAV file ({audio}
in the argv, or CARET_AUDIO_PATH), the client's vocabulary in
CARET_VOCABULARY and the language hint in
CARET_LANGUAGE_HINT; agent, image and cleanup commands read
the prompt on stdin and write the answer on stdout. An
https:// lane POSTs one JSON object and reads one back:
| Lane | Request body | Response body |
|---|---|---|
| speech | codec, sample_rate_hz, channels, audio_base64, vocabulary, language_hint | {"text": "…"} |
| agent | prompt, visible_text, vocabulary | {"text": "…"} |
| image | prompt, aspect_ratio, quality | {"mime_type": "image/png", "data_base64": "…"} |
| cleanup | system, transcript, prompt | {"text": "…"} |
A provider that speaks a different shape gets a small local shim
that translates, with the provider's key in the shim's environment
and never in the lane string. The adapter contract belongs to the
reference, not the protocol; the protocol only sees what
/health advertises and what the routes return. A
command: or https:// speech lane is
buffered, so partials.dictate is advertised
false and results say stt_route: "fallback".
Live partials need the streaming half of the speech interface in
lanes.go or lanes.py.
Goals, in order
- Readable before runnable. It is meant to be read: every rule on the protocol page implemented once, in an obvious place, in a codebase small enough to hold in your head.
- Dependency-free. Standard library only in both
languages, WebSocket framing included. No framework, no build
pipeline, no container, no lockfile.
go build ./...works offline on a fresh checkout. - Pluggable lanes, honest capabilities. Speech,
agent, and image providers plug in behind narrow interfaces;
whatever fails to resolve is reported off in
/healthrather than papered over. - A conformance checker, not just a server. The deliverable is as much the harness that can interrogate any V4 backend as it is the backend itself.
Shape
Every rule the protocol states is implemented once, in the file its name suggests, in both languages.
reference/
go/ recommended
protocol.go error table, close codes, audio and vocabulary rules
ws.go RFC 6455, standard library only
server.go /health, routing, credentials, the result cache
session.go the one lifecycle state machine, three finalizers
lanes.go recognizer, agent, image and cleanup providers
cleanup.go caret-cleanup/1, consumed from spec/cleanup/v1/
conformance.go the checker
cmd/caret-v4-backend/ the server binary
cmd/caret-v4-conform/ the checker binary
python/ the supported alternative
caret_v4/ protocol.py ws.py server.py session.py lanes.py
cleanup.py conformance.py __main__.py
tests/ contract tests over a real socket
Design decisions
| Decision | Rationale |
|---|---|
| One lifecycle state machine shared by all three routes | The protocol says the routes differ only in finalizer and result type; the code should prove that by construction. |
| Audio buffer independent of the recognizer | §8 of the protocol makes fallback mandatory; the buffer is the server's half of the reliability contract. |
| Dictate-only is the default build | A conforming backend needs speech and nothing else. Agent and
image lanes activate only when configured, and
/health says so. |
| Vocabulary reaches both the recognizer and cleanup | Advertising "vocabulary": true while dropping the
list is forbidden by the protocol; the lane interfaces carry it
explicitly so no provider can lose it silently. |
| Zero retention by default | In-memory audio, bounded; cached results expire in minutes; no transcript logging. Matching §10 exactly. |
The conformance checker
The checker treats the backend under test as a black box at a base
URL, any language, any host, a production backend or a first attempt
written this afternoon. It exercises what is easy to get subtly
wrong: the shape of ready, a missing and a wrong
credential, audio-total verification and audio_incomplete,
the audio echo on the result, exactly-one-terminal-event ordering
after queued partials, cumulative (never regressing) partial text,
replay with a repeated client_request_id served from
cache, cancel before and after finalize with a normal 1000
close and no terminal event, polish: false returning the
raw transcript, unknown JSON fields ignored, honest capabilities versus
served routes, text input only where text_input says so,
sha256 and byte_length matching the image
bytes actually delivered, buffered STT fallback reporting, and the
error table with its close codes and retryable flags,
including an oversized frame and a binary frame before
ready. Rules it cannot test from outside, such as the
10 second start timeout, the 60 second frame gap and the keep-alive
cadence, are printed as SKIP lines so the report says what was not
checked. Exit 0 means a V4 client will be happy; 1 means something
failed; 2 means the checker could not run. Warnings never fail a run.
go run ./cmd/caret-v4-conform -url https://backend.example.com -key "$CARET_API_KEY"
Point it at your backend, not just at these. It is the harness that makes "conforming" a thing you can check rather than a thing you claim.
What it is not
- Not a hosted service, not a relay — the protocol has no privileged party, and neither does the reference. Point the app at it in Your Agent mode.
- Not a fork target for the cleanup wording: the spec is consumed
as data from
spec/cleanup/v1/, digest-checked.
What the loopback providers are not
Loopback speech counts voiced 20 ms windows and spells the client's
vocabulary followed by filler. Loopback cleanup capitalizes a
sentence and adds a full stop. Loopback imagine draws a gradient
seeded by the prompt. They exist so the lifecycle, vocabulary
routing, silence handling and image hashing are exercisable with no
model and no network. They are not transcription, not an agent, and
not an image model, and they never claim otherwise on
/health. Wire a real provider before this faces a
keyboard, and read the limitations section of
reference/README.md
first: in-memory state only, no rate limiting, no clustering.