Merge pull request #1916 from useautumn/charlie/gateway-openrouter

Charlie/gateway openrouter
This commit is contained in:
Charlie Lamb
2026-06-12 13:29:48 +01:00
committed by GitHub
36 changed files with 1528 additions and 524 deletions

2
.gitignore vendored
View File

@@ -22,7 +22,7 @@ supabase.sh
tests/
!server/tests
!packages/mcp/tests
!packages/ai-sdk/tests
!packages/gateway/tests
!apps/leaf/tests
!vite/tests
.secrets

View File

@@ -64,7 +64,7 @@ await autumn.balances.trackTokens({
Each token parameter is an exclusive pool — no token should be counted in more than one. `input_tokens` is non-cached text input only (cached tokens go in `cache_read_tokens` / `cache_write_tokens`), and `output_tokens` is text output only (reasoning tokens go in `reasoning_tokens`, audio in `audio_input_tokens` / `audio_output_tokens`). Each pool is billed at the model's published rate for that pool, falling back to the text input/output rate when the model has none.
<Warning>
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The [`@useautumn/ai-sdk` wrapper](/documentation/external-providers/ai-sdk) does this normalization for you.
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The `@useautumn/gateway` wrappers ([AI SDK](/documentation/external-providers/ai-sdk), [OpenRouter](/documentation/external-providers/openrouter)) do this normalization for you.
</Warning>
### Markup Resolution

View File

@@ -64,7 +64,7 @@ await autumn.balances.trackTokens({
Each token parameter is an exclusive pool — no token should be counted in more than one. Each pool is billed at the model's published rate for that pool, falling back to the text input/output rate when the model has none.
<Warning>
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The [`@useautumn/ai-sdk` wrapper](/documentation/external-providers/ai-sdk) does this normalization for you.
If you pass a provider's raw totals (e.g. OpenAI's `prompt_tokens` and `completion_tokens`), subtract the cache and reasoning counts first — otherwise those tokens are billed twice. The `@useautumn/gateway` wrappers ([AI SDK](/documentation/external-providers/ai-sdk), [OpenRouter](/documentation/external-providers/openrouter)) do this normalization for you.
</Warning>
### Markup Resolution

View File

@@ -134,6 +134,7 @@
"documentation/fail-open",
"documentation/rate-limits",
"documentation/external-providers/ai-sdk",
"documentation/external-providers/openrouter",
"documentation/external-providers/convex",
"documentation/external-providers/revenuecat",
"documentation/external-providers/vercel-marketplace"

View File

@@ -201,7 +201,7 @@ curl -X POST "https://api.useautumn.com/v1/balances.track_tokens" \
### Vercel AI SDK integration
If you're using the [Vercel AI SDK](https://sdk.vercel.ai), the `@useautumn/ai-sdk` package can automatically track token usage for every `generateText` or `streamText` call — no manual `trackTokens` calls needed.
If you're using the [Vercel AI SDK](https://sdk.vercel.ai), the `@useautumn/gateway` package can automatically track token usage for every `generateText` or `streamText` call — no manual `trackTokens` calls needed.
<Card
title="Vercel AI SDK Integration"

View File

@@ -3,7 +3,7 @@ title: "Vercel AI SDK"
description: "Automatically track AI token usage with the Vercel AI SDK"
---
The `@useautumn/ai-sdk` package integrates Autumn with the [Vercel AI SDK](https://sdk.vercel.ai), automatically tracking token usage for every `generateText` or `streamText` call. No manual `trackTokens` calls needed.
The `@useautumn/gateway` package integrates Autumn with the [Vercel AI SDK](https://sdk.vercel.ai), automatically tracking token usage for every `generateText` or `streamText` call. No manual `trackTokens` calls needed.
## Setup
@@ -11,19 +11,19 @@ The `@useautumn/ai-sdk` package integrates Autumn with the [Vercel AI SDK](https
<CodeGroup>
```bash npm
npm install @useautumn/ai-sdk
npm install @useautumn/gateway
```
```bash pnpm
pnpm add @useautumn/ai-sdk
pnpm add @useautumn/gateway
```
```bash yarn
yarn add @useautumn/ai-sdk
yarn add @useautumn/gateway
```
```bash bun
bun add @useautumn/ai-sdk
bun add @useautumn/gateway
```
</CodeGroup>
@@ -38,7 +38,7 @@ Use `withAutumn` to wrap any AI SDK language model. It intercepts generate and s
```typescript
import { Autumn } from "autumn-js";
import { anthropic } from "@ai-sdk/anthropic";
import { withAutumn } from "@useautumn/ai-sdk";
import { withAutumn } from "@useautumn/gateway/ai-sdk";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
@@ -120,7 +120,7 @@ const model = withAutumn({
import { Autumn } from "autumn-js";
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { withAutumn } from "@useautumn/ai-sdk";
import { withAutumn } from "@useautumn/gateway/ai-sdk";
const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY! });

View File

@@ -0,0 +1,114 @@
---
title: "OpenRouter"
description: "Automatically track AI token usage with the OpenRouter SDK"
---
The `@useautumn/gateway` package integrates Autumn with the [OpenRouter SDK](https://openrouter.ai/docs), automatically tracking token usage for every `chat.send` call. No manual `trackTokens` calls needed.
<Note>
Using OpenRouter through the Vercel AI SDK (`@openrouter/ai-sdk-provider`)? Use the [AI SDK wrapper](/documentation/external-providers/ai-sdk) with `providerId: "openrouter"` instead.
</Note>
## Setup
#### 1. Install the package
<CodeGroup>
```bash npm
npm install @useautumn/gateway
```
```bash pnpm
pnpm add @useautumn/gateway
```
```bash yarn
yarn add @useautumn/gateway
```
```bash bun
bun add @useautumn/gateway
```
</CodeGroup>
<Note>
Requires `autumn-js` and `@openrouter/sdk` as peer dependencies.
</Note>
#### 2. Wrap your client
Use `withAutumn` to wrap your OpenRouter client. It intercepts `chat.send` calls, enables OpenRouter's [usage accounting](https://openrouter.ai/docs/use-cases/usage-accounting) on every request, reads the token usage from the response, and reports it to Autumn automatically.
```typescript
import { Autumn } from "autumn-js";
import { OpenRouter } from "@openrouter/sdk";
import { withAutumn } from "@useautumn/gateway/openrouter";
const autumn = new Autumn({ secretKey: "am_sk_test_1234" });
const openRouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
const client = withAutumn({
autumn,
openRouter,
customerId: "user_123",
});
```
#### 3. Use as normal
The wrapped client works exactly like a regular OpenRouter client — streaming and non-streaming. Token usage is tracked in the background after each call.
```typescript
// Non-streaming — usage tracked automatically
const result = await client.chat.send({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Explain quantum computing" }],
});
// Streaming — usage tracked when the stream finishes
const stream = await client.chat.send({
model: "anthropic/claude-sonnet-4-5",
messages: [{ role: "user", content: "Write a short poem" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```
## Token pools
The wrapper normalizes OpenRouter's usage accounting into the exclusive token pools that [trackTokens](/api-reference/balances/trackTokens) expects: text input (excluding cached and audio tokens), text output (excluding reasoning tokens), cache reads, cache writes, audio input, and reasoning tokens. Each pool is billed at the model's published rate.
The model is reported to Autumn as `openrouter/<slug>` (e.g. `openrouter/openai/gpt-4o`), using the resolved model from the response — so router aliases like `openrouter/auto` bill against the model that actually served the request. Autumn prices usage with OpenRouter's rates from [Models.dev](https://models.dev), and OpenRouter's own reported cost is attached to each event as the `openrouter_cost` property for reconciliation.
## Options
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `autumn` | `Autumn` | Yes | Your Autumn SDK client instance |
| `openRouter` | `OpenRouter` | Yes | The OpenRouter SDK client to wrap |
| `customerId` | `string` | Yes | The Autumn customer ID to attribute usage to |
| `featureId` | `string` | No | Target a specific AI credit system feature. Auto-detected if you only have one |
| `entityId` | `string` | No | Entity ID for entity-scoped balance tracking |
| `properties` | `Record<string, unknown>` | No | Additional properties to attach to each usage event |
## Manual tracking
If you consume OpenRouter through a path the wrapper doesn't cover (e.g. `callModel`, or raw `fetch` against the REST API), use `trackOpenRouterUsage` directly with the response's usage object — it accepts both the SDK's camelCase models and the raw snake_case API shape:
```typescript
import { trackOpenRouterUsage } from "@useautumn/gateway/openrouter";
await trackOpenRouterUsage({
autumn,
customerId: "user_123",
model: response.model, // e.g. "openai/gpt-4o"
usage: response.usage,
});
```
<Tip>
Tracking failures are caught and logged to the console — they won't break your AI features. Check your server logs if usage isn't appearing in Autumn.
</Tip>

View File

@@ -181,23 +181,6 @@
"typescript": "^6.0.2",
},
},
"packages/ai-sdk": {
"name": "@useautumn/ai-sdk",
"version": "0.0.1",
"dependencies": {
"@ai-sdk/provider": "^3.0.0",
},
"devDependencies": {
"@types/node": "^24.9.1",
"@typescript/native-preview": "catalog:",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*",
},
},
"packages/atmn": {
"name": "atmn",
"version": "1.1.8",
@@ -319,6 +302,28 @@
"react",
],
},
"packages/gateway": {
"name": "@useautumn/gateway",
"version": "0.0.1",
"dependencies": {
"@ai-sdk/provider": "^3.0.0",
},
"devDependencies": {
"@types/node": "^24.9.1",
"@typescript/native-preview": "catalog:",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
},
"peerDependencies": {
"@openrouter/sdk": "*",
"ai": "^6.0.116",
"autumn-js": "*",
},
"optionalPeers": [
"@openrouter/sdk",
"ai",
],
},
"packages/ksuid": {
"name": "@autumn/ksuid",
"version": "1.0.0",
@@ -2790,7 +2795,7 @@
"@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="],
"@useautumn/ai-sdk": ["@useautumn/ai-sdk@workspace:packages/ai-sdk"],
"@useautumn/gateway": ["@useautumn/gateway@workspace:packages/gateway"],
"@useautumn/sdk": ["@useautumn/sdk@workspace:packages/sdk"],
@@ -7066,7 +7071,7 @@
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"@useautumn/ai-sdk/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@useautumn/gateway/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@useautumn/sdk/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],

View File

@@ -12,7 +12,6 @@
"duplicates"
],
"ignoreWorkspaces": [
"packages/ai-sdk",
"packages/atmn",
"packages/autumn-js",
"packages/mcp",
@@ -82,6 +81,10 @@
"entry": ["tests/**/*.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts"]
},
"packages/gateway": {
"entry": ["tests/**/*.test.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts"]
},
"packages/ksuid": {
"project": ["src/**/*.ts"]
},

View File

@@ -1,7 +1,7 @@
lockVersion: 2.0.0
id: 05940b80-1ef8-40f4-9878-822fb2792070
management:
docChecksum: 473b520097d579f3f299da9235bea250
docChecksum: ceb7be5c31c606bdc2889dc1ebd94207
docVersion: 2.3.0
speakeasyVersion: 1.762.0
generationVersion: 2.882.0

View File

@@ -23,7 +23,7 @@
"packages/openapi",
"packages/ksuid",
"packages/stripe-sync",
"packages/ai-sdk"
"packages/gateway"
],
"catalog": {
"stripe": "19.3.0-beta.1",
@@ -139,7 +139,7 @@
"site": "cd apps/website && bun dev && cd ../..",
"docs": "bun -F @autumn/docs dev",
"docs:build": "bun -F @autumn/docs build",
"ts": "turbo run ts --filter=@autumn/server --filter=autumn-js --filter=@autumn/openapi --filter=atmn --filter=checkout --filter=@useautumn/ai-sdk --filter=@autumn/auth --filter=@autumn/mcp --filter=@autumn/leaf",
"ts": "turbo run ts --filter=@autumn/server --filter=autumn-js --filter=@autumn/openapi --filter=atmn --filter=checkout --filter=@useautumn/gateway --filter=@autumn/auth --filter=@autumn/mcp --filter=@autumn/leaf",
"kill:ts": "while pgrep -f tsgo > /dev/null; do pkill -9 -f tsgo; sleep 0.1; done",
"atmn:build": "bun -F atmn build",
"openapi:ts": "bun -F @autumn/openapi ts",

View File

@@ -1,253 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@useautumn/ai-sdk",
"devDependencies": {
"@types/node": "^24.9.1",
"tsup": "^8.4.0",
"typescript": "^5.8.3",
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*",
},
},
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"autumn-js": ["autumn-js@1.0.5", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "express": "^5.2.1", "hono": "^4.0.0", "next": "^14.0.0 || ^15.0.0", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["better-auth", "better-call", "express", "hono", "next", "react"] }, "sha512-Xh4hqx7EO+hilVjCuilLjUt5iw6RP8vxKmkKmwwHM8WhrHH4StAol17Qis9FtZ/ixeHfe4TOFojzgLhP74kETw=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="],
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"rou3": ["rou3@0.6.3", "", {}, "sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
}
}

View File

@@ -1,40 +0,0 @@
{
"name": "@useautumn/ai-sdk",
"version": "0.0.1",
"type": "module",
"description": "AI SDK for Autumn",
"main": "./dist/sdk/index.cjs",
"module": "./dist/sdk/index.js",
"types": "./dist/sdk/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"exports": {
".": {
"types": "./dist/sdk/index.d.ts",
"require": "./dist/sdk/index.cjs",
"import": "./dist/sdk/index.js"
}
},
"scripts": {
"ts": "bunx tsgo --noEmit --skipLibCheck",
"test": "bun test tests/unit",
"build": "rm -rf dist && tsup",
"prepublishOnly": "bun run build"
},
"dependencies": {
"@ai-sdk/provider": "^3.0.0"
},
"peerDependencies": {
"ai": "^6.0.116",
"autumn-js": "*"
},
"devDependencies": {
"@types/node": "^24.9.1",
"@typescript/native-preview": "catalog:",
"tsup": "^8.4.0",
"typescript": "^5.8.3"
}
}

View File

@@ -1,107 +0,0 @@
import type { LanguageModelV3 } from "@ai-sdk/provider";
import { type LanguageModelMiddleware, wrapLanguageModel } from "ai";
import { normalizeUsage, type TokenPools, type UsageLike } from "./usage.js";
export type { TokenPools, UsageLike } from "./usage.js";
type TrackTokensParams = TokenPools & {
customerId: string;
modelId: string;
featureId?: string;
entityId?: string;
properties?: Record<string, unknown>;
};
/** Structural view of the autumn-js client; older versions may not ship balances.trackTokens. */
export type AutumnClient = {
balances?: {
trackTokens?: (params: TrackTokensParams) => Promise<unknown>;
};
};
export type WithAutumnOptions = {
/** Autumn SDK client instance. */
autumn: AutumnClient;
/** The AI SDK language model to wrap. */
model: LanguageModelV3;
/** The Autumn customer ID to attribute usage to. */
customerId: string;
/** Override the provider prefix used in the model name (e.g. "openrouter", "custom"). Falls back to `model.provider`. */
providerId?: string;
/** Target a specific AI credit system feature. Auto-detected if omitted. */
featureId?: string;
/** Entity ID for entity-scoped balance tracking. */
entityId?: string;
/** Additional properties to attach to each usage event. */
properties?: Record<string, unknown>;
};
export const withAutumn = ({
autumn,
model,
customerId,
providerId,
featureId,
entityId,
properties,
}: WithAutumnOptions): LanguageModelV3 => {
const modelName = `${providerId ?? model.provider}/${model.modelId}`;
const trackUsage = async (usage: UsageLike) => {
try {
const trackTokens = autumn.balances?.trackTokens;
if (!trackTokens) {
throw new Error(
"autumn-js client does not support balances.trackTokens — upgrade autumn-js.",
);
}
await trackTokens({
...normalizeUsage(usage, modelName),
customerId,
modelId: modelName,
featureId,
entityId,
properties,
});
} catch (error) {
console.error("[Autumn Tracking] Failed to track usage:", error);
}
};
const middleware: LanguageModelMiddleware = {
specificationVersion: "v3",
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate();
await trackUsage(result.usage as UsageLike);
return result;
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream();
let trackingPromise: Promise<void> | undefined;
type StreamChunk = typeof stream extends ReadableStream<infer T>
? T
: never;
const transformStream = new TransformStream<StreamChunk, StreamChunk>({
transform(chunk, controller) {
if (chunk.type === "finish" && chunk.usage) {
trackingPromise = trackUsage(chunk.usage as UsageLike);
}
controller.enqueue(chunk);
},
async flush() {
await trackingPromise;
},
});
return {
stream: stream.pipeThrough(transformStream),
...rest,
};
},
};
return wrapLanguageModel({ model, middleware });
};

View File

@@ -1,12 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
"sdk/index": "src/index.ts",
},
format: ["cjs", "esm"],
dts: true,
splitting: false,
sourcemap: false,
clean: true,
});

View File

@@ -0,0 +1,51 @@
{
"name": "@useautumn/gateway",
"version": "0.0.1",
"type": "module",
"description": "Autumn usage tracking adapters for AI SDKs and gateways (Vercel AI SDK, OpenRouter)",
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"exports": {
"./ai-sdk": {
"types": "./dist/ai-sdk/index.d.ts",
"require": "./dist/ai-sdk/index.cjs",
"import": "./dist/ai-sdk/index.js"
},
"./openrouter": {
"types": "./dist/openrouter/index.d.ts",
"require": "./dist/openrouter/index.cjs",
"import": "./dist/openrouter/index.js"
}
},
"scripts": {
"ts": "bunx tsgo --noEmit --skipLibCheck",
"test": "bun test tests/unit",
"build": "rm -rf dist && tsup",
"prepublishOnly": "bun run build"
},
"dependencies": {
"@ai-sdk/provider": "^3.0.0"
},
"peerDependencies": {
"@openrouter/sdk": "*",
"ai": "^6.0.116",
"autumn-js": "*"
},
"peerDependenciesMeta": {
"@openrouter/sdk": {
"optional": true
},
"ai": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^24.9.1",
"@typescript/native-preview": "catalog:",
"tsup": "^8.4.0",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,67 @@
import type { LanguageModelV3 } from "@ai-sdk/provider";
import { type LanguageModelMiddleware, wrapLanguageModel } from "ai";
import { type AutumnTrackingOptions, createTracker } from "../shared/track.js";
import { normalizeUsage, type UsageLike } from "./usage.js";
export type { AutumnClient, AutumnTrackingOptions } from "../shared/track.js";
export type { TokenPools } from "../shared/usage.js";
export type { UsageLike } from "./usage.js";
export type WithAutumnOptions = AutumnTrackingOptions & {
/** The AI SDK language model to wrap. */
model: LanguageModelV3;
/** Override the provider prefix used in the model name (e.g. "openrouter", "custom"). Falls back to `model.provider`. */
providerId?: string;
};
export const withAutumn = ({
model,
providerId,
...tracking
}: WithAutumnOptions): LanguageModelV3 => {
const modelName = `${providerId ?? model.provider}/${model.modelId}`;
const track = createTracker(tracking);
const trackUsage = (usage: UsageLike) =>
track(() => ({
pools: normalizeUsage(usage, modelName),
modelId: modelName,
}));
const middleware: LanguageModelMiddleware = {
specificationVersion: "v3",
wrapGenerate: async ({ doGenerate }) => {
const result = await doGenerate();
await trackUsage(result.usage as UsageLike);
return result;
},
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream();
let trackingPromise: Promise<void> | undefined;
type StreamChunk = typeof stream extends ReadableStream<infer T>
? T
: never;
const transformStream = new TransformStream<StreamChunk, StreamChunk>({
transform(chunk, controller) {
if (chunk.type === "finish" && chunk.usage) {
trackingPromise = trackUsage(chunk.usage as UsageLike);
}
controller.enqueue(chunk);
},
async flush() {
await trackingPromise;
},
});
return {
stream: stream.pipeThrough(transformStream),
...rest,
};
},
};
return wrapLanguageModel({ model, middleware });
};

View File

@@ -1,3 +1,9 @@
import {
poolsFromParts,
type TokenParts,
type TokenPools,
} from "../shared/usage.js";
type NestedTokens = {
total?: number | null;
noCache?: number | null;
@@ -28,14 +34,6 @@ export type UsageLike = {
reasoningTokens?: number | null;
};
export type TokenPools = {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
reasoningTokens: number;
};
const flatCount = (value: LegacyCount | undefined): number | undefined =>
typeof value === "number" ? value : (value?.total ?? undefined);
@@ -43,7 +41,7 @@ const isNested = (
value: number | NestedTokens | null | undefined,
): value is NestedTokens => value != null && typeof value === "object";
const toParts = (usage: UsageLike) => {
const toParts = (usage: UsageLike): TokenParts => {
const input = usage.inputTokens;
const output = usage.outputTokens;
@@ -75,43 +73,8 @@ const toParts = (usage: UsageLike) => {
};
};
const clamp = (value: number) => Math.max(0, value);
/** Splits provider usage into exclusive token pools; throws if the provider returned no usable counts. */
export const normalizeUsage = (
usage: UsageLike,
modelName: string,
): TokenPools => {
const parts = toParts(usage);
const required = (
value: number | null | undefined,
label: string,
): number => {
if (value == null) {
throw new Error(
`[Autumn] ${label} token usage was not returned by the model provider (${modelName}). This provider may not support usage tracking.`,
);
}
return value;
};
const textInput =
parts.textInput ??
(parts.totalInput != null
? parts.totalInput - parts.cacheRead - parts.cacheWrite
: undefined);
const textOutput =
parts.textOutput ??
(parts.totalOutput != null
? parts.totalOutput - parts.reasoning
: undefined);
return {
inputTokens: clamp(required(textInput, "Input")),
outputTokens: clamp(required(textOutput, "Output")),
cacheReadTokens: clamp(parts.cacheRead),
cacheWriteTokens: clamp(parts.cacheWrite),
reasoningTokens: clamp(parts.reasoning),
};
};
): TokenPools => poolsFromParts(toParts(usage), modelName);

View File

@@ -0,0 +1,318 @@
import {
type AutumnTrackingOptions,
createTracker,
} from "../shared/track.js";
import { normalizeOpenRouterUsage, type OpenRouterUsageLike } from "./usage.js";
export type { AutumnClient, AutumnTrackingOptions } from "../shared/track.js";
export type { TokenPools } from "../shared/usage.js";
export type { OpenRouterUsageLike } from "./usage.js";
/** OpenRouter reports its own USD charge when usage accounting is on — attach it to the event. */
const withCost = (
properties: Record<string, unknown> | undefined,
cost: number | null | undefined,
) => (cost == null ? properties : { ...properties, openrouter_cost: cost });
type ChatBody = {
model?: string;
usage?: { include?: boolean } | null;
[key: string]: unknown;
};
/**
* @openrouter/sdk >= 0.12 wraps the chat body in `chatRequest` (with header
* params alongside); earlier shapes and the raw API are flat.
*/
type ChatSendRequest = ChatBody & {
chatRequest?: ChatBody;
};
/** Both non-streaming results and stream chunks carry the resolved model slug and (on the final chunk) usage. */
type UsageCarrier = {
model?: string | null;
usage?: OpenRouterUsageLike | null;
};
/**
* Structural view of the SDK's hooks surface. Standalone funcs (e.g. the
* responses API behind @openrouter/agent's callModel) bypass instance
* methods entirely, so usage for those flows is captured with an
* afterSuccess hook instead of a method proxy. Response is kept structural
* so the package works against DOM, undici and Bun fetch types alike.
*/
type ResponseLike = {
headers: { get(name: string): string | null };
clone(): ResponseLike;
text(): Promise<string>;
json(): Promise<unknown>;
};
type HookCapableClient = {
_options?: {
hooks?: {
registerAfterSuccessHook?: (hook: {
afterSuccess: <R extends ResponseLike>(
ctx: { operationID: string },
response: R,
) => R | Promise<R>;
}) => void;
};
};
};
/** Operations tracked via the afterSuccess hook; chat.send is handled by the method proxy. */
const HOOKED_OPERATIONS = new Set(["createResponses"]);
/** In-flight usage captures per wrapped client (streaming captures are fire-and-forget). */
const pendingTracking = new WeakMap<object, Set<Promise<void>>>();
/**
* Resolves once all in-flight Autumn usage tracking for a wrapped client has
* settled. Streaming responses (including @openrouter/agent's callModel,
* which always streams internally) are tracked in the background — await
* this before reading balances that must reflect the calls just made.
*/
export const trackingSettled = async (openRouter: object): Promise<void> => {
const pending = pendingTracking.get(openRouter);
while (pending && pending.size > 0) {
await Promise.allSettled([...pending]);
}
};
const findCarrier = (value: unknown): UsageCarrier | undefined => {
if (value == null || typeof value !== "object") {
return;
}
const record = value as UsageCarrier & { response?: unknown };
if (record.usage && record.model) {
return record;
}
// Responses stream events nest the result under `response`.
return findCarrier(record.response);
};
const lastCarrierFromSse = (body: string): UsageCarrier | undefined => {
let carrier: UsageCarrier | undefined;
for (const line of body.split("\n")) {
if (!line.startsWith("data:")) {
continue;
}
try {
carrier = findCarrier(JSON.parse(line.slice(5).trim())) ?? carrier;
} catch {
// Ignore non-JSON SSE payloads like "[DONE]".
}
}
return carrier;
};
/** Structural view of the @openrouter/sdk client — only chat.send is intercepted. */
export type OpenRouterLike = {
chat: {
// biome-ignore lint/suspicious/noExplicitAny: param contravariance — `any` lets any concrete send signature satisfy this structurally.
send: (request: any, ...rest: any[]) => Promise<unknown>;
};
};
export type WithAutumnOptions<T extends OpenRouterLike> =
AutumnTrackingOptions & {
/** The @openrouter/sdk client to wrap. */
openRouter: T;
};
const toModelId = (slug: string): string =>
slug.startsWith("openrouter/") ? slug : `openrouter/${slug}`;
export type TrackOpenRouterUsageOptions = AutumnTrackingOptions & {
/** Usage object from an OpenRouter response (SDK model or raw API shape). */
usage: OpenRouterUsageLike;
/** OpenRouter model slug, e.g. "openai/gpt-4o". */
model: string;
};
/** Manual escape hatch for consumption patterns the wrapped client doesn't cover (e.g. callModel). */
export const trackOpenRouterUsage = ({
usage,
model,
...tracking
}: TrackOpenRouterUsageOptions): Promise<void> => {
const modelId = toModelId(model);
return createTracker(tracking)(() => ({
pools: normalizeOpenRouterUsage(usage, modelId),
modelId,
properties: withCost(tracking.properties, usage.cost),
}));
};
const isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> =>
value != null &&
typeof value === "object" &&
Symbol.asyncIterator in value &&
typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === "function";
const hasUsage = (value: unknown): value is UsageCarrier & { usage: object } =>
value != null &&
typeof value === "object" &&
"usage" in value &&
(value as UsageCarrier).usage != null;
export const withAutumn = <T extends OpenRouterLike>(
options: WithAutumnOptions<T>,
): T => {
const { openRouter } = options;
const track = createTracker(options);
const trackCarrier = (carrier: UsageCarrier, requestModel?: string) =>
track(() => {
// Pricing is configured against the slug the caller requested;
// providers may resolve it to a dated snapshot (e.g.
// anthropic/claude-5-fable-20260609) that models.dev doesn't
// list. Router pseudo-models (openrouter/auto) only resolve
// server-side, so those fall back to the response slug.
const requested = requestModel?.endsWith("/auto")
? undefined
: requestModel;
const slug = requested ?? carrier.model ?? requestModel;
if (!slug) {
throw new Error(
"[Autumn] OpenRouter response did not include a model slug.",
);
}
const modelId = toModelId(slug);
const usage = carrier.usage ?? {};
return {
pools: normalizeOpenRouterUsage(usage, modelId),
modelId,
properties: withCost(options.properties, usage.cost),
};
});
const wrapStream = <S extends object>(
stream: S,
requestModel?: string,
): S => {
let tracked = false;
// The SDK's stream is re-consumable; the guard keeps repeat iteration from double-tracking.
async function* iterate(): AsyncGenerator<unknown> {
let finalCarrier: UsageCarrier | undefined;
try {
for await (const chunk of stream as AsyncIterable<unknown>) {
if (hasUsage(chunk)) {
finalCarrier = chunk;
}
yield chunk;
}
} finally {
if (finalCarrier && !tracked) {
tracked = true;
await trackCarrier(finalCarrier, requestModel);
}
}
}
return new Proxy(stream, {
get(target, prop, receiver) {
if (prop === Symbol.asyncIterator) {
return () => iterate();
}
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
});
};
const send = async (
request: ChatSendRequest,
...rest: unknown[]
): Promise<unknown> => {
// Usage accounting must be on for OpenRouter to return token counts
// and cost. The body lives under `chatRequest` on @openrouter/sdk
// >= 0.12 and at the top level on older shapes.
const body = request.chatRequest ?? request;
const bodyWithUsage: ChatBody = {
...body,
usage: { ...body.usage, include: true },
};
const requestWithUsage: ChatSendRequest = request.chatRequest
? { ...request, chatRequest: bodyWithUsage }
: bodyWithUsage;
const result = await openRouter.chat.send(requestWithUsage, ...rest);
if (isAsyncIterable(result)) {
return wrapStream(result, body.model);
}
await trackCarrier(result as UsageCarrier, body.model);
return result;
};
// Capture usage from API paths the method proxy can't see (the responses
// API used by @openrouter/agent's callModel — each turn fires the hook).
const captureHookedResponse = async (
response: ResponseLike,
contentType: string,
): Promise<void> => {
let carrier: UsageCarrier | undefined;
if (contentType.includes("text/event-stream")) {
carrier = lastCarrierFromSse(await response.text());
} else if (contentType.includes("json")) {
carrier = findCarrier(await response.json());
}
if (carrier) {
await trackCarrier(carrier);
}
};
const pending = new Set<Promise<void>>();
pendingTracking.set(openRouter, pending);
(openRouter as HookCapableClient)._options?.hooks?.registerAfterSuccessHook?.(
{
afterSuccess: (ctx, response) => {
if (!HOOKED_OPERATIONS.has(ctx.operationID)) {
return response;
}
const contentType = response.headers.get("content-type") ?? "";
// The clone keeps the SDK's own body read untouched.
const capture = captureHookedResponse(
response.clone(),
contentType,
).catch((error) => {
console.error("[Autumn Tracking] Failed to track usage:", error);
});
pending.add(capture);
capture.finally(() => pending.delete(capture));
// Await JSON captures so balances are settled when the call
// returns; streams can't be awaited without buffering them —
// callers needing settled balances use trackingSettled().
return contentType.includes("text/event-stream")
? response
: capture.then(() => response);
},
},
);
const wrappedChat = new Proxy(openRouter.chat, {
get(target, prop, receiver) {
if (prop === "send") {
return send;
}
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
});
const wrapped = new Proxy(openRouter, {
get(target, prop, receiver) {
if (prop === "chat") {
return wrappedChat;
}
const value = Reflect.get(target, prop, receiver);
return typeof value === "function" ? value.bind(target) : value;
},
});
// trackingSettled accepts either the wrapped or the underlying client.
pendingTracking.set(wrapped, pending);
return wrapped;
};

View File

@@ -0,0 +1,87 @@
import { poolsFromParts, type TokenPools } from "../shared/usage.js";
type PromptDetails = {
cachedTokens?: number | null;
cached_tokens?: number | null;
cacheWriteTokens?: number | null;
cache_write_tokens?: number | null;
audioTokens?: number | null;
audio_tokens?: number | null;
};
type CompletionDetails = {
reasoningTokens?: number | null;
reasoning_tokens?: number | null;
};
/**
* Lenient view over OpenRouter usage across all its surfaces: chat
* completions (prompt/completion naming) and the responses API
* (input/output naming), in SDK camelCase or raw snake_case.
*/
export type OpenRouterUsageLike = {
promptTokens?: number | null;
prompt_tokens?: number | null;
inputTokens?: number | null;
input_tokens?: number | null;
completionTokens?: number | null;
completion_tokens?: number | null;
outputTokens?: number | null;
output_tokens?: number | null;
promptTokensDetails?: PromptDetails | null;
prompt_tokens_details?: PromptDetails | null;
inputTokensDetails?: PromptDetails | null;
input_tokens_details?: PromptDetails | null;
completionTokensDetails?: CompletionDetails | null;
completion_tokens_details?: CompletionDetails | null;
outputTokensDetails?: CompletionDetails | null;
output_tokens_details?: CompletionDetails | null;
/** OpenRouter's own charge in USD credits, present when usage accounting is enabled. */
cost?: number | null;
};
/** Splits OpenRouter usage into exclusive token pools; prompt/completion totals are inclusive, so detail pools are subtracted out. */
export const normalizeOpenRouterUsage = (
usage: OpenRouterUsageLike,
modelName: string,
): TokenPools => {
const promptDetails =
usage.promptTokensDetails ??
usage.prompt_tokens_details ??
usage.inputTokensDetails ??
usage.input_tokens_details;
const completionDetails =
usage.completionTokensDetails ??
usage.completion_tokens_details ??
usage.outputTokensDetails ??
usage.output_tokens_details;
const promptTotal =
usage.promptTokens ??
usage.prompt_tokens ??
usage.inputTokens ??
usage.input_tokens;
const completionTotal =
usage.completionTokens ??
usage.completion_tokens ??
usage.outputTokens ??
usage.output_tokens;
return poolsFromParts(
{
totalInput: promptTotal,
totalOutput: completionTotal,
cacheRead: promptDetails?.cachedTokens ?? promptDetails?.cached_tokens ?? 0,
cacheWrite:
promptDetails?.cacheWriteTokens ??
promptDetails?.cache_write_tokens ??
0,
reasoning:
completionDetails?.reasoningTokens ??
completionDetails?.reasoning_tokens ??
0,
audioInput: promptDetails?.audioTokens ?? promptDetails?.audio_tokens ?? 0,
},
modelName,
);
};

View File

@@ -0,0 +1,155 @@
import type { TokenPools } from "./usage.js";
export type TrackTokensParams = TokenPools & {
customerId: string;
modelId: string;
featureId?: string;
entityId?: string;
properties?: Record<string, unknown>;
};
/** Tracking options every adapter shares. */
export type AutumnTrackingOptions = {
/**
* Autumn SDK client instance. When omitted, a minimal fetch client is
* created from AUTUMN_API_KEY (or AUTUMN_SECRET_KEY).
*/
autumn?: AutumnClient;
/** The Autumn customer ID to attribute usage to. */
customerId: string;
/** Target a specific AI credit system feature. Auto-detected if omitted. */
featureId?: string;
/** Entity ID for entity-scoped balance tracking. */
entityId?: string;
/** Additional properties to attach to each usage event. */
properties?: Record<string, unknown>;
};
export type TrackedEvent = {
pools: TokenPools;
modelId: string;
/** Overrides the options-level properties when set. */
properties?: Record<string, unknown>;
};
/**
* Structural view of the Autumn SDK client. @useautumn/sdk exposes
* trackTokens at the client root; balances.trackTokens is accepted for
* clients that namespace it. `balances` stays `unknown` so SDK namespace
* classes without trackTokens still satisfy the type. Older clients may
* ship neither shape.
*/
export type AutumnClient = {
trackTokens?: (params: TrackTokensParams) => Promise<unknown>;
balances?: unknown;
};
type TrackTokensCarrier = {
trackTokens?: (params: TrackTokensParams) => Promise<unknown>;
};
/** Wire keys for the fallback fetch client — mirrors the SDK's outbound mapping. */
const WIRE_KEYS: Record<string, string> = {
customerId: "customer_id",
entityId: "entity_id",
featureId: "feature_id",
modelId: "model_id",
inputTokens: "input_tokens",
outputTokens: "output_tokens",
cacheReadTokens: "cache_read_tokens",
cacheWriteTokens: "cache_write_tokens",
audioInputTokens: "audio_input_tokens",
audioOutputTokens: "audio_output_tokens",
reasoningTokens: "reasoning_tokens",
};
const toWire = (params: TrackTokensParams) =>
Object.fromEntries(
Object.entries(params)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => [WIRE_KEYS[key] ?? key, value]),
);
/** Minimal fetch client for POST /v1/balances.track_tokens, keyed from env. */
const envClient = (): AutumnClient => {
const env = typeof process === "undefined" ? undefined : process.env;
const secretKey = env?.AUTUMN_API_KEY ?? env?.AUTUMN_SECRET_KEY;
// Optional override; virtually everyone is on the default
const baseUrl = env?.AUTUMN_BASE_URL ?? "https://api.useautumn.com";
return {
trackTokens: async (params: TrackTokensParams) => {
// Thrown here so the miss flows through the usual swallow-and-log path
if (!secretKey) {
throw new Error(
"[Autumn] No autumn client was passed and AUTUMN_API_KEY is not set.",
);
}
const response = await fetch(`${baseUrl}/v1/balances.track_tokens`, {
method: "POST",
headers: {
authorization: `Bearer ${secretKey}`,
"content-type": "application/json",
},
body: JSON.stringify(toWire(params)),
});
if (!response.ok) {
throw new Error(
`track_tokens failed (${response.status}): ${await response.text()}`,
);
}
return response.json();
},
};
};
/**
* Binds tracking options once; each call resolves its event lazily inside
* trackTokenUsage so resolution errors are swallowed with the rest.
*/
export const createTracker = ({
autumn = envClient(),
customerId,
featureId,
entityId,
properties,
}: AutumnTrackingOptions) =>
(getEvent: () => TrackedEvent): Promise<void> =>
trackTokenUsage({
autumn,
getParams: () => {
const event = getEvent();
return {
...event.pools,
customerId,
modelId: event.modelId,
featureId,
entityId,
properties: event.properties ?? properties,
};
},
});
/** Tracking failures (including getParams throwing) are logged, never thrown into the AI response path. */
export const trackTokenUsage = async ({
autumn,
getParams,
}: {
autumn: AutumnClient;
getParams: () => TrackTokensParams;
}): Promise<void> => {
try {
// Bind so class-based SDK clients keep their `this` when invoked.
const balances = autumn.balances as TrackTokensCarrier | undefined;
const trackTokens =
balances?.trackTokens?.bind(balances) ?? autumn.trackTokens?.bind(autumn);
if (!trackTokens) {
throw new Error(
"Autumn client does not support trackTokens — upgrade @useautumn/sdk.",
);
}
await trackTokens(getParams());
} catch (error) {
console.error("[Autumn Tracking] Failed to track usage:", error);
}
};

View File

@@ -0,0 +1,77 @@
/** Mutually exclusive token pools — each pool is priced at its own rate server-side. */
export type TokenPools = {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheWriteTokens: number;
reasoningTokens: number;
audioInputTokens?: number;
audioOutputTokens?: number;
};
export const clamp = (value: number) => Math.max(0, value);
export const requiredCount = (
value: number | null | undefined,
label: string,
modelName: string,
): number => {
if (value == null) {
throw new Error(
`[Autumn] ${label} token usage was not returned by the model provider (${modelName}). This provider may not support usage tracking.`,
);
}
return value;
};
/**
* Provider-agnostic intermediate: inclusive totals plus whichever detail
* pools the provider reports. Adapters map their wire shapes onto this;
* poolsFromParts subtracts the details out so pools end up exclusive.
*/
export type TokenParts = {
/** Inclusive input total (cache and audio counted in). */
totalInput?: number | null;
/** Exclusive text input — wins over totalInput when reported directly. */
textInput?: number | null;
/** Inclusive output total (reasoning counted in). */
totalOutput?: number | null;
/** Exclusive text output — wins over totalOutput when reported directly. */
textOutput?: number | null;
cacheRead?: number;
cacheWrite?: number;
reasoning?: number;
/** Set (even to 0) only when the provider has an audio input pool. */
audioInput?: number;
};
/** Splits provider usage into exclusive token pools; throws if the provider returned no usable counts. */
export const poolsFromParts = (
parts: TokenParts,
modelName: string,
): TokenPools => {
const cacheRead = parts.cacheRead ?? 0;
const cacheWrite = parts.cacheWrite ?? 0;
const reasoning = parts.reasoning ?? 0;
const audioInput = parts.audioInput ?? 0;
const textInput =
parts.textInput ??
(parts.totalInput != null
? parts.totalInput - cacheRead - cacheWrite - audioInput
: undefined);
const textOutput =
parts.textOutput ??
(parts.totalOutput != null ? parts.totalOutput - reasoning : undefined);
return {
inputTokens: clamp(requiredCount(textInput, "Input", modelName)),
outputTokens: clamp(requiredCount(textOutput, "Output", modelName)),
cacheReadTokens: clamp(cacheRead),
cacheWriteTokens: clamp(cacheWrite),
reasoningTokens: clamp(reasoning),
...(parts.audioInput !== undefined && {
audioInputTokens: clamp(audioInput),
}),
};
};

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { normalizeUsage } from "../../src/usage.js";
import { normalizeUsage } from "../../src/ai-sdk/usage.js";
const MODEL = "openai/gpt-test";

View File

@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test";
import type { LanguageModelV3, LanguageModelV3Usage } from "@ai-sdk/provider";
import { generateText, streamText } from "ai";
import { withAutumn } from "../../src/index.js";
import { withAutumn } from "../../src/ai-sdk/index.js";
type TrackTokensParams = {
customerId: string;

View File

@@ -0,0 +1,427 @@
import { describe, expect, test } from "bun:test";
import {
type OpenRouterLike,
trackingSettled,
trackOpenRouterUsage,
withAutumn,
} from "../../src/openrouter/index.js";
import type { TrackTokensParams } from "../../src/shared/track.js";
const usage = {
promptTokens: 13,
completionTokens: 7,
promptTokensDetails: { cachedTokens: 2, cacheWriteTokens: 1 },
completionTokensDetails: { reasoningTokens: 2 },
cost: 0.0042,
};
const expectedPools = {
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 2,
cacheWriteTokens: 1,
reasoningTokens: 2,
audioInputTokens: 0,
};
const createAutumn = () => {
const calls: TrackTokensParams[] = [];
return {
calls,
autumn: {
balances: {
trackTokens: async (params: TrackTokensParams) => {
calls.push(params);
},
},
},
};
};
type SendRequest = Record<string, unknown>;
const createClient = (result: unknown) => {
const requests: SendRequest[] = [];
const client = {
apiKey: "sk-test",
chat: {
send: async (request: SendRequest) => {
requests.push(request);
return result;
},
},
models: {
list: async () => ["openai/gpt-4o"],
},
};
return { client: client as OpenRouterLike & typeof client, requests };
};
const streamOf = (chunks: unknown[]): AsyncIterable<unknown> => ({
async *[Symbol.asyncIterator]() {
for (const chunk of chunks) {
yield chunk;
}
},
});
describe("withAutumn (openrouter)", () => {
test("non-streaming send tracks normalized pools with openrouter cost", async () => {
const { calls, autumn } = createAutumn();
const { client, requests } = createClient({
model: "openai/gpt-4o",
usage,
choices: [],
});
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
properties: { source: "test" },
});
await wrapped.chat.send({ model: "openai/gpt-4o", messages: [] });
expect(requests[0]?.usage).toEqual({ include: true });
expect(calls).toEqual([
{
...expectedPools,
customerId: "cus_123",
modelId: "openrouter/openai/gpt-4o",
featureId: undefined,
entityId: undefined,
properties: { source: "test", openrouter_cost: 0.0042 },
},
]);
});
test("preserves caller usage options while forcing include", async () => {
const { autumn } = createAutumn();
const { client, requests } = createClient({
model: "openai/gpt-4o",
usage,
});
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
await wrapped.chat.send({
model: "openai/gpt-4o",
messages: [],
usage: { include: false },
});
expect(requests[0]?.usage).toEqual({ include: true });
});
test("prefers the requested slug over a provider-resolved snapshot slug", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({
// Providers resolve aliases to dated snapshots that pricing data
// may not list — the requested slug must win.
model: "anthropic/claude-5-fable-20260609",
usage,
});
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
await wrapped.chat.send({
model: "anthropic/claude-fable-5",
messages: [],
});
expect(calls[0]?.modelId).toBe("openrouter/anthropic/claude-fable-5");
});
test("router pseudo-models use the resolved slug from the response", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({ model: "openai/gpt-4o", usage });
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
await wrapped.chat.send({ model: "openrouter/auto", messages: [] });
expect(calls[0]?.modelId).toBe("openrouter/openai/gpt-4o");
});
test("falls back to the request model when the response omits one", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({ usage });
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
await wrapped.chat.send({ model: "openai/gpt-4o", messages: [] });
expect(calls[0]?.modelId).toBe("openrouter/openai/gpt-4o");
});
test("streaming tracks once from the final usage chunk", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient(
streamOf([
{ choices: [{ delta: { content: "hel" } }], usage: null },
{ choices: [{ delta: { content: "lo" } }], usage: null },
{ model: "openai/gpt-4o", choices: [], usage },
]),
);
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
const stream = (await wrapped.chat.send({
model: "openrouter/auto",
messages: [],
stream: true,
})) as AsyncIterable<{ choices?: { delta?: { content?: string } }[] }>;
let text = "";
for await (const chunk of stream) {
text += chunk.choices?.[0]?.delta?.content ?? "";
}
expect(text).toBe("hello");
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({
...expectedPools,
modelId: "openrouter/openai/gpt-4o",
});
});
test("streaming does not track when iteration stops before usage arrives", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient(
streamOf([
{ choices: [{ delta: { content: "hel" } }], usage: null },
{ model: "openai/gpt-4o", choices: [], usage },
]),
);
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
const stream = (await wrapped.chat.send({
model: "openai/gpt-4o",
messages: [],
stream: true,
})) as AsyncIterable<unknown>;
for await (const _chunk of stream) {
break;
}
expect(calls).toHaveLength(0);
});
test("missing usage is caught and never breaks the response", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({ model: "openai/gpt-4o", usage: null });
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
const result = (await wrapped.chat.send({
model: "openai/gpt-4o",
messages: [],
})) as { model: string };
expect(result.model).toBe("openai/gpt-4o");
expect(calls).toHaveLength(0);
});
test("non-chat properties and methods pass through", async () => {
const { autumn } = createAutumn();
const { client } = createClient({ model: "openai/gpt-4o", usage });
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
expect(wrapped.apiKey).toBe("sk-test");
expect(await wrapped.models.list()).toEqual(["openai/gpt-4o"]);
});
test("afterSuccess hook tracks responses-API JSON bodies (callModel path)", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({});
type Hook = {
afterSuccess: (
ctx: { operationID: string },
response: Response,
) => Response | Promise<Response>;
};
const hooks: Hook[] = [];
const hookedClient = Object.assign(client, {
_options: {
hooks: { registerAfterSuccessHook: (hook: Hook) => hooks.push(hook) },
},
});
withAutumn({ autumn, openRouter: hookedClient, customerId: "cus_123" });
expect(hooks).toHaveLength(1);
const body = {
model: "openai/gpt-4o-mini",
usage: {
input_tokens: 12,
input_tokens_details: { cached_tokens: 2 },
output_tokens: 7,
output_tokens_details: { reasoning_tokens: 3 },
total_tokens: 19,
cost: 0.001,
},
};
// JSON captures are awaited by the hook, so tracking is settled here.
await hooks[0]?.afterSuccess(
{ operationID: "createResponses" },
new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
}),
);
// Ignored operation should not track.
await hooks[0]?.afterSuccess(
{ operationID: "listModels" },
new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
}),
);
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({
inputTokens: 10,
outputTokens: 4,
cacheReadTokens: 2,
reasoningTokens: 3,
modelId: "openrouter/openai/gpt-4o-mini",
properties: { openrouter_cost: 0.001 },
});
});
test("afterSuccess hook tracks the final usage event of a responses SSE stream", async () => {
const { calls, autumn } = createAutumn();
const { client } = createClient({});
type Hook = {
afterSuccess: (
ctx: { operationID: string },
response: Response,
) => Response | Promise<Response>;
};
const hooks: Hook[] = [];
withAutumn({
autumn,
openRouter: Object.assign(client, {
_options: {
hooks: {
registerAfterSuccessHook: (hook: Hook) => hooks.push(hook),
},
},
}),
customerId: "cus_123",
});
const sse = [
'data: {"type":"response.output_text.delta","delta":"hi"}',
'data: {"type":"response.completed","response":{"model":"openai/gpt-4o-mini","usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}',
"data: [DONE]",
"",
].join("\n");
hooks[0]?.afterSuccess(
{ operationID: "createResponses" },
new Response(sse, {
headers: { "content-type": "text/event-stream" },
}),
);
// Streaming captures are fire-and-forget; trackingSettled awaits them.
await trackingSettled(client);
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({
inputTokens: 5,
outputTokens: 2,
modelId: "openrouter/openai/gpt-4o-mini",
});
});
test("nested chatRequest body (@openrouter/sdk >= 0.12) gets usage forced and model resolved", async () => {
const { calls, autumn } = createAutumn();
const { client, requests } = createClient({ usage });
const wrapped = withAutumn({
autumn,
openRouter: client,
customerId: "cus_123",
});
await wrapped.chat.send({
appTitle: "demo",
chatRequest: { model: "openai/gpt-4o-mini", messages: [] },
});
const sent = requests[0] as {
appTitle?: string;
usage?: unknown;
chatRequest?: { model?: string; usage?: unknown };
};
expect(sent.chatRequest?.usage).toEqual({ include: true });
expect(sent.usage).toBeUndefined();
expect(sent.appTitle).toBe("demo");
expect(calls[0]?.modelId).toBe("openrouter/openai/gpt-4o-mini");
});
});
describe("trackOpenRouterUsage", () => {
test("tracks snake_case raw API usage and prefixes the model slug", async () => {
const { calls, autumn } = createAutumn();
await trackOpenRouterUsage({
autumn,
usage: {
prompt_tokens: 13,
completion_tokens: 7,
prompt_tokens_details: { cached_tokens: 2, cache_write_tokens: 1 },
completion_tokens_details: { reasoning_tokens: 2 },
cost: 0.001,
},
model: "anthropic/claude-sonnet-4-5",
customerId: "cus_123",
});
expect(calls[0]).toMatchObject({
...expectedPools,
modelId: "openrouter/anthropic/claude-sonnet-4-5",
properties: { openrouter_cost: 0.001 },
});
});
test("does not double-prefix an already-prefixed slug", async () => {
const { calls, autumn } = createAutumn();
await trackOpenRouterUsage({
autumn,
usage: { promptTokens: 1, completionTokens: 1 },
model: "openrouter/openai/gpt-4o",
customerId: "cus_123",
});
expect(calls[0]?.modelId).toBe("openrouter/openai/gpt-4o");
});
});

View File

@@ -0,0 +1,139 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
createTracker,
type TrackTokensParams,
trackTokenUsage,
} from "../../src/shared/track.js";
const params: TrackTokensParams = {
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
audioInputTokens: 0,
customerId: "cus_123",
modelId: "openrouter/openai/gpt-4o",
};
describe("trackTokenUsage client shapes", () => {
test("calls root-level trackTokens bound to the client (current @useautumn/sdk)", async () => {
const calls: TrackTokensParams[] = [];
// Class instance so an unbound invocation would lose `this` and throw.
class FakeSdk {
private readonly sink = calls;
async trackTokens(p: TrackTokensParams) {
this.sink.push(p);
}
}
await trackTokenUsage({
autumn: new FakeSdk(),
getParams: () => params,
});
expect(calls).toEqual([params]);
});
test("prefers balances.trackTokens when present, bound to balances", async () => {
const calls: string[] = [];
class Balances {
private readonly name = "balances";
async trackTokens(_p: TrackTokensParams) {
calls.push(this.name);
}
}
await trackTokenUsage({
autumn: {
balances: new Balances(),
trackTokens: async () => {
calls.push("root");
},
},
getParams: () => params,
});
expect(calls).toEqual(["balances"]);
});
test("a client with neither shape logs and never throws", async () => {
await expect(
trackTokenUsage({ autumn: {}, getParams: () => params }),
).resolves.toBeUndefined();
});
});
describe("env fallback client", () => {
const originalFetch = globalThis.fetch;
const originalKey = process.env.AUTUMN_API_KEY;
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalKey === undefined) {
Reflect.deleteProperty(process.env, "AUTUMN_API_KEY");
} else {
process.env.AUTUMN_API_KEY = originalKey;
}
});
test("no autumn client → POSTs snake_case wire params with the env key", async () => {
process.env.AUTUMN_API_KEY = "am_sk_test";
const requests: { url: string; init: RequestInit }[] = [];
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
requests.push({ url: String(url), init: init ?? {} });
return new Response(JSON.stringify({ value: 0.1 }), {
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
await createTracker({ customerId: "cus_123" })(() => ({
pools: {
inputTokens: 10,
outputTokens: 5,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
},
modelId: "openrouter/openai/gpt-4o",
}));
expect(requests).toHaveLength(1);
expect(requests[0]?.url).toBe(
"https://api.useautumn.com/v1/balances.track_tokens",
);
const headers = requests[0]?.init.headers as Record<string, string>;
expect(headers.authorization).toBe("Bearer am_sk_test");
expect(JSON.parse(String(requests[0]?.init.body))).toEqual({
customer_id: "cus_123",
model_id: "openrouter/openai/gpt-4o",
input_tokens: 10,
output_tokens: 5,
cache_read_tokens: 0,
cache_write_tokens: 0,
reasoning_tokens: 0,
});
});
test("missing key is logged, never thrown", async () => {
// Assigning undefined would store the string "undefined"
Reflect.deleteProperty(process.env, "AUTUMN_API_KEY");
Reflect.deleteProperty(process.env, "AUTUMN_SECRET_KEY");
await expect(
createTracker({ customerId: "cus_123" })(() => ({
pools: {
inputTokens: 1,
outputTokens: 1,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
},
modelId: "openrouter/openai/gpt-4o",
})),
).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,13 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
"ai-sdk/index": "src/ai-sdk/index.ts",
"openrouter/index": "src/openrouter/index.ts",
},
format: ["cjs", "esm"],
dts: true,
splitting: false,
sourcemap: false,
clean: true,
});

View File

@@ -21791,8 +21791,7 @@ webhooks:
type: object
properties:
on_increase:
description: How to handle billing when quantity increases mid-cycle (prepaid
features only).
description: How to handle billing when quantity increases mid-cycle.
type: string
enum:
- bill_immediately
@@ -21800,8 +21799,7 @@ webhooks:
- prorate_next_cycle
- bill_next_cycle
on_decrease:
description: How to handle credits when quantity decreases mid-cycle (prepaid
features only).
description: How to handle credits when quantity decreases mid-cycle.
type: string
enum:
- prorate

View File

@@ -9519,7 +9519,7 @@ paths:
@example
```typescript
// Schedule a transition from a trial plan to a paid plan
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
```
@param customerId - The ID of the customer to create the schedule for.
@@ -22823,8 +22823,7 @@ webhooks:
type: object
properties:
on_increase:
description: How to handle billing when quantity increases mid-cycle (prepaid
features only).
description: How to handle billing when quantity increases mid-cycle.
type: string
enum:
- bill_immediately
@@ -22832,8 +22831,7 @@ webhooks:
- prorate_next_cycle
- bill_next_cycle
on_decrease:
description: How to handle credits when quantity decreases mid-cycle (prepaid
features only).
description: How to handle credits when quantity decreases mid-cycle.
type: string
enum:
- prorate

View File

@@ -1,16 +1,16 @@
lockVersion: 2.0.0
id: 7b300647-cd76-49e9-bf77-7d1bf5446d66
management:
docChecksum: 966a038781b87a2de162d06d92898e9c
docChecksum: 6b73cd58f0e4ebb4da09877adb1060be
docVersion: 2.3.0
speakeasyVersion: 1.762.0
generationVersion: 2.882.0
releaseVersion: 0.10.17
configChecksum: 4722f16a8dee67ebd4038caf3c345296
persistentEdits:
generation_id: 1fc85627-42eb-4644-9b77-ebf0dbfdfae5
pristine_commit_hash: 7e3d5d307c63db59cdb1d86c9ce2a6375b28f301
pristine_tree_hash: 7846287f358513cee6a50f31c4a93895b71abdca
generation_id: 5ad37585-7012-44f4-a99d-3adf964eaefc
pristine_commit_hash: b219ef5020fafbe0aefcfcaedb9299375d22beff
pristine_tree_hash: ff19a9a4b7914486171819db75c15a278ecce901
features:
typescript:
additionalDependencies: 0.1.0
@@ -4781,8 +4781,8 @@ trackedFiles:
pristine_git_object: 0ebe5146cd24c153cb9b7655e4f502c09f7d4abd
docs/sdks/billing/README.md:
id: dc915331dd9d
last_write_checksum: sha1:8c62d404551b2bd21c514e9a2f08dace569f33a0
pristine_git_object: c9e48bc4601c94faced9e1a4e70086c5d2d02c2b
last_write_checksum: sha1:a66a2d2140cb45116ff0c5fee2f612a2f73d3048
pristine_git_object: 6615dd642c553459aea6ded6582e2b9050ce75f5
docs/sdks/customers/README.md:
id: 9332759cffc2
last_write_checksum: sha1:74cd5f6cf800e1d86b2c332fed3c3cd53f3eeb6b
@@ -4873,8 +4873,8 @@ trackedFiles:
pristine_git_object: d1d2c39eb61de5da6dc66da31605995ee35edd8b
src/funcs/billing-create-schedule.ts:
id: fd662bfcdc10
last_write_checksum: sha1:a4205d004f0ac84be107a6966d9564f39d2b3709
pristine_git_object: 19b7260ea891ce902d9914a795bb89606b45317c
last_write_checksum: sha1:5b8cad986fd03dbc6afd776cf1472ce9a3d06b4d
pristine_git_object: a9707b612806d4ea8d696232f7546a9095e8db13
src/funcs/billing-multi-attach.ts:
id: 67491e2d8249
last_write_checksum: sha1:00ba80c1f98e7a8be29db0cf5a6957433f687861
@@ -5341,8 +5341,8 @@ trackedFiles:
pristine_git_object: 571de419ea3321d79acec4bddbb46b1580007115
src/sdk/billing.ts:
id: 10905058c4ad
last_write_checksum: sha1:fe5b900f735f8aae86c96ac4aaf3db16ee99c8b4
pristine_git_object: 46b9d884b1eee5f8278cf6cb0c6f348cd037ca18
last_write_checksum: sha1:a660c6f7cda849ea36821322bbf756d3e76b4254
pristine_git_object: 0647a0e866200ac2d495a978ec00fcdb08530ce7
src/sdk/customers.ts:
id: d33e193e0c00
last_write_checksum: sha1:8d64f03efa17b4ef45a6d67a44d23e2943f1cd8b

View File

@@ -8799,7 +8799,7 @@ paths:
@example
```typescript
// Schedule a transition from a trial plan to a paid plan
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
```
@param customerId - The ID of the customer to create the schedule for.
@@ -20949,7 +20949,7 @@ webhooks:
type: object
properties:
on_increase:
description: How to handle billing when quantity increases mid-cycle (prepaid features only).
description: How to handle billing when quantity increases mid-cycle.
type: string
enum:
- bill_immediately
@@ -20957,7 +20957,7 @@ webhooks:
- prorate_next_cycle
- bill_next_cycle
on_decrease:
description: How to handle credits when quantity decreases mid-cycle (prepaid features only).
description: How to handle credits when quantity decreases mid-cycle.
type: string
enum:
- prorate

View File

@@ -2,8 +2,8 @@ speakeasyVersion: 1.762.0
sources:
Autumn API:
sourceNamespace: autumn-api
sourceRevisionDigest: sha256:ed0d91660cb51ae63edc29c6c68ac6c0917693a4c5c75e5223e6983173c053dc
sourceBlobDigest: sha256:f6ce05c72b6f48ba32bf5c5f97d810e923ede72762703cf3c03bea4312fa778b
sourceRevisionDigest: sha256:093670207a94a2fc117e507cb655932ed2f69ddd41cfed5ab98a7cee3d617af5
sourceBlobDigest: sha256:1c40d45fd710e4ebbac2112f8097b072046c5a60b644185e9c0eb38c0e439e39
tags:
- latest
- 2.3.0
@@ -18,10 +18,10 @@ targets:
autumn:
source: Autumn API
sourceNamespace: autumn-api
sourceRevisionDigest: sha256:ed0d91660cb51ae63edc29c6c68ac6c0917693a4c5c75e5223e6983173c053dc
sourceBlobDigest: sha256:f6ce05c72b6f48ba32bf5c5f97d810e923ede72762703cf3c03bea4312fa778b
sourceRevisionDigest: sha256:093670207a94a2fc117e507cb655932ed2f69ddd41cfed5ab98a7cee3d617af5
sourceBlobDigest: sha256:1c40d45fd710e4ebbac2112f8097b072046c5a60b644185e9c0eb38c0e439e39
codeSamplesNamespace: autumn-api-typescript-code-samples
codeSamplesRevisionDigest: sha256:8147107cbc89c5f1656bf27acbcd53d3c3dd40a7b13ffb541586287addaa60f1
codeSamplesRevisionDigest: sha256:9ea6b523982e0cc7cdff128d8178ae86016e9a56bc99226a682a73b1f7aaa71a
autumn-python:
source: Autumn API Stripped
sourceNamespace: autumn-api-stripped

View File

@@ -305,7 +305,7 @@ Use this endpoint to schedule future plan changes (e.g. switch from a trial plan
@example
```typescript
// Schedule a transition from a trial plan to a paid plan
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
```
@param customerId - The ID of the customer to create the schedule for.
@@ -837,7 +837,7 @@ Use this endpoint to schedule future plan changes (e.g. switch from a trial plan
@example
```typescript
// Schedule a transition from a trial plan to a paid plan
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
```
@param customerId - The ID of the customer to create the schedule for.

View File

@@ -34,7 +34,7 @@ import { Result } from "../types/fp.js";
* @example
* ```typescript
* // Schedule a transition from a trial plan to a paid plan
* const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
* const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
* ```
*
* @param customerId - The ID of the customer to create the schedule for.

View File

@@ -87,7 +87,7 @@ export class Billing extends ClientSDK {
* @example
* ```typescript
* // Schedule a transition from a trial plan to a paid plan
* const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781207020293,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782416620293,"plans":[{"planId":"pro_plan"}]}] });
* const response = await client.billing.createSchedule({ customerId: "cus_123", phases: [{"startsAt":1781265695558,"plans":[{"planId":"trial_plan"}]},{"startsAt":1782475295558,"plans":[{"planId":"pro_plan"}]}] });
* ```
*
* @param customerId - The ID of the customer to create the schedule for.