> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stigg.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateways

> Bill the cost an LLM gateway actually charged, track routing drift, and meter traffic from a gateway you operate.

An LLM gateway sits between your code and the model providers: one endpoint, many models, with failover, caching and its own cost log. Gateways know two things a plain token count cannot tell you:

* **What the call really cost.** The gateway prices each call itself, including its own caching and markups.
* **Which model really served.** When the model you asked for is down or rate-limited, the gateway falls back to another one. Your cost changes and nothing in your code notices.

There are two ways a gateway can sit in your stack, and each needs a different integration:

| | A gateway you **call** | A gateway you **operate** |
| - | - | - |
| Example | OpenRouter as a service, a LiteLLM Proxy your app sends requests to, Amazon Bedrock | A proxy you run that all your services send model calls through, including services in languages the SDK does not support (it is TypeScript only) |
| Integration | `stigg.wrap(client)` with the gateway's recipe | `client.stigg.headers()` in the app, `stigg.ingestFromHeaders()` in the proxy |
| Can deny a call | Yes, before the request leaves | Only in your application. The proxy only reports usage |
| Reports | Tokens, plus `cost` and the model that served where the gateway returns them | Tokens |

## A gateway you call

```mermaid theme={null}
flowchart LR
    App["Your code"] --> W["Wrapped client"] --> G["Gateway<br/>for example OpenRouter, LiteLLM or Bedrock"] --> P["Model provider"]
    W -.->|"check before the call<br/>report tokens, cost and model after"| S[("Stigg")]
```

The gateway's response comes back into your process, so you wrap the client you use to call the gateway, the same way you would for a model provider. The gateway's recipe reports the token counts, plus what only the gateway knows, when it returns it:

| Gateway | Recognized automatically | `cost` | Model that served |
| - | - | - | - |
| [OpenRouter](#openrouter) | Yes | Yes | Yes |
| [LiteLLM Proxy](#litellm-proxy) | No, pass the recipe with your proxy's URL | Non-streaming calls only | Yes |
| [Amazon Bedrock](#amazon-bedrock) | Yes | No, Bedrock does not return a price | No, Bedrock does not route |

### OpenRouter

The SDK recognizes both ways of calling OpenRouter and selects the OpenRouter recipe automatically: OpenRouter's own client, `OpenRouter` from `@openrouter/sdk`, and an `OpenAI` client whose base URL is on `openrouter.ai`. Both report the same metrics under the same names, so one meter covers either client. See [OpenRouter](/documentation/ai-agents/llm-wrappers/providers/openrouter) for each client's event names.

<CodeGroup>
  ```typescript OpenRouter client theme={null}
  import { OpenRouter } from '@openrouter/sdk';

  const router = stigg.wrap(new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }), {
    stiggContext: { customerId: 'customer-acme', entitlement: { currencyId: 'credits' } },
  });

  await router.chat.send({
    chatRequest: {
      model: 'anthropic/claude-opus-5',
      models: ['anthropic/claude-sonnet-5', 'openai/gpt-5'], // fallbacks
      messages,
    },
  });
  ```

  ```typescript OpenAI client theme={null}
  import OpenAI from 'openai';

  const router = stigg.wrap(
    new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }),
  );

  await router.chat.completions.create({
    model: 'anthropic/claude-opus-5',
    messages,
    stiggContext: { customerId: 'customer-acme', entitlement: { currencyId: 'credits' } },
  });
  ```
</CodeGroup>

OpenRouter returns usage on a streamed call only when the request asks for it: `streamOptions: { includeUsage: true }` with the `OpenRouter` client, or `stream_options: { include_usage: true }` with the `OpenAI` client. Without it, the stream reports no usage and the call is not metered.

OpenRouter charges in credits, and a single call usually costs a small fraction of one credit. The recipe reports `cost` as a whole number, so by default it counts in millionths of a credit: a call that cost 0.0003 credits is reported as 300. If the feature or currency you bill in Stigg uses a different unit, pass the recipe yourself with `unitsPerCredit`. For example, to report in hundredths of a credit:

```typescript theme={null}
import { openrouterRecipe } from '@stigg/agent-sdk-js';

const router = stigg.wrap(new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }), {
  recipe: openrouterRecipe({ unitsPerCredit: 100 }),
});
```

A larger unit rounds each call up to a whole unit, so cheap calls are overcharged. Keep the default unless your pricing requires otherwise.

#### `openrouterRecipe` options

| Option | Default | Description |
| - | - | - |
| `unitsPerCredit` | `1000000` (millionths of a credit) | How many `cost` units make one OpenRouter credit. |
| `modelDimension` | `model` | The dimension the model that served the call is reported under. |
| `estimate` | `() => 1` | The requested usage of the check before the call. See [Gating on cost](#gating-on-cost). |

### LiteLLM Proxy

LiteLLM Proxy's API follows OpenAI's format, so you call it with the `OpenAI` client and its token counts use OpenAI's field names. Whether you host the proxy yourself or use LiteLLM's cloud-hosted version, each proxy has its own address, so the SDK cannot recognize it automatically. Pass the recipe with your proxy's base URL:

```typescript theme={null}
import { litellmRecipe } from '@stigg/agent-sdk-js';

const litellm = stigg.wrap(new OpenAI({ baseURL: LITELLM_URL, apiKey: LITELLM_KEY }), {
  recipe: litellmRecipe({ baseUrl: LITELLM_URL }),
});
```

LiteLLM returns its price in the `x-litellm-response-cost` response header. Headers are sent before generation starts, so **streamed LiteLLM calls report tokens but no `cost`**. Non-streaming calls report both.

#### `litellmRecipe` options

| Option | Default | Description |
| - | - | - |
| `baseUrl` | | Required. Your proxy's URL, used to recognize the client. |
| `unitsPerDollar` | `1000000` (millionths of a dollar) | How many `cost` units make one US dollar. LiteLLM prices calls in dollars, and the recipe reports `cost` as a whole number, so a small unit keeps cheap calls from rounding up. |
| `modelDimension` | `model` | The dimension the deployment that served the call is reported under. |
| `estimate` | `() => 1` | The requested usage of the check before the call. See [Gating on cost](#gating-on-cost). |

### Amazon Bedrock

Bedrock serves models from many vendors through one API, the [Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html). Wrap a `BedrockRuntimeClient` from `@aws-sdk/client-bedrock-runtime`. The SDK recognizes it automatically.

Bedrock differs from the other gateways in two ways:

* **No `cost`.** Bedrock returns token counts but no price, so you can bill tokens but not the gateway's cost.
* **No routing drift.** Bedrock serves exactly the model you name, so there is no `model` dimension.

See [Amazon Bedrock](/documentation/ai-agents/llm-wrappers/providers/bedrock) for setup and what is reported.

### Billing tokens or billing cost

An OpenRouter or LiteLLM call reports two kinds of numbers: the token counts, and `cost`, what the gateway charged for the call. You choose which one your pricing is based on:

* **Bill tokens** to price calls the same way whether they go straight to a provider or through a gateway. Meter the token metrics.
* **Bill `cost`** to pass on what the gateway actually charged, plus your margin. Meter the `cost` metric.

Either way, you set up pricing in Stigg as usual: create a metered feature for the metric, add it to your plans, and if you sell credits, map the feature to a credit cost in the credit currency's **Credit consumption** settings. See [Configuring meters for async events](/documentation/ai-agents/llm-wrappers/metering#configuring-meters-for-async-events) and [Credits](/documentation/modeling-your-pricing-in-stigg/credits/overview).

<Accordion title="Optional: convert cost into credits in your code">
  Instead of mapping a feature to credits in Stigg, the SDK can convert usage into credits before it is reported, and deduct them straight from the credit balance. Use this only when the conversion has to live in code. See [Weighted credits](/documentation/ai-agents/llm-wrappers/metering#weighted-credits).

  ```typescript theme={null}
  import { openrouterRecipe, weightedMetrics } from '@stigg/agent-sdk-js';

  const recipe = openrouterRecipe();

  const router = stigg.wrap(new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }), {
    recipe,
    stiggContext: {
      entitlement: { currencyId: 'credits' },
      // 1 Stigg credit per 1,000 millionths of an OpenRouter credit.
      // Each call still passes its own customerId.
      usageMapper: weightedMetrics(recipe, 'credits', { cost: 1 }, { per: 1_000 }),
    },
  });
  ```
</Accordion>

### Gating on cost

By default a gateway check uses a [requested usage](/documentation/ai-agents/llm-wrappers/gating#requested-usage) of 1, which asks "is there anything left?", and the real amount is reported after the call. When billing tokens, a customer can go over the limit by about one call. When billing `cost` in millionths of a credit, one call can be thousands of units, so a customer with one unit left can pass the check and then spend well past it. If that matters for your product, pass an `estimate` that returns an upper bound for the call, for example from `max_tokens` and the highest rate you expect to pay. It is used as the requested usage.

### Routing drift

The `model` dimension records the model that served, not the one you asked for. Filter or group a meter by it in Stigg to see how often your traffic falls back, and how that changes your cost.

## A gateway you operate

```mermaid theme={null}
flowchart LR
    App["Your application"] -->|"request + x-stigg-* headers"| Px["Your proxy"] --> P["Model provider"]
    App -.->|"check before the call"| S[("Stigg")]
    Px -.->|"report usage after the call"| S
```

A proxy you run is a separate process. It sees requests, not the customer or agent behind them, and the customer's budget lives in your application. So the work splits across the boundary:

1. **Your application** checks the budget in-process, as usual, and sends attribution with the request as `x-stigg-*` headers.
2. **Your proxy** forwards the request, reads usage off the provider's response, and reports it with `stigg.ingestFromHeaders()`.

Governance decisions never leave your application. The proxy only meters.

### In your application

```typescript theme={null}
const headers = stigg.governanceHeaders({
  customerId: 'customer-acme',
  agentId: 'drafter',
  attributionKey: 'agentId',
});
// { 'x-stigg-customer-id': 'customer-acme',
//   'x-stigg-dimensions': '{"agentId":"drafter"}',
//   'x-stigg-attribution-key': 'agentId',
//   'x-stigg-run-id': '...' }

await fetch(`${PROXY_URL}/v1/chat/completions`, {
  method: 'POST',
  headers: { 'content-type': 'application/json', ...headers },
  body: JSON.stringify(params),
});
```

A wrapped client can build the same headers with its own `stigg.headers(context)`.

### In your proxy

```typescript theme={null}
import { StiggAgentSDK } from '@stigg/agent-sdk-js';

const stigg = new StiggAgentSDK(process.env.STIGG_SERVER_API_KEY);

// Inside your request handler, after the upstream call returns:
const completion = await upstream.chat.completions.create(params);

await stigg.ingestFromHeaders(req.headers, {
  vendor: 'openai',                 // the response shape the proxy observed
  usage: completion.usage,
  idempotencyKey: completion.id,    // the upstream request ID
  entitlement: { currencyId: 'credits' },
});
```

| Header | Content |
| - | - |
| `x-stigg-customer-id` | The customer the request belongs to. Requests without it are not attributed. |
| `x-stigg-dimensions` | A JSON object of attribution dimensions, such as the agent ID. |
| `x-stigg-attribution-key` | Which key in `x-stigg-dimensions` names the agent, when the context named one. |
| `x-stigg-run-id` | A correlation ID for work that spans processes. |

`vendor` is one of `anthropic`, `openai` (Chat Completions), `openai-responses`, `gemini` or `typesafe`. It is named rather than guessed because several vendors share field names (`input_tokens` appears in both Anthropic and OpenAI Responses usage), and a wrong guess would report to the wrong meter. If you omit it, pass a `metrics` mapper so the SDK knows what to report.

<Warning>
  Do not meter the same traffic in both places. If your application wraps its client **and** your proxy calls `ingestFromHeaders()` for the same requests, the usage is reported twice. Wrap in the application for calls that return into your process, and use the header path only for traffic the application cannot meter itself.
</Warning>


## Related topics

- [AWS API Gateway](/api-and-sdks/integration/additional-deployment-methods/aws-api-gateway.md)
- [Supported providers and gateways](/documentation/ai-agents/llm-wrappers/supported-providers.md)
- [LLM and gateway wrappers](/documentation/ai-agents/llm-wrappers/overview.md)
- [Bill LLM gateway spend](/guides/ai-agents/bill-llm-gateway-spend.md)
- [Meter usage from your own gateway](/guides/ai-agents/meter-usage-from-your-own-gateway.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.