Skip to content

Gateway

SDK

There is no Lobstack client to install. The Gateway speaks the OpenAI Chat Completions protocol, so the OpenAI SDK is the client — change the base URL and the key.

You do not need a Lobstack packageThe Gateway speaks the OpenAI Chat Completions protocol, so the official OpenAI SDKs are the supported client in every language — change two values and your existing code works. Everything below does that.

There are three packages on npm and none of them is required. The scope is @lobstack-ai and not @lobstack; only the CLI is unscoped. Copy the name from here rather than guessing it.@lobstack-ai/mcp 0.1.0 — The Lobstack Gateway as an MCP server over stdio: route a prompt, list models, send a call, read what it cost — inside Claude Desktop, Claude Code, Cursor or Zed. Docs.@lobstack-ai/gateway 0.1.0 — A typed TypeScript client for the Lobstack Gateway, shipping the OpenAPI description and the JSON Schema for the receipt returned on every response. Docs.lobstack 0.1.1 — The Lobstack CLI: the Gateway from a terminal, with a local OpenAI-compatible proxy and the cost of each call shown as it happens. Zero dependencies. Docs.

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 and /models to it, both 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);
// The model that actually served it, which may differ from what you asked for.
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 ?? "");
}
On a stream, cost is in the bodyThe cost headers are absent on a streamed response, because the headers flush before the provider has counted a token. The price is on the final chunk instead, under x_lobstack beside the usual usage object — see Metering. The chunk is sent unconditionally; you do not need stream_options.include_usage, which the Gateway does not read.
Do not price the tokens yourselfThis page used to tell you to multiply the counts by the registry price. That advice cost us three months: our own desktop client followed it, priced our tokens against its bundled copy of our rate card, and rendered $0.00 for every request the router sent to a model its copy had never heard of — next to a correct invoice. Read x_lobstack.cost_usd off the frame. It is null, never 0, when we could not price the call.

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.

lobstack.tstypescript
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, 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 and ignores the rest. The SDK will happily send the others and the request will succeed without them, which is the failure mode worth knowing about: nothing errors, the parameter just has no effect.

Sent by the SDKWhat happens
model, messages, stream, max_tokens, temperature, toolsForwarded, subject to the per-model rules on Chat Completions.
top_p, tool_choice, n, stop, response_format, seed, logprobsDropped silently. Not forwarded, no header reports it.
stream_options.include_usageIgnored. The final chunk carries usage and x_lobstack cost regardless.
Assistants, embeddings, images, audio, batches, filesNot implemented. The Gateway serves chat completions and a model listing.

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.

Lobstack

One key over every frontier model, a receipt on every call, and an agent that waits before it changes anything.

© 2026 LobstackAll rights reserved  Status