Building a Letta Code custom provider: static first, live second

Model: Kimi K3 · ✓ Verified on letta-code 0.34.26, wallaby-letta-provider 0.1.2 · 2026-10-28

Fresh sessions in Letta Code failed on their first message with Unknown model "wallaby/kimi-k3". Opening /model once fixed it. That symptom sent us down the wrong path for two releases, and the real cause had been public in the issue tracker for ten days.

This is the build log behind wallaby-letta-provider 0.1.2, a small npm package that puts our gateway inside Letta Code's /connect flow. It is also the checklist we would hand to anyone shipping a provider mod: three lessons, one wrong turn, and a live catalog that now updates without a release.

What the package gives a Letta Code user today:

  • kimi-k3 with a 1M context window, image input, and effort levels from off to max
  • A live catalog: a new model id appears in /model once it is listed and priced, without another npm release
  • Costs read from the same public price file the gateway bills against

Why a mod instead of a config file

Letta Code's local backend wants provider metadata: a display name, an API style, a base URL, an auth variable, a model list with context limits and prices. A provider mod hands that metadata to the runtime at registration time, so the provider shows up in /connect and /model like a built-in one.

Letta's documented recipe is small. You call letta.providers.register("wallaby", {...}) with api: "openai-completions", a base URL, an env var name, and a list of models. Model ids must not contain a slash, which matters later. Provider mods are local-backend only; they do not add providers for agents managed through the Letta API.

Our first release did the obvious thing and also implemented listModels, so the catalog would track whatever the gateway serves. That decision produced the bug.

The bug we misdiagnosed

Version 0.1.0 shipped with listModels. A fresh session then failed on its first message with Unknown model "wallaby/kimi-k3", until you opened /model once and picked the model. After that, the session worked.

We read the symptom as a catalog timing problem: the dynamic catalog is empty at session start, so the first lookup misses. Version 0.1.1 removed listModels and shipped a static catalog. The symptom disappeared.

That was a workaround built on a wrong diagnosis. We filed issue #5147, and the project's integration bot answered the next day: dynamic catalogs do start empty in each session, but resolving a turn already refreshes the provider catalog and retries when the model is missing. The error we saw came from a different bug, where a saved model handle for a mod provider could pick up an extra provider/ prefix, producing handles like wallaby/wallaby/kimi-k3. Reselecting the model rewrote the handle, which is exactly why the /model re-select worked. The prefix bug was fixed in PR #5079, which closes the public root-cause report #4995, and shipped in v0.34.17.

So we retested v0.34.17 with listModels turned back on, using two brand-new agents and one headless prompt each. Both answered on the first turn, no /model visit needed.

One edge case survives. Resuming an agent whose model handle was saved before v0.34.17 still fails, because the lookup runs against the openai provider even though the saved handle is correct; re-selecting the model clears it. We asked whether a migration is planned and have no answer yet. A fix does not always reach saved state.

The diagnosis was theirs, the fix was theirs, and our static catalog had been hiding a bug that was already gone.

The published 0.1.2 tarball's header comment still records the original catalog-timing diagnosis. The repository note is corrected; the next release will carry it.

Two lessons came out of that. Search the issue tracker before shipping a workaround: #4995 had been public since October 8 with the full root cause, ten days before our 0.1.0. And keep the workaround after the fix, because a provider that fails closed is worse than one that carries a static fallback.

What 0.1.2 does instead

The current release keeps both paths and orders them:

  • The static catalog registers kimi-k3 immediately, so the first turn of a fresh session always resolves.
  • listModels then merges whatever the gateway's /models endpoint returns. Ids containing a slash are dropped, because Letta model ids cannot contain one.
  • A model without a matching entry in the public pricing.json is skipped rather than shown at $0. A wrong price is worse than a missing row.
  • If /models returns 401 or 403, the mod raises Wallaby rejected this API key (HTTP 401) when the model list refreshes. A headless run that never refreshes the list can still fall back to the host's default model instead of surfacing it.

Three metadata fields that decide the experience

Most of a provider's quality is metadata, not the HTTP call. Three fields mattered most.

Thinking levels. The model schema accepts a thinkingLevelMap that maps each level to a provider-specific value; it comes from pi-ai's Model type rather than the provider recipe docs. We map off to none, minimal to minimal, low to low, medium to medium, high to high, and max to max, with xhigh folded into high. We verified on the endpoint that none, minimal, and max are all accepted and that reasoning_tokens responds.

Image input. A model declares input: ["text", "image"]. We checked that the gateway accepts an image_url data URL before declaring it; a 1x1 PNG came back with the pixel described correctly. Declaring image support without that check is how you get a 400 on the first paste.

Cost. The model entry carries per-million input, output, and cached-input prices. We read them from the public price file and keep the static values in sync with it. If the number in the picker disagrees with the bill, users stop trusting the picker.

Living with a two-day release cadence

letta-code shipped six tagged releases in ten npm publishes between October 18 and October 28, from v0.34.17 to v0.34.26. That cadence is the real maintenance cost of a mod, more than the code itself.

Our routine is small: run the unit tests, run one headless turn against a fresh agent on the latest release, and read the provider API surface when a release lands. The mod is one TypeScript file with no dependencies, so a break surfaces in minutes. The npm package had about 490 downloads in its first nine days, which is enough to make a silent break embarrassing.

If you write a mod, pin your repro to a version, keep a static path that cannot fail, and assume the API will grow. Ours did: listModels and connect fields arrived as documented surface, and the model schema already carried a thinking-level map.

Try it

letta install npm:wallaby-letta-provider

Then /reload, /connect wallaby, /model, and pick wallaby/kimi-k3. Kimi K3 runs $2.70 per million input tokens, $13.50 output, and $0.27 for cached input, 10% below the official list; new accounts start with $0.50 of trial credit.

What comes next

The next model we add needs no code change: an entry in /models and a price in pricing.json, and the provider picks it up. That is why the live path exists, and why the static path stays.

Disclosure: Wallaby Token sells API access to Kimi K3, and wallaby-letta-provider is our package. The Letta issue and the release notes cited here are public.

If you want the install steps rather than the build log, the Letta Code walkthrough covers them in four commands. Running a different terminal agent? The pi setup is the same pattern with a different host. To try the endpoint behind the mod, wallabytoken.com gives new accounts $0.50 in trial credit, no card required.

Like the Dispatch? Add Wallaby as a preferred source on Google, or subscribe via RSS.
Did this help?