Merge pull request #1916 from useautumn/charlie/gateway-openrouter
Charlie/gateway openrouter
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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! });
|
||||
|
||||
|
||||
@@ -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>
|
||||
43
bun.lock
43
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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"]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
51
packages/gateway/package.json
Normal file
51
packages/gateway/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
67
packages/gateway/src/ai-sdk/index.ts
Normal file
67
packages/gateway/src/ai-sdk/index.ts
Normal 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 });
|
||||
};
|
||||
@@ -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);
|
||||
318
packages/gateway/src/openrouter/index.ts
Normal file
318
packages/gateway/src/openrouter/index.ts
Normal 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;
|
||||
};
|
||||
87
packages/gateway/src/openrouter/usage.ts
Normal file
87
packages/gateway/src/openrouter/usage.ts
Normal 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,
|
||||
);
|
||||
};
|
||||
155
packages/gateway/src/shared/track.ts
Normal file
155
packages/gateway/src/shared/track.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
77
packages/gateway/src/shared/usage.ts
Normal file
77
packages/gateway/src/shared/usage.ts
Normal 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),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
427
packages/gateway/tests/unit/openrouter.test.ts
Normal file
427
packages/gateway/tests/unit/openrouter.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
139
packages/gateway/tests/unit/track.test.ts
Normal file
139
packages/gateway/tests/unit/track.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
13
packages/gateway/tsup.config.ts
Normal file
13
packages/gateway/tsup.config.ts
Normal 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,
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user