Heimdallm SDKs
The Heimdallm SDKs are first-party clients for every operation of the public API: chat completions, text completions, Messages and Responses, System One, embeddings, rerank, images, video and mesh jobs, speech and transcription, files, batches, the catalogue, the account, deposits, management keys and the routes a provider's own key reaches. Both are generated from the same OpenAPI document the gateway is tested against, and each fails its own build on a documented operation it has no method for.
An existing OpenAI, Anthropic or OpenRouter integration keeps working unchanged (see SDKs & frameworks); the Heimdallm SDKs add what those clients have no surface for, such as video and mesh jobs, rerank, the catalogue, generation costs and the provider routes.
| Language | Install | Clients |
|---|---|---|
| TypeScript | npm install @heimdallm/sdk | Heimdallm |
| Python | pip install heimdallm-sdk | Heimdallm, AsyncHeimdallm |
Both take the API base URL and a Heimdallm API key (sk-hd-v1-…), and read
HEIMDALL_BASE_URL and HEIMDALL_API_KEY when none is given. Catalogue reads need no key. The base URL
is refused unless it is HTTPS (plain HTTP is allowed on loopback only), and a redirect is never
followed, since either would carry the key somewhere else.
A first request
import { Heimdallm } from '@heimdallm/sdk';
const client = new Heimdallm({
baseUrl: 'https://api-staging.impossiblecarrot.cc/api/v1',
apiKey: process.env.HEIMDALL_API_KEY,
});
const completion = await client.chat.completions.create({
model: 'deepseek/deepseek-v4-flash-0731-abliterated',
messages: [{ role: 'user', content: 'Name the capital of Iceland.' }],
});
console.log(completion.choices[0]?.message.content, completion.usage?.cost);
import os
from heimdallm_sdk import Heimdallm
client = Heimdallm(
base_url="https://api-staging.impossiblecarrot.cc/api/v1",
api_key=os.environ["HEIMDALL_API_KEY"],
)
completion = client.chat.completions.create(
model="deepseek/deepseek-v4-flash-0731-abliterated",
messages=[{"role": "user", "content": "Name the capital of Iceland."}],
)
print(completion["choices"][0]["message"]["content"], completion["usage"]["cost"])
Python requests are keyword arguments and replies are plain dictionaries, both typed by the
TypedDicts in heimdallm_sdk.types. A member a newer API adds to a reply arrives in the dictionary
rather than failing validation. AsyncHeimdallm is the same client for asyncio.
What each client holds
| Area | TypeScript | Python |
|---|---|---|
| Text | chat.completions, completions, messages, responses, systemone | the same names |
| Vectors | embeddings, rerank | the same names |
| Media | images, videos, meshes, audio.speech, audio.transcriptions | the same names |
| Catalogue | models, families, providers, endpoints, benchmarks, catalogue, marketplace | the same names |
| Account | account, generations, files, batches, deposits, auth | the same names |
| Management | keys, with a management key | keys |
| Provider | provider.status, pullNow, probeNow, earnings, statements | provider.pull_now, probe_now, … |
Method names match across the two SDKs, camel case in TypeScript and snake case in Python
(messages.countTokens, messages.count_tokens). Each SDK exports OPERATIONS, which maps every
METHOD path of the API document to the method that calls it.
Streaming
In TypeScript, stream: true returns an async iterable. In Python, each streaming resource has a
stream(...) method beside create(...).
const stream = await client.chat.completions.create({
model: 'deepseek/deepseek-v4-flash-0731-abliterated',
messages: [{ role: 'user', content: 'Count to five.' }],
stream: true,
});
for await (const event of stream) {
if (event.object === 'chat.completion.chunk') process.stdout.write(event.choices[0]?.delta.content ?? '');
}
for event in client.chat.completions.stream(
model="deepseek/deepseek-v4-flash-0731-abliterated",
messages=[{"role": "user", "content": "Count to five."}],
):
if event["object"] == "chat.completion.chunk" and event["choices"]:
print(event["choices"][0]["delta"].get("content") or "", end="")
Each dialect is read on its own terms: chat and text completion chunks end at [DONE], Messages yields
its named events through message_stop, Responses its response.* events, and images their
image_generation.* events. A request that fails after its 200 has been sent ends the stream with an
error, which both SDKs raise as StreamError carrying the event and the generation id. Leaving the loop
early closes the connection, and the generation is settled for what was produced up to there. With
stream_options.include_tool_dispatch, a chat stream also yields a tool_call_dispatch event for each
completed tool call; see the streaming guide.
Generation ids and cost
A chat or text completion reply carries its cost in usage.cost. Messages and Responses replies have no
member for it, so the charge is read from the generation record by the id the gateway assigns before
anything can fail:
const { data: message, generationId } = await client.messages
.create({ model: 'deepseek/deepseek-v4-flash-0731-abliterated', max_tokens: 256, messages })
.withResponse();
const generation = await client.generations.retrieve(generationId!);
with client.capture() as captured:
message = client.messages.create(
model="deepseek/deepseek-v4-flash-0731-abliterated", max_tokens=256, messages=messages
)
generation = client.generations.retrieve(captured.generation_id)
withResponse() (TypeScript) and capture() (Python) also expose the serving provider
(X-Provider-Name) and the raw response.
Video and mesh jobs
Video and mesh generation run as jobs. create submits once and
is never resent, wait reads the job until it settles, and content returns the finished asset as an
unread response, so a large file can be streamed to disk.
const job = await client.videos.create({ model: 'author/video-model', prompt: 'A lighthouse in fog' });
const done = await client.videos.wait(job.id, { onProgress: (state) => console.log(state.status) });
const mp4 = await client.videos.content(done.id);
await writeFile('lighthouse.mp4', Buffer.from(await mp4.arrayBuffer()));
wait raises JobError when the job fails or its deadline (two hours by default) passes. A deadline
leaves the job running on the server, where retrieve reads it again by id.
Files and audio
files.create stores a document for 48 hours and returns the id a chat, Responses or Messages request
references (see Documents & files). Transcription takes the audio as a file and sends
it as multipart. Python takes bytes, an open binary file, or (filename, content, content_type);
TypeScript takes a Blob.
Errors and retries
A non-2xx answer raises an APIError subclass named for its status, such as AuthenticationError,
InsufficientCreditsError, NotFoundError or RateLimitError. Each carries the HTTP status, the
envelope's message and error type, and the generation id when one was assigned (see
Errors & rate limits). A request that never received an answer raises
APIConnectionError, or APITimeoutError when the client's own deadline passed.
Retries never spend credit twice. A GET is retried on a lost connection, a timeout, and on 408, 429 and
5xx answers. Every other method is retried only on 429, which the gateway answers before admitting any
work: a POST that failed in transit or with a 5xx may already have been billed, so it is reported rather
than sent again. A Retry-After longer than a minute is returned to the caller instead of waited out.
Two retries is the default, and maxRetries / max_retries changes it per client or per request.
Keys and sign-in
keys manages API keys with a management key, and a created key's secret
appears in that reply only. A program that signs a user in rather than holding a key uses the PKCE
exchange: createPkcePair() / create_pkce_pair() makes the verifier and challenge, authorizeUrl() /
authorize_url() builds the approval page address, and auth.exchangeCode / auth.exchange_code trades
the returned code for a key. See Authentication.