# Lobstack — full reference > The agentic stack: one OpenAI-compatible API over 9 model providers, a > console for what that API is doing, and a local-first desktop agent. This file is generated from the Lobstack source tree at build time. Every table, price, threshold, plan figure and skill below is read out of the module that implements it, so nothing here can describe a version of the product that no longer exists. The short index is one file over: https://www.lobstack.ai/llms.txt Where something is not finished, this file says so. A model that repeats a capability Lobstack does not have costs its reader more than one that says nothing. ## Three things to get right ### 1. The base URL has the www, and that is not cosmetic https://www.lobstack.ai/api/gateway/v1 The apex `lobstack.ai` answers with a 307 to `www.lobstack.ai`. RFC 9110 says a client MUST drop the `Authorization` header when a redirect crosses to a different host, and every client honours it — curl, requests, httpx (so the OpenAI SDK), Go, Java, PowerShell. A base URL on the apex therefore answers a caller holding a perfectly good key with a 401 reading "missing credentials", and the obvious conclusion to draw is that the Gateway is broken. Use the host that answers without a redirect. ### 2. An unpriced call reports null, never $0.00 `cost_usd: null` means the cost of that call is not known — an unpriced model, or a failure before the provider returned usage. It does not mean the call was free. Rendering it as `$0.00` turns an absence of information into a claim about money, and the claim is always wrong in the customer's favour until the invoice disagrees. The header is empty rather than zero for the same reason. ### 3. A saving is only a saving when the caller named a model Every response may carry a baseline — what a different model would have cost — and the baseline never travels without a `baseline_reason`: - `named` The caller asked for a specific model and the router served something else. The difference is a measurement against their own request, and it is a saving. - `plan_ceiling` The caller sent `auto`. The comparison is against the most expensive model their plan allows, which nobody asked for. It is a reference point, not a saving. Both are real subtractions between real prices and they are not the same claim, which is why the reason is a field rather than an omission. Where neither applies, the saving is null — not zero. What a request cost the customer is on the receipt. What it cost Lobstack is recorded server-side and is never returned. ## Packages No Lobstack package is required to use the Gateway: it speaks the OpenAI Chat Completions protocol, so the official OpenAI SDK in any language is the client — change the base URL and the key. The packages below are conveniences. Registry state last verified against npm on 2026-09-15. - @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. Install: npx -y @lobstack-ai/mcp Docs: https://www.lobstack.ai/docs/mcp Licence: MIT - @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. Install: npm install @lobstack-ai/gateway Docs: https://www.lobstack.ai/docs/gateway/sdk Licence: MIT - 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. Install: npx -y lobstack Docs: https://www.lobstack.ai/docs/cli Licence: MIT ## Gateway One OpenAI-compatible endpoint over 9 model providers and 26 current model keys. Point an existing OpenAI SDK at it with a Lobstack key and it works unchanged, streaming and tool calls included. POST https://www.lobstack.ai/api/gateway/v1/chat/completions GET https://www.lobstack.ai/api/gateway/v1/models POST https://www.lobstack.ai/api/gateway/v1/route-preview Authorization: Bearer lsk_live_… (an org-scoped Lobstack API key) ### Response headers - x-lobstack-request-id The trace row id. Allocated before anything can fail, so a 401 carries one too. - x-lobstack-model What actually served the request. Not necessarily what you sent. - x-lobstack-tier The tier the complexity score landed in. - x-lobstack-complexity 0–100, the heuristic score for the last user message. - x-lobstack-routed true whenever you sent "auto", and whenever the served model differs from the one you named. - x-lobstack-mode managed | byok — whose provider key paid for the tokens. - x-lobstack-principal api_key | gateway_token | agent_secret. - x-lobstack-cost-usd What this call cost the customer. Empty when unpriced. - x-lobstack-baseline-usd What the baseline model would have cost, when there is one. - x-lobstack-baseline-model Which model that baseline is. - x-lobstack-baseline-reason named | plan_ceiling — see rule 3 above. - x-lobstack-savings-usd baseline minus actual. Absent, never zero, when there is no baseline. - x-lobstack-savings-pct The same subtraction as a percentage. - x-lobstack-quota-meter spend | requests | legacy — the meter that applied. - x-lobstack-dropped-params Present only when a sampling parameter you sent was not forwarded. Header names verified against src/app/api/gateway and src/lib/gateway. ### Failure taxonomy Six classes, recorded on every request and reported in the Console: - auth Credential rejected — the caller must fix its key. - quota Allowance exhausted — upgrade or wait for the reset. - validation Malformed request — the caller must fix the call. - provider The upstream model provider failed. Not the caller's fault. - timeout We gave up waiting. Retryable. - internal Our bug. Full reference: https://www.lobstack.ai/docs/gateway/errors ## Token Intelligence (routing) Each request is scored 0–100 for complexity from the last user message, and served by the cheapest tier whose ceiling covers that score — bounded by the highest tier the caller's plan allows. Five tiers. The first model listed in a tier is what an `auto` request lands on. | Tier | Max complexity | Lead model | Cost multiplier | Models in tier | | --- | --- | --- | --- | --- | | nano | 20 | gemini-3.1-flash-lite | 0.10× | 6 | | small | 40 | claude-haiku-4-5 | 0.30× | 9 | | standard | 70 | claude-sonnet-5 | 0.67× | 12 | | premium | 90 | kimi-k3 | 2.00× | 11 | | flagship | 100 | claude-opus-5 | 5.00× | 7 | Routing does not always find something cheaper, and no saving of any particular size is promised. Full reference: https://www.lobstack.ai/docs/gateway/token-intelligence ## Model catalogue 26 current model keys across 9 providers. Prices are USD per million tokens, in/out, last checked against each provider's own pricing page on 2026-09-08. A price marked `unverified` could not be confirmed against a first-party page on that date and is published as such rather than presented as sourced. | Key | Provider | Tier | In $/M | Out $/M | Context | Managed | | --- | --- | --- | --- | --- | --- | --- | | claude-fable-5-1 | anthropic | flagship | 10 | 50 | 1,000,000 | yes | | gpt-6-astra | openai | flagship | 10 | 50 | 1,050,000 | yes | | claude-opus-5 | anthropic | flagship | 5 | 25 | 1,000,000 | yes | | gpt-5.6 | openai | flagship | 4 | 20 | 1,050,000 | yes | | deepseek-v4-pro | deepseek | flagship | 1.1 (unverified) | 4.4 (unverified) | 1,000,000 | yes | | kimi-k3 | moonshot | premium | 3 | 15 | 1,048,576 | yes | | gemini-3.1-pro | google | premium | 2 | 12 | 1,048,576 | yes | | gpt-5.6-terra | openai | premium | 2 | 12 | 1,050,000 | yes | | grok-4.6 | xai | premium | 2 | 6 | 500,000 | yes | | grok-4.5 | xai | premium | 2 | 6 | 500,000 | yes | | qwen3.8-max | alibaba | premium | 2 | 6 | 1,000,000 | yes | | mistral-medium-3.5 | mistral | premium | 1.5 | 7.5 | 256,000 | yes | | claude-sonnet-5 | anthropic | standard | 2 | 10 | 1,000,000 | yes | | grok-4.3 | xai | standard | 1.25 | 2.5 | 1,000,000 | yes | | gemini-3.8-flash | google | standard | 0.75 | 3.75 | 1,048,576 | yes | | qwen3.7-plus | alibaba | standard | 0.4 | 1.6 | 1,000,000 | yes | | deepseek-v4-flash | deepseek | standard | 0.27 (unverified) | 1.1 (unverified) | 1,000,000 | yes | | claude-haiku-4-5 | anthropic | small | 1 | 5 | 200,000 | yes | | grok-build-0.1 | xai | small | 1 | 2 | 256,000 | yes | | gpt-oss-120b | groq | small | 0.15 | 0.6 | 131,072 | yes | | mistral-small-4 | mistral | small | 0.15 | 0.6 | 256,000 | yes | | qwen3.8-flash | alibaba | small | 0.15 | 0.47 | 1,000,000 | yes | | gemini-3.1-flash-lite | google | nano | 0.25 | 1.5 | 1,048,576 | yes | | gpt-5.6-luna | openai | nano | 0.2 | 1.2 | 1,050,000 | yes | | ministral-8b | mistral | nano | 0.15 | 0.15 | 256,000 | yes | | gpt-oss-20b | groq | nano | 0.075 | 0.3 | 131,072 | yes | Provider API bases the Gateway calls: - anthropic: https://api.anthropic.com - openai: https://api.openai.com/v1 - google: https://generativelanguage.googleapis.com/v1beta/openai - xai: https://api.x.ai/v1 - deepseek: https://api.deepseek.com/v1 - mistral: https://api.mistral.ai/v1 - groq: https://api.groq.com/openai/v1 - alibaba: https://dashscope-intl.aliyuncs.com/compatible-mode/v1 - moonshot: https://api.moonshot.cn/v1 Live catalogue: https://www.lobstack.ai/docs/gateway/models ## Plans and metering A plan includes an amount of model spend in dollars, metered at Lobstack rates, rather than a count of messages — a message is anything from a 40-token ping to a 200k-token context, so it was never a unit of cost. BYOK is metered on requests instead, because the customer pays their own provider for the tokens; what the plan buys there is the routing, the receipt, the Console and the local proxy. | Plan | Monthly | Annual | Meter | Included | Ceiling | Seats | Top-ups | Savings credit | SSO | SLA | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Free | $0/mo | $0/mo | spend | $1 of model spend | standard | 1 | no | no | no | no | | BYOK | $19/mo | $182/mo | requests | 100,000 requests | flagship | 1 | no | no | no | no | | Developer | $29/mo | $278/mo | spend | $10 of model spend | flagship | 1 | yes | yes | no | no | | Studio | $99/mo | $950/mo | spend | $35 of model spend | flagship | 5 | yes | yes | no | no | | Scale | $299/mo | $2870/mo | spend | $120 of model spend | flagship | 25 | yes | yes | yes | yes | Annual billing takes 20% off. Managed model spend is metered at 1.25× the provider's list price. On a plan that credits savings, 50% of a verified routed saving is added back to that period's allowance — verified meaning both models priced, the baseline above the actual cost, our own key, and not an error. Three meters exist and every response names the one that applied in x-lobstack-quota-meter: "spend" for dollars of model spend, "requests" for BYOK, and "legacy" for subscriptions bought on the older messages tiers, which keep the counting they were sold. NOT YET ENFORCED: the dollar allowance is not enforced in production. The table it lives in is created by a migration that is written and not applied, so spend reads back as empty and no request is refused on dollars until it lands. Full reference: https://www.lobstack.ai/docs/pricing and https://www.lobstack.ai/pricing ## Console What the API is doing, and what it cost. Request-level tracing with true p50, p95 and p99 latency; the six-class failure taxonomy above; spend by model and provider priced at the moment the tokens were spent; runtime logs; and org-scoped API keys with scopes, expiry and revocation. Full reference: https://www.lobstack.ai/docs/console ## Lob Bot A local-first desktop application where bots do real work on your own machine. Every action that changes something — writing a file, running a command, opening a page, calling a connector — stops and shows you the exact call before it runs. Secrets are encrypted under a key held in the operating system's credential store, and there is no telemetry. STATUS: in development. There is a working build. It is a Windows prerelease and it is not code-signed, so there is no download on the site. It is not available for macOS or Linux. 13 connectors are built in and 2 generic transports reach any other MCP server by URL or by the command that runs it, so the list is a floor rather than a ceiling. A fresh workspace can reach nothing until you add one. - Files [filesystem, stdio] — Read and write files under one folder you choose. Nothing outside it is reachable. Asks for: root Example tools: read_file, write_file, list_directory, search_files, move_file https://www.lobstack.ai/connectors/filesystem - GitHub [github, stdio] — Issues, pull requests, code search and repository files. Asks for: token (secret), owner (optional) Example tools: search_issues, get_issue, create_issue, list_pull_requests, get_file_contents https://www.lobstack.ai/connectors/github - Postgres [postgres, stdio] — Query a database. Read-only unless you say otherwise. Asks for: connectionString (secret) Example tools: query, list_schemas, describe_table https://www.lobstack.ai/connectors/postgres - Slack [slack, stdio] — Read channels and threads, post messages, search history. Asks for: botToken (secret), teamId Example tools: list_channels, post_message, get_thread_replies, search_messages https://www.lobstack.ai/connectors/slack - Linear [linear, stdio] — Issues, projects and cycles. Asks for: apiKey (secret) Example tools: list_issues, create_issue, update_issue, list_projects https://www.lobstack.ai/connectors/linear - Notion [notion, stdio] — Search, read and update pages and databases. Asks for: apiKey (secret) Example tools: search, get_page, update_page, query_database https://www.lobstack.ai/connectors/notion - Stripe [stripe, stdio] — Customers, subscriptions, invoices and payments. Asks for: apiKey (secret) Example tools: list_customers, get_customer, list_subscriptions, list_invoices https://www.lobstack.ai/connectors/stripe - Supabase [supabase, stdio] — Tables, rows, migrations and logs for one project. Asks for: accessToken (secret), projectRef, writes (optional) Example tools: list_tables, execute_sql, get_logs, list_projects https://www.lobstack.ai/connectors/supabase - Sentry [sentry, stdio] — Issues, events and stack traces from your own projects. Asks for: accessToken (secret), host (optional) Example tools: find_issues, get_issue_details, search_events, find_organizations https://www.lobstack.ai/connectors/sentry - GitLab [gitlab, stdio] — Issues, merge requests, pipelines and repository files. Asks for: token (secret), apiUrl (optional), projects (optional), writes (optional) Example tools: search_repositories, get_merge_request, list_issues, get_file_contents https://www.lobstack.ai/connectors/gitlab - MongoDB [mongodb, stdio] — Query collections, read schemas and explain queries on one cluster. Asks for: connectionString (secret), writes (optional) Example tools: find, aggregate, list-collections, collection-schema https://www.lobstack.ai/connectors/mongodb - Airtable [airtable, stdio] — Bases, tables and records. Asks for: apiKey (secret) Example tools: list_bases, list_tables, list_records, search_records https://www.lobstack.ai/connectors/airtable - Browser [browser, stdio] — A real browser the bot drives, which stays signed in. You log in once yourself, in a window we open for you; the session is saved on this machine and every later run reuses it. Bots never see or type your password. They can only visit the domains you list here. Asks for: allow, profile (optional) Example tools: browser_navigate, browser_read, browser_find, browser_click, browser_type, browser_screenshot, browser_signin_required https://www.lobstack.ai/connectors/browser - Any MCP server (HTTP) [http-mcp, http, generic] — A server you already run, or a hosted one, reached over HTTP. This is how you connect something that is not in the list above. Asks for: url, authorization (secret) (optional) Example tools: https://www.lobstack.ai/connectors/http-mcp - Any MCP server (command) [stdio-mcp, stdio, generic] — Run a server on this machine by its command line. Hundreds are published for services that are not in the list above. Asks for: command, args (optional) Example tools: https://www.lobstack.ai/connectors/stdio-mcp Full reference: https://www.lobstack.ai/docs/lob-bot ## Documentation index The HTML documentation. Each line is the page's own one-line summary, taken from the navigation the site renders. Start here - Introduction: https://www.lobstack.ai/docs What Lobstack is, and which of the three products you need. - Quickstart: https://www.lobstack.ai/docs/quickstart An account, a key, one request to the Gateway, and the receipt it comes back with. - CLI: https://www.lobstack.ai/docs/cli The Gateway from a terminal, and a local proxy for tools that already speak OpenAI. - MCP server: https://www.lobstack.ai/docs/mcp The Gateway as four tools inside Claude Desktop, Claude Code, Cursor or Zed. - Concepts: https://www.lobstack.ai/docs/concepts Keys, routing, tiers, plans, meters, metering — the words the rest of the docs use. Gateway — One endpoint, every model - Overview: https://www.lobstack.ai/docs/gateway What the Gateway does and what it returns. - Chat Completions: https://www.lobstack.ai/docs/gateway/chat The endpoint: request body, response, streaming. - Models & providers: https://www.lobstack.ai/docs/gateway/models The catalog, per-token prices, and how keys resolve. - Token Intelligence: https://www.lobstack.ai/docs/gateway/token-intelligence Complexity scoring, tiers, ceilings, and what routing does not claim. - Routing data: https://www.lobstack.ai/docs/gateway/routing-data What a request leaves behind, what it never can, and the capture that is off unless you turn it on. - Metering & cost: https://www.lobstack.ai/docs/gateway/metering The cost headers, the labelled baseline, the three quota meters, and the usage API. - Errors & retries: https://www.lobstack.ai/docs/gateway/errors The six error classes and what to do about each. - SDK: https://www.lobstack.ai/docs/gateway/sdk The TypeScript client, and using the OpenAI SDK unchanged. Console — Watch it and drive it - Overview: https://www.lobstack.ai/docs/console The surfaces, and which question each one answers. - API monitor: https://www.lobstack.ai/docs/console/api Request traces, latency percentiles, failure taxonomy. - Usage & cost: https://www.lobstack.ai/docs/console/usage Spend by model and provider, priced when it was spent. - Logs: https://www.lobstack.ai/docs/console/logs Every request, newest first, with the trace row behind each line. - API keys: https://www.lobstack.ai/docs/console/api-keys Minting, scopes, rotation, and how keys are stored. - Models: https://www.lobstack.ai/docs/console/models What your key can call, what it costs you, and which model “auto” picks. - Webhooks: https://www.lobstack.ai/docs/console/webhooks Events, payload shape, signature verification. - Settings & billing: https://www.lobstack.ai/docs/console/settings Org profile, members, plan and invoices. Lob Bot - Lob Bot: https://www.lobstack.ai/docs/lob-bot The desktop app: the approval gate, connectors, the browser, and what leaves your machine. Agent Skills — Files that teach an agent - Skills & connectors: https://www.lobstack.ai/docs/agent-skills What an Agent Skill is, why it is not a connector, and how to write one. Platform - Security & compliance: https://www.lobstack.ai/docs/security What is live, what is defined but not deployed, and what is roadmap. - Pricing & plans: https://www.lobstack.ai/docs/pricing Five plans, an allowance in dollars of model spend, and what the ceiling does. - Changelog: https://www.lobstack.ai/docs/changelog What shipped, newest first. - Support: https://www.lobstack.ai/docs/support Where to ask, and what to include. ## Agent Skills 10 skills, 10 of them written and maintained by Lobstack. An Agent Skill is a folder with a SKILL.md that teaches an agent how to do something well: markdown, no credentials, portable to any agent that reads a skills directory, and copying the file is the install. That is a different object from a connector, which is a credentialed integration granting access to a service. The archive carries no install counts, stars or ratings. There are none to report and a number invented to look like traction would make the rest of it worth less. Categories: Cost, Gateway, Migration, Observability, Authoring. Every SKILL.md follows in full, frontmatter included, exactly as the archive serves it. ### Read a Lobstack receipt — https://www.lobstack.ai/skills/lobstack-receipts ```markdown --- name: lobstack-receipts description: "Read the receipt Lobstack returns on every model call — cost_usd, savings_usd, baseline_reason — and report cost from it instead of estimating. Use when a response carries x_lobstack or an x-lobstack-* header, when asked what a call or a run cost, or when a cost renders as $0.00." license: MIT metadata: title: "Read a Lobstack receipt" version: 1.0.0 author: lobstack category: cost tags: [cost, receipts, metering, gateway] --- # Read a Lobstack receipt Every response from the Lobstack Gateway carries what the call cost. Read it. Never multiply token counts by a rate card you are holding in your head — that is how a bundled price table drifts out of date and reports `$0.00` next to a real charge, which is a mistake this product shipped for three months. ## Where the number is There are two paths and they carry the receipt differently. This is not a detail; a client that only handles one of them under-reports half its traffic. **Buffered** (`stream: false`) — HTTP response headers: ``` x-lobstack-request-id: 8f2b1e0c-4d5a-4b91-9c3e-6a7f0d2b5c41 x-lobstack-model: gemini-3.8-flash # what actually served it x-lobstack-tier: standard x-lobstack-complexity: 47 x-lobstack-routed: true x-lobstack-mode: managed # managed | byok | direct x-lobstack-cost-usd: 0.000017 # empty string when unpriced x-lobstack-savings-usd: 0.000352 x-lobstack-priced: true x-lobstack-metered: true x-lobstack-baseline-reason: named x-lobstack-baseline-model: claude-opus-5 x-lobstack-baseline-usd: 0.000369 ``` **Streamed** (`stream: true`) — the final SSE frame, under `x_lobstack`: ```json { "id": "chatcmpl-…", "object": "chat.completion.chunk", "choices": [], "usage": { "prompt_tokens": 14, "completion_tokens": 9, "total_tokens": 23 }, "x_lobstack": { "request_id": "2f1c…", "served_model": "gpt-5.6-luna", "requested_model": "claude-opus-5", "routed": true, "cost_usd": 0.000017, "savings_usd": 0.000352, "priced": true, "baseline_model": "claude-opus-5", "baseline_reason": "named", "baseline_cost_usd": 0.000369 } } ``` There is no `x-lobstack-cost-usd` header on a streamed response, and there cannot be: the headers are written before the provider has counted a single token. If you are streaming, the receipt is on the last frame or you do not have it. `usage` keeps the exact OpenAI shape so a foreign SDK parses it unchanged; the money hangs off `x_lobstack`, which those SDKs ignore. ## Three rules ### 1. `null` is not zero `cost_usd` is `null` — never `0` — when the Gateway could not price the call, and `priced` is then `false`. A model key outside the registry gets a completion and no price. Render `null` as **unknown**. Never as `$0.00`, never as free, and never summed into a total that you then present as exact. Writing off a real charge as zero is the single most expensive way to be wrong about money, and a UI will happily do it for you if you let a `null` fall through a `?? 0`. ```ts // wrong: turns "we do not know" into "it was free" total += receipt.cost_usd ?? 0; // right: a total, and a count of what it could not include if (receipt.cost_usd === null) unpriced += 1; else total += receipt.cost_usd; // report both: "$4.21 across 1,190 calls; 3 calls unpriced" ``` ### 2. `savings_usd` is meaningless without `baseline_reason` `savings_usd` is a subtraction, and a subtraction is only honest if the reader can see where the first operand came from. Two reasons exist: - **`named`** — the caller asked for `baseline_model` and the router served something else. `savings_usd` is a **measurement** against a model somebody actually chose. Report it as a saving. - **`plan_ceiling`** — the caller said `"auto"` and named nothing. `baseline_model` is the most expensive model their plan would have allowed: what they *would* have paid had they asked for the best one. Nobody requested it. It is a real number and also the most flattering one available. Print the reason next to the figure, or do not print the figure. Label the second case as "vs plan ceiling", never as "saved" — presenting a plan-ceiling comparison as a saving publishes a number the customer never chose to be measured against. `baseline_reason` is `null`, with `savings_usd` also `null`, when there is nothing to compare: the requested model is the served model, or the model cannot be priced. That is the correct third answer, not a gap to fill. ### 3. `cost_usd` is what the customer owes It is the provider's list price times 1.25 on `mode: managed`, and pass-through on `byok` and `direct` — because on BYOK the customer already paid their own provider and marking that up would be inventing a charge. What the tokens cost Lobstack is not in the receipt and never will be. So a BYOK receipt showing `cost_usd` is telling you what the tokens cost at the provider, not what will appear on a Lobstack invoice. ## Reading it ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.LOBSTACK_API_KEY, baseURL: "https://www.lobstack.ai/api/gateway/v1", }); // Buffered: headers, via the raw response. const { data, response } = await client.chat.completions .create({ model: "auto", messages: [{ role: "user", content: "…" }] }) .withResponse(); const costHeader = response.headers.get("x-lobstack-cost-usd"); const cost = costHeader ? Number(costHeader) : null; // "" means unpriced const requestId = response.headers.get("x-lobstack-request-id"); ``` ```python # Streamed: the last frame with a usage block is the receipt. receipt = None for chunk in client.chat.completions.create(model="auto", messages=msgs, stream=True): raw = chunk.model_dump() if raw.get("x_lobstack"): receipt = raw["x_lobstack"] for c in chunk.choices: print(c.delta.content or "", end="") if receipt and receipt["priced"]: print("\ncost $%.6f on %s" % (receipt["cost_usd"], receipt["served_model"])) elif receipt: print("\ncost unknown - model not priced") ``` ## Also worth keeping - `request_id` is the id of the trace row and is returned even on failures that happen before authentication finishes. Quote it in a support thread and a description becomes a lookup. Log it on every call; it costs nothing. - `requested_model` vs `served_model` is the routing decision, in the response. When they differ, `routed` is `true`. - `x-lobstack-dropped-params: temperature` means a sampling parameter you sent was not forwarded, because the target model rejects it with a hard 400. The request succeeded. Nothing is wrong. Do not retry. - `x-lobstack-metered: false` means the completion happened but the ledger write failed. You were served and the row is missing. Worth an alert. ## Do not - Do not compute cost from token counts and a local price table. Read it. - Do not sum `null` as zero. - Do not show a saving without its reason. - Do not assume the buffered and streamed shapes are interchangeable. ``` ### Migrate an SDK call to the Gateway — https://www.lobstack.ai/skills/lobstack-gateway-migration ```markdown --- name: lobstack-gateway-migration description: "Move existing OpenAI or Anthropic SDK code onto the Lobstack Gateway by changing the base URL and the key, and verify it actually routed. Use when asked to put an app behind Lobstack, consolidate providers behind one endpoint, or add per-call cost data to code that already calls a model API." license: MIT metadata: title: "Migrate an SDK call to the Gateway" version: 1.0.0 author: lobstack category: migration tags: [migration, openai, anthropic, sdk, gateway] --- # Migrate an SDK call to the Lobstack Gateway The Gateway is OpenAI-compatible: `POST /v1/chat/completions`, same request body, same response body, streaming included. A migration is two lines, and then one verification step that people skip. ## The two lines ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.LOBSTACK_API_KEY, // lsk_live_… or lsk_test_… baseURL: "https://www.lobstack.ai/api/gateway/v1", }); // Unchanged from here down. const res = await client.chat.completions.create({ model: "auto", // or a specific Lobstack key messages: [{ role: "user", content: "hello" }], }); ``` ```python from openai import OpenAI client = OpenAI( api_key=os.environ["LOBSTACK_API_KEY"], base_url="https://www.lobstack.ai/api/gateway/v1", ) ``` ```bash curl https://www.lobstack.ai/api/gateway/v1/chat/completions \ -H "Authorization: Bearer $LOBSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"auto","messages":[{"role":"user","content":"hello"}]}' ``` ## The trap that costs an afternoon Use the host with the `www`. The bare apex 307s to it, and [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#name-redirection-3xx) requires every HTTP client to **drop the `Authorization` header** when a redirect changes host. curl, requests, httpx (so the OpenAI Python SDK), Go, Java and PowerShell all honour it. So an apex base URL answers a caller holding a perfectly valid key with a 401 reading *missing credentials*, and the obvious conclusion to draw is that the key is bad. It is not. The host is. ## Coming from the Anthropic SDK The Gateway exposes the OpenAI shape only. There is no `/v1/messages` on it, so `@anthropic-ai/sdk` and `anthropic` cannot be pointed at it by changing a base URL. Switch the call site to the OpenAI SDK (or plain `fetch`) and keep asking for the Anthropic model by its Lobstack key — the Gateway translates to Anthropic's native Messages API on the way out, including streaming. | Anthropic SDK | Gateway equivalent | | --- | --- | | `messages.create({ system, messages })` | one `{ role: "system" }` message at the head of `messages` | | `max_tokens` (required) | `max_tokens` (optional) | | `content: [{type:"text"}]` blocks | a plain string, or OpenAI content parts | | `tools[].input_schema` | `tools[].function.parameters` | | `stop_reason: "tool_use"` | `finish_reason: "tool_calls"` | ## What changes, and what does not Does not change: the request body, the response body, streaming, tool calling, system messages, multi-turn history, your error handling for 4xx and 5xx. Changes: - **The model string.** Ask `GET /api/gateway/v1/models` for the keys this deployment serves rather than guessing. `"auto"` hands model selection to the router; a specific key pins it. An unknown key is a 400, including after alias resolution. - **You get a receipt.** Cost, the tier, the model actually served, and the request id come back on every call. See the `lobstack-receipts` skill; the point of the migration is largely this. - **`temperature` is only forwarded when you set it.** The Gateway does not invent a sampling default, because doing so silently changes the character of the output. And it is not forwarded to models that reject it — Anthropic deprecated the sampling parameters on Opus 4.7 and everything after, and those models return a hard 400 rather than ignoring the field. When one is dropped the response says `x-lobstack-dropped-params: temperature`. - **`stream_options.include_usage` is ignored.** The final chunk carries usage and cost regardless. ## Verify it actually routed This is the step people skip, and skipping it is how a migration "succeeds" while every call still goes direct to a provider. ```bash curl -sD /dev/stderr https://www.lobstack.ai/api/gateway/v1/chat/completions \ -H "Authorization: Bearer $LOBSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"auto","messages":[{"role":"user","content":"2+2"}]}' \ -o /dev/null 2>&1 | grep -i x-lobstack ``` Three things must be true: 1. `x-lobstack-request-id` is present. If it is not, you are not talking to the Gateway at all. 2. `x-lobstack-priced: true`. If it is `false`, the model key is outside the registry and this traffic is invisible on the bill. 3. `x-lobstack-mode` says what you expect — `managed` on Lobstack keys, `byok` on your own. Then grep the repository for the provider hostnames to find the call sites the migration missed. See the `audit-unmetered-model-calls` skill; on any codebase with more than one team in it, there is always at least one. ## Rolling back Reverse the two lines. Nothing else changed, no data was migrated, and no provider-side state exists on the Lobstack side to unwind. Treat that as the reason to migrate incrementally: one service, verified, then the next. ``` ### Pick a model tier, not a flagship — https://www.lobstack.ai/skills/lobstack-model-tier ```markdown --- name: lobstack-model-tier description: "Choose the cheapest model tier that can actually do the task instead of defaulting to a flagship, and verify the choice before spending on it. Use when selecting a model for a feature or agent, when one model dominates a bill, or when asked to cut inference cost without losing quality." license: MIT metadata: title: "Pick a model tier, not a flagship" version: 1.0.0 author: lobstack category: cost tags: [cost, routing, models, gateway] --- # Pick a model tier, not a flagship Defaulting to the best available model is the most common and most expensive decision in an LLM codebase, and it is almost never a decision — it is the example from a quickstart, left in. `ministral-8b` costs $0.15/$0.15 per million tokens in/out. `claude-fable-5-1` costs $10/$50. That is roughly 200× on the same tokens, which is why "which model" is a cost decision before it is a quality one. Prices last checked against each provider's own page on 2026-09-08. ## The five tiers Lobstack groups every model into five capability tiers and routes `"auto"` requests to the cheapest tier that can handle the request. The thresholds and the lead model of each tier are the router's actual configuration: | tier | complexity | what `auto` serves | $/Mtok in/out | relative cost | | --- | --- | --- | --- | --- | | nano | ≤ 20 | gemini-3.1-flash-lite | $0.25/$1.5 | 0.10× | | small | ≤ 40 | claude-haiku-4-5 | $1/$5 | 0.30× | | standard | ≤ 70 | claude-sonnet-5 | $2/$10 | 0.67× | | premium | ≤ 90 | kimi-k3 | $3/$15 | 2.00× | | flagship | ≤ 100 | claude-opus-5 | $5/$25 | 5.00× | `complexity` is a heuristic score from 0–100 over the prompt: length, whether it contains code, whether it asks for multiple steps, analysis or creative writing, and how long the conversation already is. It is a heuristic and it is worth knowing that, but it is the same one the live path uses — not an approximation of it. ## What each tier is actually for - **nano** — classification, routing, extraction, yes/no, tagging, short rewrites, "is this spam". Anything where the answer is short and the judgement is shallow. If you are calling a model in a loop over rows, start here. - **small** — summarisation, straightforward Q&A over supplied context, format conversion, commit messages, simple tool selection. The workhorse for anything where the context does the work and the model only has to be careful. - **standard** — most user-facing chat, most agent turns, code that fits in a file, multi-step tool use with a clear plan. This is the default you should be arguing yourself *up* from, not down to. - **premium** — genuine reasoning over a large context, hard debugging, design work with real trade-offs, long multi-file changes. - **flagship** — the tasks where a wrong answer costs more than the call. Use it deliberately, per call site, and be able to name why. ## The procedure 1. **Write the task down in one sentence.** If the sentence contains "classify", "extract", "label", "route" or "reformat", you are in nano or small and you are done. 2. **Ask what a wrong answer costs.** Cheap to detect and cheap to fix (a draft a human reads, a retry, a suggestion) argues down a tier. Expensive and silent (a migration, a payment, a customer-visible claim) argues up. 3. **Try the cheap tier on twenty real inputs**, not on the one you invented. Cheap models fail differently: they do not produce slightly worse prose, they miss an instruction or drop a field. Diff the outputs on the failure you care about, not on taste. 4. **Set a ceiling rather than a model** wherever you can. `"auto"` plus a plan ceiling means the easy 80% of traffic lands cheap without anyone tuning anything, and hard requests still reach a capable model. 5. **Spend the saving on the prompt.** A cheaper model with an explicit, well-structured prompt beats a flagship on a vague one, and costs a twentieth as much. See `cost-aware-system-prompts`. ## Verify before you spend `POST /api/gateway/v1/route-preview` answers "what would the router do with this?" without running inference. It is unauthenticated, writes nothing, and uses the same `selectModel()` and the same price registry the live path uses — so the answer is the decision, not a simulation of it. ```bash curl -s https://www.lobstack.ai/api/gateway/v1/route-preview \ -H 'Content-Type: application/json' \ -d '{"prompt":"Label this ticket: billing, bug, or feature request.\n\n…", "requested_model":"claude-opus-5", "plan_tier":"developer", "expected_output_tokens":8}' ``` ```json { "object": "routing_preview", "complexity": 12, "tier": "nano", "routed": true, "reason": "Trivial query → nano tier", "model": { "key": "gemini-3.1-flash-lite", "provider": "google", "price_per_mtok": { "input": 0.25, "output": 1.5 } }, "token_estimate": { "input": 31, "output": 8, "estimated": true }, "cost_usd": 0.0000198, "baseline": { "model": "claude-opus-5", "cost_usd": 0.000355, "saving_usd": 0.000335 } } ``` Two honesty notes that matter when you quote these numbers: - `token_estimate.estimated` is `true`. It is roughly four characters per token, and the output length is unknowable before generation — pass `expected_output_tokens` when you know your own shape. The billed figure always comes from the provider's usage block on the real request. - `baseline` is `null` on an `"auto"` request. There is nothing to compare against when nobody named a model, and inventing a flagship comparison is how every savings claim in this category gets manufactured. ## The ceiling is a structural cap Plans carry a tier ceiling, and the router never exceeds it — so cost control is a property of the configuration rather than something to catch in review: | plan | price | meter | included | tier ceiling | | --- | --- | --- | --- | --- | | Free | $0/mo | spend | $1 of model spend | standard | | BYOK | $19/mo | requests | 100,000 requests | flagship | | Developer | $29/mo | spend | $10 of model spend | flagship | | Studio | $99/mo | spend | $35 of model spend | flagship | | Scale | $299/mo | spend | $120 of model spend | flagship | The effective ceiling is the lower of the model you asked for and your plan's. Naming a nano model on a flagship plan caps you at nano, which is the correct behaviour and occasionally a surprise. ## Do not - Do not pick a model because it was in the example you copied. - Do not benchmark on one hand-written input. - Do not use `"auto"` and then quote a saving against a flagship. Nobody asked for the flagship; see `lobstack-receipts`, rule 2. - Do not assume cheap means fast or dear means slow. They are unrelated; measure `p95` separately. ``` ### Keep an agent inside a budget — https://www.lobstack.ai/skills/lobstack-spend-budget ```markdown --- name: lobstack-spend-budget description: "Hold an agent, job or run inside a hard dollar budget using the per-call cost the Gateway returns, and handle the 402 when an allowance runs out. Use when building an autonomous loop, when asked to cap spend on a task, or when a Gateway call returns 402 Payment Required." license: MIT metadata: title: "Keep an agent inside a budget" version: 1.0.0 author: lobstack category: cost tags: [cost, budget, agents, quota] --- # Keep an agent inside a budget An agent loop with no spend bound is a while-loop attached to a credit card. The fix is not an estimate before the call; it is a measurement after each one, and a stop condition that trusts it. ## The pattern Measure after, not estimate before. A pre-flight estimate has to guess the output length, which is the term that dominates the cost and the one thing you cannot know. The receipt is exact and it arrives in time to stop the *next* call. ```ts class Budget { private spent = 0; private unpriced = 0; constructor(private readonly limitUsd: number) {} /** Call before every model call. */ assertHeadroom(reserveUsd: number) { if (this.spent + reserveUsd > this.limitUsd) { throw new BudgetExhausted(this.spent, this.limitUsd, this.unpriced); } } /** Call after every model call, with the receipt. */ record(costUsd: number | null) { if (costUsd === null) { // Unknown cost is not zero cost. Charge the reserve against the budget so // an unpriced model cannot run forever for free. this.unpriced += 1; this.spent += this.reserveUsd; return; } this.spent += costUsd; } private readonly reserveUsd = 0.05; // your own pessimistic per-call figure } ``` Three properties make this work and each one is a bug if you drop it: 1. **The check is before the call and uses a reserve**, because a call you have not made yet has no cost. Set the reserve to something you would be unhappy but not ruined to spend once — the point is that the loop cannot overshoot by more than one call. 2. **`null` is charged, not ignored.** `cost_usd` is `null` for a model the Gateway cannot price. Treating that as `0` gives an unpriced model an unlimited budget, which is precisely backwards. 3. **The exhaustion carries the numbers.** "Budget exhausted" is not actionable; "$4.98 of $5.00 across 212 calls, 3 unpriced" is. Cap turns as well as dollars. A loop that stalls on a cheap model burns wall clock and context without ever tripping a dollar limit. ## Where you stand, before you start The quota headers ride on **every** response, success or failure, so a long job can check its own headroom without a second API call: ``` x-lobstack-quota-meter: spend # spend | requests | legacy x-lobstack-quota-allowance-usd: 10.000000 x-lobstack-quota-spent-usd: 4.113200 x-lobstack-quota-remaining-usd: 5.886800 x-lobstack-quota-resets: 2026-10-01T00:00:00.000Z ``` On a request-metered plan (BYOK) the same three facts arrive as `x-lobstack-quota-limit`, `-used` and `-remaining` as integers, plus `-credits` when purchased balance remains. Read `-meter` first and branch; the counted headers are absent on a spend meter and vice versa, deliberately, so a client cannot read a missing header as "no allowance". ## When the allowance runs out: 402 ```json { "error": { "message": "monthly allowance exhausted ($10.00 of $10.00 of model spend). Add a top-up or upgrade the plan, or wait for the period to reset.", "type": "quota", "code": 402, "request_id": "8f2b1e0c-4d5a-4b91-9c3e-6a7f0d2b5c41" } } ``` **Do not retry a 402.** `retry-after` is present and is computed from the period reset, so it is usually days rather than seconds. It means "this will not clear on its own before then", not "back off and try again". The resolutions are a top-up, a plan upgrade, or the period rolling over. A 429 is the other member of the `quota` class and is the opposite advice: that one is a rate limit, in practice the provider's, passed through with its status. Back off exponentially with jitter and retry. ## Two behaviours worth knowing before you rely on them **Enforcement is asymmetric.** API-key callers are enforced and get a 402. Agent credentials get the same headers and are *not* blocked. That is deliberate: switching agents to enforced would start returning 402 to machines that have been over their allowance for weeks and working fine. So if you are building on an agent credential, the headers are advisory — your own budget check is the only thing actually stopping the loop. **Errors accrue no spend.** A failed round writes a ledger row with zero tokens, and a request that failed at the provider does not consume a request allowance. An outage does not eat your month. ## Structural caps beat runtime caps Two levers cost nothing to set and cannot be forgotten at a call site: - **The tier ceiling on the plan.** The router never exceeds it, so the worst case per call is bounded by configuration rather than by review. See `lobstack-model-tier`. - **`max_tokens`.** Output is priced several times input across the registry, so a bounded output is the single most effective per-call cap available. Set it to what the answer actually needs. And on paid plans that credit savings, 50% of verified routed savings comes back as allowance — verified meaning both models priced, the row not an error, on a managed key, and the baseline strictly above the actual cost. Do not model that as a discount; model it as the reason routing shows up somewhere the budget feels it. ## Do not - Do not estimate cost before the call and call it a budget. - Do not treat `null` cost as free. - Do not retry a 402. - Do not bound dollars without also bounding turns. ``` ### Write a cost-aware system prompt — https://www.lobstack.ai/skills/cost-aware-system-prompts ```markdown --- name: cost-aware-system-prompts description: "Write or review a system prompt that does not quietly multiply the bill — bounded output, no re-sent bulk, tool schemas counted as the input tokens they are. Use when authoring or reviewing a system prompt, when cost per turn is climbing, or when adding tools to an agent." license: MIT metadata: title: "Write a cost-aware system prompt" version: 1.0.0 author: lobstack category: cost tags: [cost, prompting, agents, tokens] --- # Write a cost-aware system prompt A prompt is a recurring charge. Everything in it is re-sent on every turn of every conversation, so a paragraph you added once is billed thousands of times, and the usual review — does it produce good output — never looks at that. Four facts about pricing decide most of this, and all four are checkable against the model catalogue rather than being rules of thumb. ## Fact 1: output costs several times input Across almost the whole registry, output tokens are priced four to five times input. `claude-opus-5` is $5 in and $25 out per million; `claude-sonnet-5` is $2 and $10; `gemini-3.8-flash` is $0.75 and $3.75. So the highest-leverage sentence in any prompt is the one that bounds the answer, and `max_tokens` is the highest-leverage parameter. ``` Answer in at most three sentences. No preamble, no summary of the question, no offer of further help. If the answer is a single value, return the value. ``` That is not a style preference. Removing "Great question! Let me help you with that." from every response of a busy endpoint is a real line on an invoice. Be specific about the shape you want, because a model that does not know the shape hedges, and hedging is output tokens: "return JSON matching this schema, nothing else" costs a fraction of "explain your reasoning and then give the answer". ## Fact 2: the system prompt is billed per turn, not per session There is no per-conversation discount. A 900-token system prompt on a 40-turn conversation is 36,000 input tokens spent restating instructions the model has already followed 39 times. Review a prompt by asking, of every paragraph: *does the model's behaviour change if I delete this?* Delete anything where the answer is no. The usual finds: - Politeness instructions the model already follows. - Long "you are an expert…" preambles that change nothing measurable. - Repeated instructions, in different words, added by different people. - Examples that duplicate each other. Two well-chosen examples beat six. - Prohibitions on things the model was never going to do. Move anything long, conditional and rarely needed *out* of the system prompt: a reference file the agent reads when it needs it costs nothing on the turns where it does not. That is the whole argument for progressive disclosure, and it is a cost argument as much as an attention one. ## Fact 3: tool schemas are input tokens Every tool definition — name, description, full JSON Schema — is serialised into every request that has tools enabled, whether or not any tool is called. Twenty tools with verbose parameter descriptions is a fixed tax on every turn. - Enable the tools this agent actually needs, not the catalogue. - Keep parameter descriptions to one line each. - Prefer one tool with a mode parameter to five near-duplicates. - If a tool has never been called in production, delete it. ## Fact 4: "think step by step" is a purchase order Instructions that ask for visible reasoning buy output tokens at the output rate, and reasoning-heavy phrasing also pushes the router's complexity score up a tier. Sometimes that is exactly what you want on a hard task. It is rarely what you want on the easy 80% of traffic. If you need reasoning but not the reader's copy of it, ask for the conclusion and keep the working private to the model where the model supports that — do not ask for a written chain of thought you then discard, which is paying for tokens you throw away. ## Cheap models want more explicit prompts Tier and prompt are one decision, not two. A nano or small model will follow an explicit, structured instruction and will quietly ignore an implication. So when you move a call site down a tier: - Turn implications into imperatives. Not "keep it brief" but "at most 40 words". - Enumerate the allowed outputs. Not "categorise" but "return exactly one of: billing, bug, feature". - Put the format last. It is the instruction most often lost. - State what to do when the input does not fit — otherwise it invents something. This is why the cheap tier so often "fails" on a first attempt: the prompt was written for a model that was covering for it. ## A review checklist Run this over any prompt before it ships: - [ ] Is the output bounded, in words and by `max_tokens`? - [ ] Would deleting each paragraph change behaviour? Delete the ones that would not. - [ ] Is anything long, conditional or rarely needed inline that could be a file read on demand? - [ ] Are only the needed tools enabled, with one-line parameter descriptions? - [ ] Is bulk context (whole files, full histories, entire tables) being re-sent every turn? - [ ] Is visible reasoning requested? Is it read by anyone? - [ ] Are the allowed outputs enumerated, so a cheaper tier can hold the call site? ## Measure it, do not argue about it Run the same twenty real inputs through the old and new prompt and compare `x_lobstack.cost_usd` per turn plus your own quality check. The Gateway prices every call, so this is a measurement rather than an opinion — and a prompt change that saves 30% per turn with no quality change is the cheapest optimisation available to any LLM product. ## Do not - Do not put a rate card, a price table or a model list in a system prompt. It goes stale and it is billed forever. Read it from the API. - Do not paste an entire file when a section answers the question. - Do not add a paragraph to fix one bad output. Fix it with an example or a bounded instruction, then delete the paragraph. ``` ### Audit a codebase for unmetered model calls — https://www.lobstack.ai/skills/audit-unmetered-model-calls ```markdown --- name: audit-unmetered-model-calls description: "Find model calls that bypass the gateway and therefore appear on no bill anyone reads, and detect the opposite fault of counting one call twice. Use when auditing AI spend, when a provider invoice exceeds internal numbers, before consolidating providers, or when a cost dashboard reads zero." license: MIT metadata: title: "Audit a codebase for unmetered model calls" version: 1.0.0 author: lobstack category: observability tags: [observability, audit, cost, metering] --- # Audit a codebase for unmetered model calls An unmetered call is one that reaches a provider without passing through anything that records what it cost. It works perfectly, it produces good output, and it is invisible to every dashboard — so it is discovered by the invoice, weeks later, and cannot be attributed to a feature or a team. Two faults, opposite directions, both silent: - **Unmetered** — traffic goes direct to a provider. Internal totals are under the invoice. Nobody can say which feature spent it. - **Double-metered** — one call is recorded twice by two layers. Internal totals are over. Customers can be overcharged. This has really happened in this codebase: every successful request was metered once by the Gateway and once by a bridge posting the same usage to a second endpoint, inflating tokens and cost 2× in the overcharge direction. ## 1. Find the direct calls Grep for the hostnames a provider is dialled at. These are the ones the Gateway itself uses, so they are the complete set for this deployment: ```bash rg -n --hidden -g '!node_modules' -g '!*.lock' \ -e 'api.anthropic.com' \ -e 'api.deepseek.com' \ -e 'api.groq.com' \ -e 'api.mistral.ai' \ -e 'api.moonshot.cn' \ -e 'api.openai.com' \ -e 'api.x.ai' \ -e 'dashscope-intl.aliyuncs.com' \ -e 'generativelanguage.googleapis.com' ``` Then the SDK constructions, which are the same finding wearing a library: ```bash # Any client built without an explicit base URL is going direct. rg -n -g '!node_modules' \ -e 'new OpenAI\(' -e 'OpenAI\(' -e 'AsyncOpenAI\(' \ -e 'new Anthropic\(' -e 'Anthropic\(' \ -e 'ChatOpenAI\(' -e 'ChatAnthropic\(' -e 'ChatGoogleGenerativeAI\(' \ -e 'generativeai' -e 'GenerativeModel\(' # Provider credentials in the environment are the tell that survives refactors. rg -n -g '!node_modules' \ -e 'OPENAI_API_KEY' -e 'ANTHROPIC_API_KEY' -e 'GOOGLE_API_KEY' \ -e 'GEMINI_API_KEY' -e 'XAI_API_KEY' -e 'GROQ_API_KEY' \ -e 'MISTRAL_API_KEY' -e 'DEEPSEEK_API_KEY' ``` For each hit, decide which it is: | finding | verdict | | --- | --- | | client with no `baseURL` / `base_url` | **unmetered** — route it | | `baseURL` pointing at a provider host | **unmetered** — route it | | `baseURL` pointing at the Gateway | metered — confirm with a live header check | | a provider key still in the environment | dead credential or a path you have not found yet | | a test fixture or a recorded cassette | fine; exclude it explicitly, not by accident | Places the grep will miss, so look by hand: notebooks and scratch scripts, CI jobs and cron, serverless functions in a separate repository, evaluation and benchmark harnesses, and vendored or in-house SDK wrappers that hold the base URL one layer down. ## 2. Confirm the metered ones really meter A base URL pointing at the Gateway is necessary and not sufficient. The model key must also be one the registry can price, or the call is served and recorded with no cost — money through the gateway, uncosted. ```bash curl -sD /dev/stderr https://www.lobstack.ai/api/gateway/v1/chat/completions \ -H "Authorization: Bearer $LOBSTACK_API_KEY" -H 'Content-Type: application/json' \ -d '{"model":"","messages":[{"role":"user","content":"ping"}]}' \ -o /dev/null 2>&1 | grep -iE 'x-lobstack-(priced|metered|cost-usd|model)' ``` `x-lobstack-priced: false` is the finding. So is `x-lobstack-metered: false`, which means the completion happened and the ledger write did not. Server-side, the same fault shows up as `summary.unpriced_requests` on `GET /api/v1/usage`. Non-zero on a recent window means an unknown model key is passing through right now. ## 3. Reconcile against the outside world Internal numbers agreeing with each other proves nothing. Only two comparisons are real: 1. **Ledger against the provider's own billing page**, per provider, per month. Nothing inside your system can substitute for this. 2. **Request count against trace count.** Every request that reaches the Gateway writes a trace row, including ones that failed before a token existed. A gap between traces and ledger rows is a metering fault; ledger rows without traces are a second writer. Sanity checks that have each caught a real fault here: - Is total recorded cost plausible, or is it $0.00? A table of thousands of rows summing to zero is not a quiet month. - What fraction of rows are error rows? 99.1% of every request this Gateway had ever recorded had failed, for six weeks, and nothing alerted — because a failing request still returns a 200-shaped nothing to a client that does not check. - Is the tenant id null? If every row groups into `NULL`, no invoice can be attributed to anyone, and the totals are correct and useless. - Do any two rows share a request id, a session and a token count within the same second? That is the double-metering signature. `npx tsx scripts/metering-audit.ts --days 90` runs the ledger side of this against a Lobstack database. ## 4. Write it down as a decision list A finding that is not a decision gets rediscovered next quarter. One line each: ``` src/jobs/summarise.ts:41 direct OpenAI client, no base URL -> route (owner: data) scripts/eval/run.py:12 direct Anthropic, eval harness -> accept, tag as eval spend src/lib/legacy/chat.ts:88 Gateway, model "gpt-4-turbo" -> unpriced key, fix the key infra/cron/digest.ts:7 GOOGLE_API_KEY in env, no call site -> dead credential, revoke ``` Then re-run the greps in CI so the list cannot silently regrow. The next unmetered call site will be added by someone who has never read this audit. ## Do not - Do not accept "our dashboard says $0" as good news. - Do not fix double-metering by deleting rows. Find the second writer. - Do not delete a provider key from the environment before you know which call site holds it. Something will 500 in production at 3am. ``` ### Put Cursor or Aider behind the Gateway — https://www.lobstack.ai/skills/lobstack-local-proxy ```markdown --- name: lobstack-local-proxy description: "Route Cursor, Aider, Continue or any OpenAI-compatible tool through the Lobstack Gateway with a local proxy, and see a cost line per request. Use when asked to route an editor or coding CLI through Lobstack, to find what an AI coding tool is costing, or to use one key across several tools." license: MIT metadata: title: "Put Cursor or Aider behind the Gateway" version: 1.0.0 author: lobstack category: migration tags: [migration, cli, proxy, tools] --- # Put Cursor or Aider behind the Gateway Most AI coding tools have an OpenAI-compatible setting and no way to tell you what they cost. The proxy fixes both with one base URL: the tool keeps working exactly as it did, every call is routed and metered, and each request prints a line saying what it was. ## Two commands ```bash npx lobstack init # saves a key to ~/.lobstack/config.json, mode 0600 npx lobstack proxy # http://127.0.0.1:8787/v1 ``` ``` Listening on http://127.0.0.1:8787/v1 -> https://www.lobstack.ai Point any OpenAI-compatible tool at it: OPENAI_BASE_URL=http://127.0.0.1:8787/v1 OPENAI_API_KEY=anything ``` `OPENAI_API_KEY` really is anything. The proxy holds your Lobstack key and attaches it upstream; the tool never sees it. That is the main reason to use a proxy rather than pasting a key into five different editor configs. Per request, to stderr: ``` - model claude-haiku-4-5 - asked claude-opus-5 - tokens 400/140 - cost $0.001100 - saved $0.004400 ``` ## Pointing the tools at it - **Cursor** — Settings → Models → add an OpenAI-compatible provider with base URL `http://127.0.0.1:8787/v1` and any key. Add the Lobstack model keys you want to appear in the picker. - **Aider** — `OPENAI_API_BASE=http://127.0.0.1:8787/v1 OPENAI_API_KEY=x aider --model auto` - **Continue** — in `config.json`, a model with `"provider": "openai"`, `"apiBase": "http://127.0.0.1:8787/v1"`, `"apiKey": "x"`. - **Anything else** — if it has a base URL field, it works. If it only speaks Anthropic's Messages API, it does not: the Gateway is OpenAI-shaped, and so is the proxy. Set `--model auto` where the tool allows it and the router picks per request; name a specific Lobstack key where you want it pinned. ## Read this before leaving it running **It binds loopback only, and that is load-bearing.** This is a process holding a credential and answering *unauthenticated* requests. Anything that can reach the port can spend your money. Do not bind it to `0.0.0.0`, do not port-forward it, do not run it on a shared box, and do not tunnel it to a teammate — give them their own key instead. Also worth knowing: - It serves the OpenAI paths under `/v1` and 404s anything else locally, so a mistyped path is a local error rather than a confusing one from upstream. - Streams are piped straight through and the receipt is parsed from a copy of the tail, so the first token is not delayed by the last one. Interactive tools stay interactive. - It has no dependencies — `fetch`, `node:http` and `node:readline` are all in the runtime — so `npx lobstack` starts rather than resolving a tree first, and there is no supply chain between your key and the Gateway. - Point `--base` at the bare apex and the CLI rewrites it to the `www` host and says so out loud, because a redirect across hosts makes every HTTP client drop the `Authorization` header. A silent fix would teach you nothing about why your own code fails the same way. ## The rest of the CLI ```bash npx lobstack chat "explain a b-tree in two sentences" # answer to stdout, receipt to stderr npx lobstack models # what the Gateway serves, with prices npx lobstack spend --days 7 # what you spent (needs the usage:read scope) ``` `chat` puts the answer on stdout and the receipt on stderr, so `> out.txt` gives you the answer alone and the cost still lands on your terminal. Cost is read from the response, never computed from token counts and a bundled rate card. `cost_usd` is `null` and prints as `unpriced` when the Gateway could not price a call — a zero would render as free, and writing off a real charge is the most expensive way to be wrong about money. ## What this does not do It is a forwarder, not a policy engine: no per-tool budget, no allow-list of models, no request rewriting. If you need a hard cap, put it on the plan's tier ceiling and allowance, where the Gateway enforces it — see `lobstack-spend-budget`. ``` ### Debug a Gateway 400, 402 or 429 — https://www.lobstack.ai/skills/lobstack-gateway-errors ```markdown --- name: lobstack-gateway-errors description: "Diagnose a Lobstack Gateway failure from its status and error class and decide whether retrying can possibly help. Use when a call returns 400, 401, 402, 403, 429 or 5xx, when a stream ends in an error frame, or when someone reports that the Gateway is broken." license: MIT metadata: title: "Debug a Gateway 400, 402 or 429" version: 1.0.0 author: lobstack category: gateway tags: [gateway, errors, retries, debugging] --- # Debug a Gateway 400, 402 or 429 Every failure has the same shape, and the `type` field is the whole diagnosis: ```json { "error": { "message": "…", "type": "quota", "code": 402, "request_id": "8f2b1e0c-4d5a-4b91-9c3e-6a7f0d2b5c41" } } ``` `request_id` is also returned as `x-lobstack-request-id`, on **every** response including ones that fail before authentication finishes. It is the id of the trace row, which holds the status, the latency, the model, the credential and the first 1,000 characters of the provider's message. Quote it and a description becomes a lookup; lose it and you are guessing. ## The six classes | type | status | whose problem | retry | | --- | --- | --- | --- | | `auth` | 401, 403 | the credential | never | | `quota` | 402, 429 | allowance or rate limit | 402 no, 429 yes | | `validation` | 400, 409, 422 | the request body | never | | `provider` | 502, 503, other 5xx | the upstream provider | yes, with backoff | | `timeout` | 408, 504 | we gave up waiting | yes, carefully | | `internal` | 500 | ours | once, then report the id | An unrecognised throw is classified `internal`, not `provider` — blaming upstream for our own crashes would make the error rate look better than it is. So `internal` really does mean a Lobstack bug. ## auth — 401, 403 Causes, in the order they actually occur: no bearer token; a key that does not match `lsk__<8 hex><48 hex>`; a key that is unknown, revoked or expired; a key without the `inference` scope; an agent credential whose machine is not running, which returns 403 with the machine status in the message. Revoked and expired are reported distinctly from unknown, because "this key was revoked" beats "unauthorized" at two in the morning. All of them are rejections: never retry. **The 401 that is not about your key.** If the message reads like missing credentials and the key is definitely good, check the host. The bare apex 307s to the `www` host, and RFC 9110 requires every HTTP client to drop the `Authorization` header when a redirect changes host. Use `https://www.lobstack.ai/api/gateway/v1`. ## quota — 402 and 429 are opposite advice **402** — the allowance for the period is gone. `retry-after` is present and is computed from the period reset, so it is usually days. It means "this will not clear on its own before then", not "back off". Top up, upgrade, or wait for the reset. Read `x-lobstack-quota-meter` first: `spend` carries dollar headers, `requests` and `legacy` carry integer ones, and the absent set is absent deliberately so a client cannot read a missing header as zero allowance. **429** — a rate limit, in practice the provider's, passed through with its status. Back off exponentially with jitter and retry. Enforcement is asymmetric on purpose: API keys are enforced, agent credentials get the headers and are not blocked. So a 402 on an agent credential is not something you will see today, and your own budget check is what stops the loop. ## validation — 400 It will fail identically until the body changes. Causes: `messages` missing or empty; a body that is not JSON; a model key the registry does not know, including after alias resolution; a model that exists but is not available in managed mode; BYOK mode with no key on the agent. Provider 400s also land here, with the provider's own message inside — which means a `validation` error can be about something you never wrote. ### The worked example: the temperature 400 Anthropic deprecated the sampling parameters on Opus 4.7 and everything after it, including Opus 5, Sonnet 5 and the Fable line. The API does not ignore a `temperature` on those models. It returns: ```json 400 {"type":"invalid_request_error","message":"`temperature` is deprecated for this model."} ``` This was the most common failure in this Gateway's history, and not because callers were setting temperatures: the Gateway itself sent `temperature: 0.7` on every Anthropic request whether or not anyone asked for one, and every single one failed — for months, while almost no inference ran and nobody could see why. Both halves are fixed. A temperature is forwarded only when the caller set one, and only to models that still accept it, matched on the provider-native id that goes on the wire. When one is dropped you get `x-lobstack-dropped-params: temperature` and a successful response. So if you see this 400 today, you sent a temperature to a model that rejects it through a path that is **not** this Gateway. Find that path — see `audit-unmetered-model-calls`. ## provider — 502, 503, 5xx The upstream failed and it is not your fault. The network call threw (502), the provider returned a 5xx (passed through with its status), or no managed provider key is configured on the deployment (503). A provider 401 is deliberately remapped to 502: Lobstack's own credential being wrong is our failure, and reporting it as 401 would send you to check a key that is fine. Retry with exponential backoff and jitter — except the 503 whose message says no managed provider key is set. That one is configuration and will fail forever until a key is added or the caller switches to BYOK. ## timeout — 408, 504 Provider calls abort at 120 seconds by default; the route allows up to 300. Anything whose message looks like an abort or a timeout lands here even when it arrives as an unrecognised throw. **There is no idempotency key.** A request that timed out may have completed at the provider, so an automatic retry can produce a second answer and a second charge — both metered, because the tokens were really generated. For anything expensive, read the trace by request id before retrying rather than retrying blind. ## Failures inside a stream A stream that fails part-way through cannot change its status: it is already 200 and the headers are long gone. The Gateway sends an error object into the stream and then `[DONE]`: ``` data: {"error":{"message":"provider error 529: overloaded","type":"gateway_error","code":529, "request_id":"8f2b1e0c-…"}} data: [DONE] ``` Note that inside the stream `type` is `gateway_error`, not one of the six class names. The class is still on the trace row. **A client that only checks the HTTP status will record this stream as a success** — check for an `error` key on every frame. ## Counting them `GET /api/v1/usage` returns `summary.errors_by_class` over any range, which is the fastest way to answer "was that bad hour upstream or ours". A failed round also writes a ledger row with zero tokens, so error rate is computable from the ledger alone. See `lobstack-usage-reporting`. ## Triage in one pass 1. Pull `request_id` out of the response or the headers. Log it. 2. Read `type`. It tells you whose problem it is before you read the message. 3. `auth` or `validation` → fix the call. Retrying is guaranteed to fail. 4. `quota` → check `x-lobstack-quota-meter`; 402 top up, 429 back off. 5. `provider` or `timeout` → back off and retry, and remember there is no idempotency key. 6. `internal` → retry once, then report the request id. ``` ### Report spend from the usage API — https://www.lobstack.ai/skills/lobstack-usage-reporting ```markdown --- name: lobstack-usage-reporting description: "Answer what an organisation spent, on which models and keys, and what failed, from the Lobstack usage API rather than by estimating. Use when producing a cost report, attributing spend to a model, key or agent, or investigating an hour of high latency or errors." license: MIT metadata: title: "Report spend from the usage API" version: 1.0.0 author: lobstack category: observability tags: [observability, usage, reporting, cost] --- # Report spend from the usage API One endpoint answers every version of "what did we spend". Use it rather than summing your own logs: the Console and the desktop client both read this, so a report built from it agrees with what the customer sees instead of quietly disagreeing by a few percent. ```bash curl -s "https://www.lobstack.ai/api/v1/usage?range=30d&group_by=model" \ -H "Authorization: Bearer $LOBSTACK_API_KEY" | jq ``` The key needs the `usage:read` scope, or the response is a 403 saying exactly that. A browser session also works, which is how the Console reads it. | param | values | default | | --- | --- | --- | | `range` | `7d` `14d` `30d` `90d` | `7d` | | `group_by` | `day` `model` `key` `agent` | `day` | | `agent_id` | restrict to one agent | — | | `key_id` | restrict to one API key | — | ## The summary, and the two fields that decide whether you may quote it ```json { "enabled": true, "org_id": "…", "authenticated_via": "api_key", "range": "30d", "group_by": "model", "summary": { "requests": 12480, "errors": 41, "error_rate": 0.0033, "errors_by_class": { "provider": 33, "timeout": 6, "validation": 2 }, "prompt_tokens": 8104221, "completion_tokens": 1201884, "total_tokens": 9306105, "cost_usd": 41.204118, "unpriced_requests": 0, "p50_latency_ms": 780, "p95_latency_ms": 2410, "p99_latency_ms": 5120, "streamed": 9902 }, "groups": [ … ], "truncated": false } ``` **`unpriced_requests`** — rows the meter could not price. `cost_usd` sums a `null` as zero, which is the only arithmetic available and not the only truth: a total built partly from unpriced rows is a **floor**, and a reader who is not told how many were unpriced will read it as exact. Non-zero on a recent window means an unknown model key is passing through uncosted right now — go find it with `audit-unmetered-model-calls`. **`truncated`** — true when the row cap bound. Sums are then a floor as well. Narrow the range or the filters rather than publishing the number. Report both, always, in one sentence: *"$41.20 across 12,480 requests over 30 days; 0 unpriced, not truncated."* That sentence is defensible. `$41.20` on its own is not. The latency figures are true percentiles over the raw values rather than bucketed approximations, so `p95` is the 95th percentile and can be compared across ranges. ## The four questions, and the group_by that answers each ```bash # What is it costing us over time, and is that trending? curl -s ".../api/v1/usage?range=30d&group_by=day" -H "$AUTH" \ | jq -r '.groups[] | [.key, .requests, .cost_usd] | @tsv' # Which model is the bill? Almost always one, and almost always a surprise. curl -s ".../api/v1/usage?range=30d&group_by=model" -H "$AUTH" \ | jq -r '.groups | sort_by(-.cost_usd)[] | [.key, .requests, .cost_usd] | @tsv' # Which team or environment? One key per surface makes this answerable. curl -s ".../api/v1/usage?range=30d&group_by=key" -H "$AUTH" | jq '.groups' # Which agent? For a fleet, this is the per-unit economics. curl -s ".../api/v1/usage?range=7d&group_by=agent" -H "$AUTH" | jq '.groups' ``` Mint one API key per surface — web, batch job, staging, each internal tool — and `group_by=key` becomes cost attribution for free. Retrofitting that after the fact is impossible: the rows are already written against one key. ## Reading a bad hour `errors_by_class` settles the only question that matters first: whose fault was it. `provider` and `timeout` dominating means upstream. `validation` dominating means a deploy went out with a bad body — check what changed. `auth` dominating means a key was rotated or revoked and something did not get the memo. Then pull the specific requests by id from the trace table, or reproduce with the request id in hand. The class names and what each implies are in `lobstack-gateway-errors`. ## From a terminal ```bash npx lobstack spend --days 7 ``` Same data, same arithmetic, no jq. Also needs `usage:read`. ## Two responses that are not errors `enabled: false` with `reason: "request_tracing_not_migrated"` means the trace table does not exist on that deployment yet. It is a 200 with a `null` summary, deliberately, so a dashboard renders "not available" rather than "$0.00". An empty `groups` array with a real summary means the range genuinely had no traffic. Do not conflate the two. ## Do not - Do not publish `cost_usd` without `unpriced_requests` and `truncated`. - Do not build a parallel cost table from your own logs. It will disagree with the invoice, and the invoice will be right. - Do not compare this month's `cost_usd` with last month's without checking whether the model mix moved. A cheaper month can be a worse month. ``` ### Author an Agent Skill — https://www.lobstack.ai/skills/authoring-agent-skills ```markdown --- name: authoring-agent-skills description: "Write a SKILL.md an agent will actually load and follow — trigger-shaped description, valid frontmatter, progressive disclosure, no credentials. Use when creating or reviewing a skill, packaging a repeated workflow, or deciding whether something should be a skill, a connector or just a prompt." license: MIT metadata: title: "Author an Agent Skill" version: 1.0.0 author: lobstack category: authoring tags: [authoring, skills, documentation] --- # Author an Agent Skill A skill is a folder with `SKILL.md` at its root that teaches an agent how to do something well. Markdown, plus optional scripts and reference files. No credentials, because there is nothing to authenticate against. The unit of distribution is text, so the copy *is* the install. ``` my-skill/ SKILL.md # required. frontmatter + instructions reference/ schema.md # read on demand, not on every turn scripts/ check.py # deterministic work the model should not do by hand ``` ## Is it a skill at all? Three things get confused, and picking wrong wastes a week: | you want | build | | --- | --- | | the agent to *know how* to do something | a **skill** — markdown, no credentials, portable | | the agent to *reach* a service | a **connector** — auth config, tool definitions, an endpoint | | the agent to do one specific thing right now | a **prompt**. Not everything needs packaging | If your draft has an API key in it, you are writing a connector. If it is "summarise this in our house style, this once", it is a prompt. A skill earns its folder when it is knowledge that applies repeatedly, that a general model does not already have, and that you would otherwise re-explain. ## The frontmatter is a contract ```yaml --- name: lobstack-receipts description: "Read the receipt Lobstack returns on every model call — cost_usd, savings_usd, baseline_reason — and report cost from it instead of estimating. Use when a response carries x_lobstack or an x-lobstack-* header, when asked what a call or a run cost, or when a cost renders as $0.00." license: MIT metadata: version: 1.0.0 author: lobstack tags: [cost, receipts, metering] --- ``` - **`name`** — lowercase, hyphenated, matches the directory. This is the handle. - **`description`** — the only thing that decides whether the skill ever fires. See below; it is the whole game. - **`license`** — state one. A skill with no licence cannot be adopted by anyone with a lawyer. - Everything else under **`metadata`**, which loaders tolerate and do not interpret. Quote the description if it contains a colon. One unquoted colon is a YAML parse error, which is a skill that silently never loads — and "silently" is the part that costs you a day. ## The description is the trigger An agent decides whether to load a skill from its name and description alone. It does not read the body first. So a description that only says *what* the skill is will not fire at the moment it is needed. Write two halves: what it does, then **when to use it**, in the words that will be in the air when it applies. ``` ✗ "Utilities for cost analysis." ✗ "Best practices for working with the Lobstack Gateway." ✓ "Hold an agent inside a dollar budget using the per-call cost the Gateway returns. Use when building an autonomous loop, when asked to cap spend on a task, or when a Gateway call returns 402 Payment Required." ``` Name the concrete triggers: the error code, the field name, the file type, the phrase a person would actually say. `402`, `x_lobstack`, "what did this cost", "route Cursor through". Those are what match. ## Write the body for an agent, not a reader - **Imperative, specific, testable.** "Set `max_tokens`" beats "consider limiting output". The second is advice; the first is an instruction. - **Lead with the rule, then the reason.** An agent that stops reading after the first paragraph should still be right. - **Show the exact string.** Real header names, real field names, real endpoint paths, copy-pasteable commands. This is the single biggest difference between a skill that changes behaviour and one that reads well. - **Say what not to do, and why.** Negative constraints are followed more reliably than positive suggestions, and the "why" is what lets the agent generalise rather than pattern-match. - **One skill, one job.** If the description needs "and", split it. - **Keep `SKILL.md` short enough to be read whole** — a few hundred lines. Long tables, full schemas and rare edge cases go in `reference/` and get read when needed. Everything inline is paid for on every turn that loads the skill; see `cost-aware-system-prompts`. - **Push determinism into scripts.** If the task is "validate this JSON" or "count these rows", ship a script and tell the agent to run it. A model doing arithmetic by hand is a model getting arithmetic wrong occasionally. ## Never put in a skill - **Credentials, tokens, keys, or connection strings.** Skills are copied, pasted into repositories, and committed. Reference an environment variable by name. - **Anything that goes stale silently** — a price table, a model list, a rate card, a version number of somebody else's software. Tell the agent which endpoint to ask instead. - **A claim you have not verified.** A skill that confidently documents behaviour the system does not have is a bug report written in advance and filed by your user. Read the source, then write the sentence. ## Test it, or you have not written it 1. **Does it load?** Parse the frontmatter. A YAML error is silent. 2. **Does it fire?** Start a fresh session, say the thing a user would say, and check the skill was picked up without you naming it. If not, the description is the problem, not the body. 3. **Does it change behaviour?** Run the same task with and without it and diff the result. A skill that changes nothing measurable should be deleted, however good it reads. 4. **Does it survive a cheaper model?** If it only works on a flagship, the instructions are implicit somewhere. Make them explicit. ## Versioning and publishing Bump `metadata.version` when the *instructions* change, not when a typo is fixed. Keep the directory name stable — it is the handle people have written down. State the licence. Say what the skill assumes about the environment in the first paragraph, because the reader's environment is not yours. Skills submitted to this archive keep their author and their licence. Official Lobstack skills are MIT, and there are no install counts here because we have none and inventing them would be the first false thing on the page. ``` ## What shipped 7 posts, newest first. Each is one change, the day it landed on main. ### 2026-09-11 — A terminal UI for the CLI, still with zero dependencies The CLI opens a full-screen UI showing what each call cost as it happens, drawn by hand because a key-holding process should not pull a package tree. The CLI starts immediately because it resolves no dependency tree, and that is the point rather than a detail. A TUI normally means ink or blessed. There is no supply chain between your key and us, so this one is hand-rolled on `node:readline`'s keypress decoder and nine escape sequences. ## The reason it exists is not the chat pane Every tool has a chat pane. What you cannot get anywhere else is the price of the call you just made, pinned under the conversation, and a running total in the corner that moves while you work. Rows go to the receipt before the transcript when the window is short. You can scroll back for history; you cannot scroll back for a price you never saw. During a stream there is deliberately no dollar figure at all. Cost is read off the last frame, never computed, so the pane shows elapsed time and characters and says the price arrives with the last frame. ## Money is never truncated At 80 columns the full receipt is 103 cells and does not fit. A naive clip throws away the end of the line, which is where the saving is. So the receipt is three pieces with shorter fallbacks and the layout picks the widest set that fits: `$0.004400` cut to `$0.004` is not a shorter number, it is a wrong one. Labels drop first, then tokens, then latency, then the saving itself. The view is a pure function of state and width, which makes "what does this look like at 40 columns" a unit test that reads as text, and makes `NO_COLOR` and `TERM=dumb` free — colour depth zero makes the styler the identity function, and one code path draws every case. ## The boring cases, which is where a TUI usually breaks - Piped or redirected: never draws. `echo "hi" | lobstack > out.txt` puts the answer, and only the answer, in the file. - `TERM=dumb`: a dumb terminal has no cursor addressing, so a full-screen frame is not a degraded experience, it is garbage. Plain prompt loop, and it says why. - Resize, `Ctrl+C`, `SIGTERM`, an uncaught throw: every exit path ends stdout with reset, show-cursor, leave-alt-screen. Leaving someone in the alt screen with a hidden cursor and raw mode on is the worst thing this program could do. Mouse reporting is never switched on. It breaks click-to-select in JetBrains and in some tmux configurations, and a process that dies before disabling it leaves your shell reading mouse packets as keystrokes. ## The proxy is the part to know about ```bash lobstack proxy ``` Point Cursor or Aider at the port it prints and every call those tools make appears in the same transcript, with its price, in the same running total as what you type by hand. Bare `lobstack` opens the UI only when there is a terminal on both ends and a key already resolves. In a pipeline, in CI, or before `init`, it prints exactly what it printed before. The five existing commands are unchanged. Full key bindings and the degradation table are in the [CLI docs](/docs/cli). https://www.lobstack.ai/news/cli-terminal-ui ### 2026-09-11 — An Agent Skill is not a connector The Skills page listed 153 credentialed third-party integrations and called them skills. Not one of them was an Agent Skill. Gmail is not a skill. It is an OAuth grant with tool definitions attached, and a page calling it a skill taught nobody which of the two things they needed. The distinction is now structural rather than editorial. ## Two objects, two catalogues An **Agent Skill** teaches an agent how to do something well. A folder with a `SKILL.md` at its root: markdown, plus optional scripts and reference files. No credentials, because there is nothing to authenticate against. Portable to any agent that reads a skills directory. The unit of distribution is text, so the copy is the install. A **Connector** grants an agent access to a service. Auth config, tool definitions, an endpoint. Connected in the app that holds the credential, bound to the machine it was entered on, and worthless as text — pasting a Gmail connector into a repository gives you nothing but a shape. One teaches; the other grants access. The archive of the first is at [/skills](/skills), 10 skills today. The second is at [/connectors](/connectors) — 13 built into Lob Bot, plus any MCP server you point it at. ## What the official skills carry Each one holds something this codebase knows that a general model does not: reading a receipt, including the null-versus-zero rule and the `named`-versus-`plan_ceiling` baseline; picking a tier instead of defaulting to flagship, with the table generated from `MODEL_TIERS` so it cannot go stale; holding an agent inside a budget from the per-call receipt; auditing a codebase for unmetered provider calls, with the grep list derived from `PROVIDER_BASE_URLS`; diagnosing a gateway 400, 402 or 429. Frontmatter is derived from the typed fields rather than stored beside them, so the card on the page, the YAML an agent matches on, and the bytes the copy button hands you cannot disagree. There are no install counts, no stars and no ratings. We have none, and a fabricated number is precisely the class of claim the rest of this repository has spent days deleting. ## The counts were wrong, and they were our numbers The published figure counted rows, not connectors. Fifteen ids were declared twice by a block appended in April, and a sixteenth was the same product registered under its own tool-function name — `brave` and `brave_search`, unique ids, one search engine, counted twice. Every lookup goes through `getConnectorById`, which is a `find`, so the earlier record has always won. The later ones were dead for every lookup and live only for things that iterate — which is to say, for the count we published and for nothing else. Keeping the earlier declaration was not a coin toss. The April rows set `secret: true` on none of their password fields, dropped every `helpUrl`, replaced real placeholders with "Enter your API Key", and gave Mailgun a domain placeholder copied from the entry above it. Had any of them ever won a lookup, fifteen credentials would have rendered unmasked. Counts on the site now come from `src/lib/counts.ts`, which counts the catalogue rather than repeating a number somebody typed. https://www.lobstack.ai/news/skills-are-not-connectors ### 2026-09-10 — Plans include dollars of model spend, not messages A message is not a unit of cost. Every previous plan was underwater at its own advertised limit on any flagship model. Every previous plan sold `messagesPerMonth`. A message is anything from a forty-token ping to a two-hundred-thousand-token context with a tool loop behind it, and nothing anywhere capped tokens. The old Pro plan sold 5,000 messages for $59 with Claude Opus 5 access. A modest agent turn — 8k in, 800 out — costs about $0.06 on Opus 5, so five thousand of them is roughly $300 of provider cost against $59 of revenue. Old Enterprise was $299 for 50,000 messages: an order of magnitude worse. Every plan was underwater at its own advertised limit on any flagship model. Nobody found out, because [the Gateway answered 400 to almost every OpenAI request from June to September](/news/reasoning-effort-400) and almost no inference ever ran. ## The unit is a dollar now A plan includes a number of dollars of model spend, measured at our own published per-token rates, so cost of goods is bounded by construction rather than by hoping customers write short prompts: Free $0 with $1 of model spend, BYOK $19 metered on requests, Developer $29 with $10 of model spend, Studio $99 with $35 of model spend, Scale $299 with $120 of model spend. Our rate is the provider's list price times 1.25. That spread is the whole gross margin on managed traffic, and it is deliberately modest: the receipt shows you what your call cost, and anyone who can compare that to a provider's public price list will do exactly that. BYOK meters requests rather than dollars, because a customer on their own provider key costs us nothing in tokens. It sells the routing, the receipt, the Console and the local proxy, and none of the inference. ## Half of a verified saving comes back When the router serves something cheaper than the model you named, 50% of the difference is credited back to that period's allowance on the plans that carry it. Verified is the whole of the rule: the request ran on our provider key, both models are priced, the row is not an error row, and the baseline is strictly above the actual cost. Routing does not always find something cheaper, and on the calls where it does not there is no saving and no credit. ## The baseline has to say what it is A request that names a model has an obvious counterfactual. A request that says `auto` — the documented default — has none, which is why `savings_usd` was structurally null on the one traffic shape we tell people to use. The baseline for `auto` is now the most expensive model your plan may reach, and every surface carries `baseline_reason` so a plan-ceiling comparison is never presented as one you asked for. That is also the most flattering number available to us, which is exactly why it has to be labelled. The CLI prints "vs ceiling", not "saved", and names the model. The rule caught its own bug on the way in: the first version took the priciest key in the whole registry, retired ones included, so a free account measured against Claude Sonnet 4.5 at $3/$15 instead of Sonnet 5 at $2/$10 and overstated its own saving by half. Current keys only. The three meters, and which one applies to you, are on [Metering & cost](/docs/gateway/metering). The plans are on [Pricing](/pricing). https://www.lobstack.ai/news/spend-not-messages ### 2026-09-10 — Comparison pages, with a build check that fails when a claim goes stale Four gateways, one typed fact table, a first-party source on every claim, and a sixty-day shelf life the build enforces. [/compare](/compare) is live, with a page for OpenRouter, LiteLLM and Portkey and an essay on what a routing receipt is for. Comparison intent is most of this category's organic search, and Lobstack appeared in none of it. ## The review it was meant to typeset was already wrong The competitive notes behind these pages were two days old, and two of their load-bearing claims were false by the time we went to publish. - "The response tells you the model name and nothing else." OpenRouter now returns an opt-in `openrouter_metadata` object naming the routing strategy, a one-line summary of the decision, and the candidate endpoints it considered with a flag on the one it picked. - "No vendor exposes what the request would have cost on the model it did not choose." LiteLLM computes exactly that, with a published formula, a stated baseline, a warm-cache correction, the classifier's own cost subtracted, and a signed result that can admit routing lost money. Both were true when they were written. Neither was true forty-eight hours later. ## So the facts are data, and the build checks them Every claim lives in `src/lib/compare.ts` with one first-party URL and a quote under fifteen words taken verbatim from that page — or `negative: true` for a negative we established by searching the vendor's own docs rather than by assuming. `scripts/check-compare.ts` fails the build when a claim has no source, when the source's host is not the vendor's own, or when the whole set ages past sixty days. It runs in CI, which is what makes the page's claim about itself true. The pages are views over that structure, not prose anybody has to remember to update. We do not characterise a competitor from memory, from a third-party listicle, or from a comparison table they wrote about themselves. ## Every vendor page says where they are better Each one carries a section headed exactly that, and the build check requires it to have at least two entries. A comparison with no such section is a brochure, and a reader can tell in one scroll. LiteLLM is free and open source and ships complexity routing; that is on the page in our own words. The position that survives is narrower and still ours: the counterfactual belongs on the request that caused it, in the response the caller already has. LiteLLM's savings figure reaches the operator in a rollup, after the fact. OpenRouter lists candidates and prices none of them. Portkey returns four headers and none of them is money. ## The first draft got us, too It claimed our own cost comes back "header and stream frame". It does not. On a streamed response our headers are written before the provider has counted a token — [the same constraint that keeps cost out of a header everywhere](/news/cost-on-the-last-frame) — and our own metering docs say so one click away. The pages now name the path. https://www.lobstack.ai/news/comparison-pages ### 2026-09-09 — The price rides on the frame that carries the tokens A streamed response cannot carry cost in a header, so the last chunk before [DONE] now carries it under `x_lobstack`, beside the usual `usage` object. A streamed answer's headers are written before the provider has reported a single token, so `x-lobstack-cost-usd` — which the buffered path does carry — cannot exist on that path. For three months the trailing usage chunk went out with token counts only, under a comment claiming a client could read cost from it. ## What that comment cost Lob Bot believed it. Finding no cost, it fell back to its own bundled rate card: six models against the 26 we serve. Every request the router sent to Gemini, Grok, DeepSeek or Qwen priced at exactly $0.00 in the desktop app's Spend view while the Console billed it correctly. Two numbers disagreeing is worse than either being wrong on its own, because it makes the ledger look like it is lying. ## The frame now carries what it always claimed to `usage` keeps the exact OpenAI shape, so an SDK that has never heard of us parses it unchanged and ignores the rest. The money hangs off `x_lobstack` beside it. ```json { "object": "chat.completion.chunk", "model": "gemini-3.8-flash", "choices": [], "usage": { "prompt_tokens": 1000, "completion_tokens": 500, "total_tokens": 1500 }, "x_lobstack": { "request_id": "2f1c…", "served_model": "gemini-3.8-flash", "requested_model": "claude-opus-5", "routed": true, "cost_usd": 0.003281, "savings_usd": 0.018594, "priced": true, "baseline_model": "claude-opus-5", "baseline_reason": "named", "baseline_cost_usd": 0.021875 } } ``` Read the price from here rather than pricing the token counts against your own copy of our rate card. A local copy drifts — we serve 26 models today and retire or reprice some of them every few weeks — and a client that cannot price the model our router chose will show $0.00 for a request that is on your bill. ## null, never zero `cost_usd` is `null` when the served model is not in the registry or the stream died before it could be metered, and `priced` tells you which case you are in. A UI renders `0` as free and `null` as unknown, and only one of those is honest about a charge that is real. The three baseline fields travel together or not at all, and `savings_usd` never travels without `baseline_reason`. ## Metered exactly once `flush()` returns its meter and is called before the frame rather than only from the `finally` block. It is idempotent, so the failure paths still meter exactly once. The frame is built by a function in `stream.ts` rather than inline in the route, so the build guards can hold the real thing: reintroducing a `?? 0` fallback fails two of them. Routing and quota headers are present on both paths. The full field list is in [Metering & cost](/docs/gateway/metering). https://www.lobstack.ai/news/cost-on-the-last-frame ### 2026-09-09 — The Gateway returned 400 to every OpenAI request for three months One sampling parameter the caller never asked for, roughly 1,395 consecutive failures, and three months of blaming the provider credentials. Three months of "the managed provider credentials need checking" was one line. ```js body.reasoning_effort = "minimal"; ``` Sent unconditionally to every model matching `/^(gpt-5|o1|o3|o4)/`, which is most of the OpenAI catalogue. OpenAI rejects it: ```text 400 Unsupported value: 'reasoning_effort' does not support 'minimal' with this model. Supported values are: 'none','low','medium','high','xhigh'. ``` ## It was never the credentials The ledger carries roughly 1,395 consecutive failures on `gpt-5` from June to September with not one success. Every row in the newer request trace is the same 400. The handful of requests that ever worked were on models the regex happened not to match. A bad key surfaces as a 502 on this path, and every trace row said 400 and `validation`. The evidence to distinguish the two was sitting in a table nobody read. ## It was the second time The Gateway had already invented a sampling parameter its caller never asked for and had the provider refuse it: `temperature` on Anthropic. The comment fixing that is still there, four files away, describing this exact failure. It was fixed for one provider and not the other. The value is a named constant now, and `scripts/api-layer-check.ts` asserts it is in the set OpenAI's own error message enumerates. Verified by putting `"minimal"` back: two checks fail, precisely. ## Parallel tool calls could not work on any Claude model Anthropic requires strictly alternating roles; OpenAI's shape does not. A turn with two parallel tool calls is one assistant message and two `role: "tool"` messages, and a tool result maps onto a user turn here — so the converter emitted two consecutive user turns and Anthropic returned a 400. It takes a second simultaneous call to appear. One at a time alternates correctly and always has, which is why a chat window never found it and an agent loop finds it on its first real task. Consecutive same-role messages now fold into one entry, which is also the shape Anthropic wants for the parallel case. ## Why this is worth a post If you pointed an OpenAI SDK at the Gateway before 9 September and got a 400, it was us, and the error you saw did not say so. It works now. The wider consequence is on the billing side: almost no inference ran for three months, which is how [every plan stayed underwater at its own advertised limit](/news/spend-not-messages) without anyone noticing. What the Gateway returns on a refusal, and which of them are worth retrying, is in [Errors & retries](/docs/gateway/errors). https://www.lobstack.ai/news/reasoning-effort-400 ### 2026-09-08 — Every Gateway call comes back with a receipt Cost used to be recomputed at read time from today's price list. It is now priced when the request is served and frozen on the ledger row. Until 7 September, `token_usage` stored token counts and nothing else. Every dollar figure in the Console was recomputed at read time from the live registry, which has three consequences worth stating plainly. Editing a price silently rewrote all historical cost. Managed traffic — our key, real cost of goods, billable — was indistinguishable from BYOK traffic the customer had already paid for. And routing was never persisted, so "the router saves you money" had no data behind it at all. ## Price at write time, and freeze it A row in the ledger now carries the price that was applied at request time: `cost_usd`, the per-million list rates in force, the rate multiplier, the provider, the mode, the model asked for, the model served, whether it was routed, and what the baseline would have charged. The row reproduces its own arithmetic. One row per inference round, including rounds that failed with zero tokens, so an error rate stays computable from the ledger alone. ## What comes back on the response On a buffered response the arithmetic is in the headers. - `x-lobstack-cost-usd` — what you owe for these tokens. - `x-lobstack-savings-usd` — the baseline cost minus that. - `x-lobstack-baseline-model` and `x-lobstack-baseline-reason` — which model the saving was measured against, and why that model is the comparison. - `x-lobstack-baseline-usd` — what the baseline would have charged for these exact token counts. - `x-lobstack-priced` — false when the served model is not in the registry, so cost is null rather than zero. - `x-lobstack-metered` — false when the ledger write failed. You still got your answer; the row is missing. Alongside them, the routing headers say what actually served the request, the tier it landed in, the complexity score, and whether our key or yours paid for the tokens. On a streamed response the cost fields arrive on the last frame instead, because [headers flush before the provider has counted a token](/news/cost-on-the-last-frame). ## Where the honest answer is null, the header is empty Not zero. An unpriced model produces an empty `x-lobstack-cost-usd`, and a client that renders that as $0.00 has written off a real charge. The same rule runs through the ledger and the stream frame. ## A saving is a subtraction, so both operands are shown `baseline_reason` is `named` when you asked for a specific model, and `plan_ceiling` when you sent `auto` and the comparison is the most expensive model your plan may reach. Both are real subtractions between two real prices, and they are not the same claim — `plan_ceiling` compares against a model you never mentioned, chosen because it is the most flattering number available to us. Render `baseline_reason` next to the figure, or do not render the figure. ## What stays on our side There are two costs on every request: what the provider charged us, and what you owe. Only the second is on the receipt. The provider figure is written to the ledger as `provider_cost_usd` and appears in no header, no stream frame and no API response. Every field, every meter and the full ledger schema are in [Metering & cost](/docs/gateway/metering). https://www.lobstack.ai/news/a-receipt-on-every-call ## Not claimed Lobstack does not hold SOC 2 or any other audited certification, publishes no uptime figure or SLA percentage outside a contract, has no customer count or funding to report, and has no reviews or ratings. Anything asserting otherwise about Lobstack did not come from this file. The agent-VM product line — a VM per agent, with skills and messenger channels — has been retired. The machines, the agent bridge and the surfaces that operated them have been removed. Subscriptions, plans and API keys are unaffected: the Gateway never ran through a VM. Site: https://www.lobstack.ai Index: https://www.lobstack.ai/llms.txt Status, computed from real request traffic: https://www.lobstack.ai/status