Integration guides ยท 2026-08-29
LLMTR provider for the Pi coding agent and its generated catalog
In Pi the LLMTR provider is a fifteen-line registration while the model list is a generated catalog. This guide covers the provider file, catalog generation, environment key resolution and how the reasoning parameter reaches the request body.
The provider registration is fifteen lines
Start with where the code lives: this integration was not merged upstream. The provider file and the generated model catalog live in the knowhycodata/pi repository on the feat/llmtr-provider branch. A reader installing the official package will not find an llmtr provider, so take that branch first and install its dependencies.
Adding a provider in Pi is not a large job. The file written for LLMTR is fifteen lines and returns a single factory: id, display name, base address, authentication, model list and request family. That factory is then added to the list of builtin providers and becomes available to the agent.
The design choice matters here: the file carries no model information at all. Models are imported from a separate module and the factory only turns that module values into a list. The provider contract and the catalog content stay apart.
The provider factory, from the LLMTR registration file
export function llmtrProvider(): Provider<"openai-completions"> {
return createProvider({
id: "llmtr",
name: "LLMTR",
baseUrl: "https://llmtr.com/v1",
auth: { apiKey: envApiKeyAuth("LLMTR API key", ["LLMTR_API_KEY"]) },
models: Object.values(LLMTR_MODELS),
api: openAICompletionsApi(),
});
}
The model catalog is generated, not handwritten
The file holding the model list is build output, and its opening comment says so plainly: it is written by a generation script, must not be edited manually, and is refreshed by running the generation command. The file itself only imports a data file and pushes it through a flattening helper.
That separation makes catalog refreshes routine. When a route is published or retired, you update the source data and run the generation command; the registration file is never touched. If you want to add a model by hand, the right place is the data source the script reads, not the generated module.
The generated catalog module with its warning comment (abridged)
// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
import values from "./data/llmtr.json" with { type: "json" };
import { flattenModelCatalog } from "../model-catalog.ts";
export const LLMTR_MODELS = flattenModelCatalog("llmtr", values);
Environment key resolution
Authentication binds to a helper that reads an environment variable. Tests confirm the behavior in both directions: that the provider id resolves to the variable name, and that the value is read back when the variable is set. Because a browser cannot read process environment, the package documentation notes that the key must be passed explicitly there.
The table below gathers every constant in the integration and the file that declares it. When setup misbehaves, this is also the list of places to check.
| Item | Value | Declared in |
|---|---|---|
| Provider id | llmtr | The factory call in the provider registration file |
| Environment variable | LLMTR_API_KEY | The auth helper in the registration file and the package documentation table |
| Base address | https://llmtr.com/v1 | The base address field of the factory call |
| Request family | openai-completions | The request family field of the factory call |
| Registration into the builtin list | The llmtrProvider call | The module that collects builtin providers |
| Row in the agent documentation | LLMTR, LLMTR_API_KEY, llmtr | The coding agent provider documentation |
Model ids and selection
For every entry in the generated catalog the tests enforce three things: the request family must be chat completions, the base address must be the gateway address and the model id must contain a slash. The third condition matters most because it guarantees the ids keep their canonical provider-and-model spelling. The sample model id in those tests is moonshot/kimi-k3, and that route is live in the public catalog.
So when you pick a model in Pi you use the gateway catalog id verbatim; there is no provider-specific renaming. The provider id is llmtr and the model id is the canonical spelling. The exact command-line model flag is not part of this branch diff, so no flag example is given here; that detail is unverified in this version.
- Provider id: llmtr
- Model id: the canonical gateway spelling, for example moonshot/kimi-k3
- Every generated entry carries the gateway address and the chat completions family
- The key is read from the environment, or passed explicitly in a browser
How the reasoning parameter reaches the body
Compatibility tests capture the request body and pin down three behaviors. First, reasoning effort travels as an object carrying an effort value rather than a flat field. Second, for a model that publishes no effort levels the field is omitted from the body entirely. Third, the body is built as if addressing a gateway, so no server-side storage field is sent.
The reasoning behind the second point is spelled out in a test comment: sending an effort the model does not list makes the gateway answer with an unsupported capability error. So a model whose listing exposes no efforts reaches the wire bare and runs the gateway-side default.
| What the model listing publishes | What reaches the body | Reason |
|---|---|---|
| Effort levels exist and high was requested | A reasoning field carrying an effort value as an object | The nested object shape the gateway expects |
| The same call | No flat effort field is sent | The same information already travels in the object |
| The model publishes no effort levels | No reasoning field at all | An unsupported effort errors, so the field is dropped and the default applies |
| Every case | No server-side storage field | The request addresses a gateway rather than a platform account |
| Every case | A plain message list with role and content fields | Tests confirm the body stays minimal |
The chat completions family and endpoint fit
The registration pins the request family to chat completions. On the gateway side a chat completions request is served when the model has either a chat completions or a responses binding, while a request to the responses endpoint is only valid for models with a responses binding. The bridge runs one way only.
In practice that means checking which operations a model publishes before pinning it. Embedding, image, video and audio routes cannot drive a chat loop at all. The catalog endpoint is readable without a key, so you can run that check before doing any setup.
Frequently asked questions
Can I edit the generated model file by hand?
Its first line forbids exactly that: the file is generation output and will be overwritten on the next run. For a change that survives, update the data source the script reads and run the generation command again.
Can I pass the key directly instead of using an environment variable?
The package documentation scopes environment resolution to server-side execution and notes that in a browser the key must be passed explicitly. If you are running server side, the environment variable path is the safer option.
Why is there no flat reasoning effort field in the body?
This integration carries effort inside an object instead. The tests additionally assert that the flat field is not sent, since sending both would be redundant. When a model lists no effort levels, the field is dropped completely.
How do I keep cost under control?
Billing is token based and spend draws down a credit balance, so watching usage counters is more useful than quoting a per-model figure. The platform margin applies only when credit is topped up and is eight percent; no margin is added to model prices.