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

# OpenRouter

> Send OpenRouter generations and agent traces into Squasher for AI observability.

Squasher works well with OpenRouter because OpenRouter exposes an OpenAI-compatible API and can provide model, token, and cost details for every request.

You can integrate in two ways:

1. **Use `@squasher-ai/agent` in your app or agent runtime**
2. **Send Langfuse-style batch events to Squasher's AI ingest endpoint**

## Option 1: Agent SDK + OpenRouter

This is the best path when you want session tracking, tool calls, and agent spans in addition to plain generations.

```bash theme={null}
bun add @squasher-ai/agent @ai-sdk/openai ai
```

```typescript theme={null}
import { AgentTelemetryClient, createAiSdkTelemetryIntegration } from "@squasher-ai/agent";
import { createOpenAI } from "@ai-sdk/openai";
import { streamText } from "ai";

const openrouter = createOpenAI({
  apiKey: process.env.OPENROUTER_API_KEY,
  baseURL: "https://openrouter.ai/api/v1",
  headers: {
    "HTTP-Referer": "https://your-app.example",
    "X-Title": "Your App",
  },
  name: "openrouter",
});

const telemetry = new AgentTelemetryClient({
  apiKey: process.env.SQUASHER_API_KEY!,
  projectId: process.env.SQUASHER_PROJECT_ID!,
  endpoint: process.env.SQUASHER_ENDPOINT,
  agentId: "support-bot",
  workflowId: "support-chat",
  environment: process.env.NODE_ENV,
});

const integration = createAiSdkTelemetryIntegration(telemetry, {
  sessionId: "chat_123",
  distinctId: "user_123",
  provider: "openrouter",
  model: "openai/gpt-4.1-nano",
  generationName: "support.reply",
  spanName: "support.run",
  tags: { surface: "support-chat" },
  // Reasoning text is not retained unless explicitly enabled:
  captureReasoning: false,
});

const result = streamText({
  telemetry: {
    isEnabled: true,
    functionId: "support.reply",
    integrations: [integration],
  },
  model: openrouter.chat("openai/gpt-4.1-nano"),
  messages: [{ role: "user", content: "Summarize this issue." }],
  tools: {},
});

result.consumeStream();
```

This captures, per step:

* the generation itself
* prompt, completion, total, **reasoning, and cached input** tokens (from the AI SDK's `usage.outputTokenDetails` / `inputTokenDetails`)
* the model and provider (with the AI SDK's `.chat` / `.responses` suffix stripped)
* **assistant text output** (`text`) and, only with `captureReasoning: true`, provider-supplied **reasoning text** (`reasoningText`)
* **cost in USD**, captured automatically when OpenRouter returns it on `providerMetadata.openrouter.usage.cost` (use [`@openrouter/ai-sdk-provider`](https://www.npmjs.com/package/@openrouter/ai-sdk-provider) or pass `providerOptions: { openrouter: { usage: { include: true } } }` to enable cost in the response). When the provider doesn't report cost, Squasher computes it server-side from your token counts × the public model rate table — no extra config required.
* tool calls and their input/output (sanitized to JSON-safe values, or omitted in `privacyMode`)
* the enclosing session / run

<h2 id="direct-http-ingest-no-sdk">
  Option 2: Direct HTTP ingest (no SDK)
</h2>

If you don't want to install an SDK, POST batch events directly. The endpoint is Langfuse-compatible: trace, generation, span, agent, tool, and event types are all accepted in the same batch and arrive on the **AI -> Sessions / Requests** views the same way an SDK call would.

```http theme={null}
POST https://ingest.squasher.ai/v1/ai/ingest/{project_id}
x-squasher-key: sq_pk_...
content-type: application/json
```

A complete worked example with cost, reasoning, text, and a tool call:

```bash theme={null}
curl -X POST "https://ingest.squasher.ai/v1/ai/ingest/$SQUASHER_PROJECT_ID" \
  -H "x-squasher-key: $SQUASHER_API_KEY" \
  -H "content-type: application/json" \
  --data @- <<'JSON'
{
  "batch": [
    {
      "id": "trace_demo_1",
      "timestamp": "2026-04-26T18:00:00.000Z",
      "type": "trace-create",
      "body": {
        "id": "trace_demo_1",
        "name": "support-chat",
        "sessionId": "chat_demo_1",
        "userId": "user_42",
        "environment": "production"
      }
    },
    {
      "id": "gen_demo_1",
      "timestamp": "2026-04-26T18:00:01.000Z",
      "type": "generation-create",
      "body": {
        "id": "gen_demo_1",
        "traceId": "trace_demo_1",
        "name": "reply",
        "model": "openai/gpt-4.1-nano",
        "provider": "openrouter",
        "startTime": "2026-04-26T18:00:01.000Z",
        "endTime":   "2026-04-26T18:00:02.412Z",
        "completionStartTime": "2026-04-26T18:00:01.180Z",
        "input":  [{ "role": "user", "content": "Why is uptime slow?" }],
        "output": {
          "text": "Looks like the uptime worker hit a 1.6s p95.",
          "reasoning": "Compared service map averages. Uptime stood out at 1,624 ms vs. 257 ms for ingest-server."
        },
        "usageDetails": {
          "input": 9216,
          "output": 255,
          "output_reasoning_tokens": 120,
          "input_cached_tokens": 5000,
          "total": 9471
        },
        "costDetails": {
          "input": 0.00046,
          "output": 0.00077,
          "total": 0.00123
        },
        "metadata": { "feature": "support-chat" }
      }
    },
    {
      "id": "tool_demo_1",
      "timestamp": "2026-04-26T18:00:01.500Z",
      "type": "tool-create",
      "body": {
        "id": "tool_demo_1",
        "traceId": "trace_demo_1",
        "parentObservationId": "gen_demo_1",
        "name": "searchTraces",
        "startTime": "2026-04-26T18:00:01.500Z",
        "endTime":   "2026-04-26T18:00:01.880Z",
        "input":  { "service": "uptime", "windowMinutes": 60 },
        "output": { "rows": 12, "p95Ms": 1624 }
      }
    }
  ]
}
JSON
```

You can post a single event or a full batch. The same endpoint accepts these `type` values:

| `type`                                    | What it represents                   | Maps to                                                                                                                                                                                                 |
| ----------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trace-create`                            | The enclosing session/run trace      | sets `session.id`, `user.id`, environment, version on its child observations                                                                                                                            |
| `generation-create` / `generation-update` | One LLM call                         | becomes a generation observation; supports `usageDetails`, `costDetails`, `model`, `modelParameters`, `input`, `output`, `completionStartTime`, `promptName`, `promptVersion`, `level`, `statusMessage` |
| `span-create` / `span-update`             | A non-LLM step inside an agent run   | becomes an `agent_span`                                                                                                                                                                                 |
| `agent-create`                            | An agent session                     | becomes an `agent_session`                                                                                                                                                                              |
| `tool-create`                             | A tool call                          | becomes a `tool_call` with `input` / `output`                                                                                                                                                           |
| `event-create`                            | A point-in-time event inside a trace | becomes a generic span                                                                                                                                                                                  |

Field reference for `generation-create`:

* `id`, `traceId`, `parentObservationId`
* `name`, `model`, `provider`, `modelParameters`
* `startTime`, `endTime`, `completionStartTime` (ISO 8601)
* `input`, `output` — any JSON. Put your assistant text under `output.text` and reasoning text under `output.reasoning` to surface them in the dashboard.
* `usageDetails` — Langfuse-style map. Squasher reads `input` / `prompt_tokens` / `input_tokens`, `output` / `completion_tokens` / `output_tokens`, `total`, plus `output_reasoning_tokens`, `input_cached_tokens`, `input_cache_read`, `input_cache_write`, `accepted_prediction_tokens`, `rejected_prediction_tokens`.
* `usage` — legacy alias supporting `promptTokens`, `completionTokens`, `totalTokens`, `totalCost`.
* `costDetails` — map with `input`, `output`, `cache_read_input_tokens`, `total`. If you only have a single number, send `{ "total": 0.00123 }`.
* `level` (`DEFAULT` / `WARNING` / `ERROR`), `statusMessage`, `version`, `environment`, `metadata`
* `promptName`, `promptVersion` for prompt-management linking

## OpenRouter-specific tips

* Set `provider` explicitly to `openrouter` when you emit custom events.
* Pass the fully qualified model string (for example `openai/gpt-4.1-nano` or `anthropic/claude-3.5-sonnet`).
* For automatic cost capture through the SDK, either use [`@openrouter/ai-sdk-provider`](https://www.npmjs.com/package/@openrouter/ai-sdk-provider) (which surfaces cost on `providerMetadata.openrouter.usage.cost`) or pass `providerOptions: { openrouter: { usage: { include: true } } }` to `streamText` / `generateText`.
* For raw HTTP ingest, send `costDetails.total` (USD) when you have it.

## Cost without doing anything

You don't have to attach cost yourself. If a generation arrives with token counts but no `costDetails` / `cost_usd` / `gen_ai.response.cost`, Squasher computes the cost server-side from a maintained per-model rate table — the same approach Langfuse and Helicone take. Provider-reported cost always wins over the computed estimate, so if you start sending OpenRouter's `usage.cost` later, the dashboard switches to the exact number with no other changes on your side.

The rates come from [models.dev](https://models.dev), the open-source community-maintained AI model database. It covers \~3,700 models across the major frontier providers (OpenAI, Anthropic, Google, x-ai, DeepSeek, Meta-Llama, Mistral, Cohere, Groq, Fireworks, Together, OpenRouter pass-throughs, and \~100 others) with cached-input rates where the provider supports prompt caching.

Squasher refreshes model rates regularly. Models that are not in the table show cost `0` until the next refresh; if a model you depend on is missing or mispriced, [open a PR on the models.dev repo](https://github.com/sst/models.dev) and the rate flows through automatically.

## Verify

After you send traffic, open your project and go to:

* **AI -> Dashboard** for aggregate cost, request, and latency charts
* **AI -> Requests** for individual generations and tool calls
* **AI -> Sessions** to follow a chat/session timeline
* **AI -> Users** to break usage down by user

## Billing behavior

Squasher counts **completed generations** as AI requests for plan enforcement and billing.

* On free plans, Squasher drops additional AI requests after the monthly included quota is exhausted.
* On paid plans, Squasher keeps ingesting and records overage usage once you go beyond the included monthly AI request quota.

## Related docs

* [Agent telemetry SDK](/sdks/agent)
* [AI observability](/features/ai-observability)
* [Agent Observations API](/api-reference/agent-observations)
