Gateway API
SDK
The Gateway speaks the OpenAI Chat Completions protocol, so the OpenAI SDK works as it is — change the base URL and the key. A typed TypeScript client is also available if you want one.
Install and construct
npm install openai
// client.ts
import OpenAI from "openai";
export const lobstack = new OpenAI({
apiKey: process.env.LOBSTACK_API_KEY, // lsk_live_…
baseURL: "https://www.lobstack.ai/api/gateway/v1",
});The base URL is https://www.lobstack.ai/api/gateway/v1. The SDK appends /chat/completions, /embeddings and /models to it, all of which the Gateway serves. Keys look like lsk_live_<8 hex><48 hex> and need the inference scope. Mint one in the Console.
Chat
const completion = await lobstack.chat.completions.create({
model: "auto",
messages: [{ role: "user", content: "Summarise this changelog in one line." }],
max_tokens: 256,
});
console.log(completion.choices[0].message.content);
// "auto" may route; a named model is served exactly or the request fails.
console.log(completion.model);Reading the Lobstack headers
The routing decision and the price are on the response headers, and both SDKs can hand you the raw response alongside the parsed body.
const { data, response } = await lobstack.chat.completions
.create({
model: "claude-opus-5",
messages: [{ role: "user", content: "hey, you there?" }],
})
.withResponse();
console.log(data.choices[0].message.content);
console.log({
served: response.headers.get("x-lobstack-model"),
tier: response.headers.get("x-lobstack-tier"),
complexity: response.headers.get("x-lobstack-complexity"),
routed: response.headers.get("x-lobstack-routed"),
costUsd: response.headers.get("x-lobstack-cost-usd"),
savingsUsd: response.headers.get("x-lobstack-savings-usd"),
// ALWAYS read this next to savingsUsd. "named" means the saving is against
// the model you asked for. "plan_ceiling" means you sent "auto" and it is
// against the priciest model your plan allows — which you did not request.
baselineReason: response.headers.get("x-lobstack-baseline-reason"),
baselineModel: response.headers.get("x-lobstack-baseline-model"),
requestId: response.headers.get("x-lobstack-request-id"),
});x-lobstack-cost-usd and x-lobstack-savings-usd are empty strings rather than zeros when there is no honest number — an unpriced model, or no baseline to measure against. Parse with that in mind.
The three baseline headers are sent only when a baseline exists, and they travel together. Read x-lobstack-baseline-reason before you render a saving: a plan_ceiling figure is measured against the most expensive model the plan allows, which the caller never asked for, and showing it as though they had is misreporting it. The full list is on Metering & cost.
Streaming
const stream = await lobstack.chat.completions.create({
model: "auto",
stream: true,
messages: [{ role: "user", content: "List three causes of a rollback." }],
});
for await (const chunk of stream) {
// The last data chunk has an empty choices array and carries usage.
if (chunk.usage) {
console.log("\ntokens:", chunk.usage.total_tokens);
continue;
}
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}A thin wrapper, if you want one
The OpenAI SDK hides the headers behind withResponse(). If you would rather have the routing and cost facts on the return value, this is the whole of it. Copy it into your codebase; there is nothing to install.
export interface LobstackReceipt {
requestId: string | null;
model: string | null;
tier: string | null;
complexity: number | null;
routed: boolean;
costUsd: number | null; // null when the model is unpriced
savingsUsd: number | null; // null when there is no baseline at all
// What savingsUsd was measured against, and why. Never render the saving
// without the reason: "plan_ceiling" is a comparison against a model the
// caller did not choose.
baselineModel: string | null;
baselineReason: "named" | "plan_ceiling" | null;
droppedParams: string | null;
}
export interface LobstackResult<T> {
data: T;
receipt: LobstackReceipt;
}
const BASE = "https://www.lobstack.ai/api/gateway/v1";
/** Headers carry empty strings where the honest answer is "no number". */
function num(h: Headers, k: string): number | null {
const v = h.get(k);
return v === null || v === "" ? null : Number(v);
}
export async function chat<T = unknown>(
apiKey: string,
body: Record<string, unknown>,
): Promise<LobstackResult<T>> {
const res = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) {
const e = json?.error ?? {};
throw Object.assign(new Error(e.message ?? `gateway error ${res.status}`), {
status: res.status,
class: e.type ?? null,
requestId: e.request_id ?? res.headers.get("x-lobstack-request-id"),
});
}
const h = res.headers;
return {
data: json as T,
receipt: {
requestId: h.get("x-lobstack-request-id"),
model: h.get("x-lobstack-model"),
tier: h.get("x-lobstack-tier"),
complexity: num(h, "x-lobstack-complexity"),
routed: h.get("x-lobstack-routed") === "true",
costUsd: num(h, "x-lobstack-cost-usd"),
savingsUsd: num(h, "x-lobstack-savings-usd"),
baselineModel: h.get("x-lobstack-baseline-model"),
baselineReason: h.get("x-lobstack-baseline-reason") as "named" | "plan_ceiling" | null,
droppedParams: h.get("x-lobstack-dropped-params"),
},
};
}It does not handle streaming: a streamed response carries no cost headers, and the price arrives in the body of the final chunk instead, which the OpenAI SDK already parses. Use the SDK for streams and read x_lobstack off that chunk.
Listing models
const models = await lobstack.models.list();
for (const m of models.data) {
console.log(m.id, m.owned_by);
}Each entry carries the OpenAI fields plus label, capabilities, tier, context_window, price_per_mtok and managed. The SDK will not surface those extras on its typed model object, so read them from the raw JSON when you need them. created is not returned. The full catalog is on Models & providers.
What the OpenAI SDK cannot do here
The Gateway reads a defined set of body fields. Most of what the SDK sends is forwarded where the provider accepts it, and a parameter it drops is named in x-lobstack-dropped-params. A few fields are ignored with no header, which is the failure mode worth knowing about: nothing errors, the parameter just has no effect.
The complete endpoint, parameter and runtime matrix lives in the OpenAI compatibility contract.
| Sent by the SDK | What happens |
|---|---|
| model, messages, stream, max_tokens, temperature, tools | Forwarded, subject to the per-model rules on Chat Completions. |
| user, response_format, tool_choice, parallel_tool_calls, top_p, stop, seed, presence_penalty, frequency_penalty | Forwarded where the provider accepts them. Any that are not are named in x-lobstack-dropped-params. The matrix is on Chat Completions. |
| metadata | Not forwarded. metadata.client is read as the client tag. |
| n, logprobs | Ignored. Not forwarded, no header reports it. |
| stream_options.include_usage | Ignored. The final chunk carries usage and x_lobstack cost regardless. |
| embeddings.create | Served by POST /embeddings, OpenAI models only. See Embeddings. |
| Assistants, images, audio, batches, files | Not implemented. |
Client-side retries in the SDK are on by default. Read the note about duplicate billing on Errors & retries before you leave them there for expensive calls: the Gateway accepts no idempotency key, so a retried request that already completed upstream is generated and metered twice.
Other clients
Anything that speaks OpenAI
The same substitution works for any tool that lets you set an OpenAI base URL and key. Nothing about the request shape changes, so a framework that wraps the OpenAI SDK inherits the Gateway without knowing about it. What it will not inherit is the headers: a wrapper that returns only the parsed body drops the receipt, and the usage API is then the way to get the cost back.