Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/api/src/routes/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const dailyActivitySchema = z.object({
inputCost: z.number(),
outputCost: z.number(),
requestCost: z.number(),
dataStorageCost: z.number(),
errorCount: z.number(),
errorRate: z.number(),
cacheCount: z.number(),
Expand Down Expand Up @@ -155,6 +156,10 @@ activity.openapi(getActivity, async (c) => {
requestCost: sql<number>`COALESCE(SUM(${tables.log.requestCost}), 0)`.as(
"requestCost",
),
dataStorageCost:
sql<number>`COALESCE(SUM(${tables.log.dataStorageCost}), 0)`.as(
"dataStorageCost",
),
errorCount:
sql<number>`SUM(CASE WHEN ${tables.log.hasError} = true THEN 1 ELSE 0 END)`.as(
"errorCount",
Expand Down Expand Up @@ -252,6 +257,7 @@ activity.openapi(getActivity, async (c) => {
const inputCost = Number(day.inputCost);
const outputCost = Number(day.outputCost);
const requestCost = Number(day.requestCost);
const dataStorageCost = Number(day.dataStorageCost);
const errorCount = Number(day.errorCount);
const cacheCount = Number(day.cacheCount);
const discountSavings = Number(day.discountSavings);
Expand All @@ -269,6 +275,7 @@ activity.openapi(getActivity, async (c) => {
inputCost,
outputCost,
requestCost,
dataStorageCost,
errorCount,
errorRate,
cacheCount,
Expand Down
40 changes: 32 additions & 8 deletions apps/docs/content/features/cost-breakdown.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,28 @@ When cost breakdown is enabled, your API responses will include additional cost
"cost_usd_input": 0.000025,
"cost_usd_output": 0.0001,
"cost_usd_cached_input": 0,
"cost_usd_request": 0
"cost_usd_request": 0,
"cost_usd_data_storage": 0.00000025
}
}
```

## Cost Fields

| Field | Description |
| ----------------------- | --------------------------------------------------------------- |
| `cost_usd_total` | Total cost for the request in USD |
| `cost_usd_input` | Cost for input/prompt tokens in USD |
| `cost_usd_output` | Cost for output/completion tokens in USD |
| `cost_usd_cached_input` | Cost for cached input tokens in USD (discounted rate) |
| `cost_usd_request` | Per-request cost in USD (for models with request-based pricing) |
| Field | Description |
| ----------------------- | ---------------------------------------------------------------------------------- |
| `cost_usd_total` | Total inference cost for the request in USD (excludes storage) |
| `cost_usd_input` | Cost for input/prompt tokens in USD |
| `cost_usd_output` | Cost for output/completion tokens in USD |
| `cost_usd_cached_input` | Cost for cached input tokens in USD (discounted rate) |
| `cost_usd_request` | Per-request cost in USD (for models with request-based pricing) |
| `cost_usd_data_storage` | LLM Gateway storage cost in USD ($0.01 per 1M tokens, only when retention enabled) |

<Callout type="info">
**Note:** `cost_usd_total` includes only provider/inference costs. Data
storage costs (`cost_usd_data_storage`) are billed separately by LLM Gateway
when data retention is enabled in organization policies.
</Callout>

## Streaming Responses

Expand Down Expand Up @@ -205,6 +213,22 @@ async function loggedRequest(model: string, messages: Message[]) {
}
```

## Data Storage Costs

When data retention is enabled in organization policies, LLM Gateway stores full request and response payloads for the configured retention period. This storage incurs a small additional cost:

- **Rate**: $0.01 per 1 million tokens
- **Applies to**: Input, cached, output, and reasoning tokens
- **When charged**: Only when retention level is set to "Retain All Data"
- **Billing mode**: In API keys mode, only storage costs are deducted from credits (inference costs are billed to your provider keys)

Storage costs are displayed separately from inference costs in the dashboard and usage breakdown to maintain transparency between provider costs and LLM Gateway platform costs.

<Callout type="success">
Enable [auto top-up](/dashboard) in billing settings to prevent request
failures when storage costs deplete your credits.
</Callout>

## Self-Hosted Deployments

If you're running a self-hosted LLM Gateway deployment, cost breakdown is always included in API responses regardless of plan. This allows you to track internal costs and allocate them across teams or projects.
2 changes: 2 additions & 0 deletions apps/gateway/src/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ describe("test", () => {
name: "Test Organization",
billingEmail: "user",
plan: "pro",
retentionLevel: "retain",
credits: "100.00", // Add credits for retention storage costs
});

await db.insert(tables.userOrganization).values({
Expand Down
43 changes: 42 additions & 1 deletion apps/gateway/src/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { validateSource } from "@/chat/tools/validate-source.js";
import { reportKeyError, reportKeySuccess } from "@/lib/api-key-health.js";
import { calculateCosts } from "@/lib/costs.js";
import { throwIamException, validateModelAccess } from "@/lib/iam.js";
import { insertLog } from "@/lib/logs.js";
import { calculateDataStorageCost, insertLog } from "@/lib/logs.js";

import {
generateCacheKey,
Expand Down Expand Up @@ -1617,6 +1617,17 @@ chat.openapi(completions, async (c) => {
);
}

// Check if organization has credits for data retention costs
// Data storage is billed at $0.01 per 1M tokens, so we need credits when retention is enabled
if (organization && organization.retentionLevel === "retain") {
if (parseFloat(organization.credits || "0") <= 0) {
throw new HTTPException(402, {
message:
"Organization has insufficient credits for data retention. Data retention requires credits for storage costs ($0.01 per 1M tokens). Please add credits or disable data retention in organization settings.",
});
}
}

if (!usedToken) {
throw new HTTPException(500, {
message: `No token`,
Expand Down Expand Up @@ -1828,6 +1839,12 @@ chat.openapi(completions, async (c) => {
estimatedCost: costs.estimatedCost,
discount: costs.discount ?? null,
pricingTier: costs.pricingTier ?? null,
dataStorageCost: calculateDataStorageCost(
promptTokens,
cachedTokens,
completionTokens,
reasoningTokens,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cached: true,
toolResults:
(cachedStreamingResponse.metadata as { toolResults?: any })
Expand Down Expand Up @@ -1936,6 +1953,12 @@ chat.openapi(completions, async (c) => {
estimatedCost: cachedCosts.estimatedCost,
discount: cachedCosts.discount ?? null,
pricingTier: cachedCosts.pricingTier ?? null,
dataStorageCost: calculateDataStorageCost(
cachedResponse.usage?.prompt_tokens,
cachedResponse.usage?.prompt_tokens_details?.cached_tokens,
cachedResponse.usage?.completion_tokens,
cachedResponse.usage?.reasoning_tokens,
),
cached: true,
toolResults: cachedResponse.choices?.[0]?.message?.tool_calls || null,
});
Expand Down Expand Up @@ -2183,6 +2206,7 @@ chat.openapi(completions, async (c) => {
cachedInputCost: null,
requestCost: null,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -2269,6 +2293,7 @@ chat.openapi(completions, async (c) => {
cachedInputCost: null,
requestCost: null,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -2418,6 +2443,7 @@ chat.openapi(completions, async (c) => {
cachedInputCost: null,
requestCost: null,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -3510,6 +3536,12 @@ chat.openapi(completions, async (c) => {
estimatedCost: costs.estimatedCost,
discount: costs.discount,
pricingTier: costs.pricingTier,
dataStorageCost: calculateDataStorageCost(
calculatedPromptTokens,
cachedTokens,
calculatedCompletionTokens,
calculatedReasoningTokens,
),
cached: false,
tools,
toolResults: streamingToolCalls,
Expand Down Expand Up @@ -3680,6 +3712,7 @@ chat.openapi(completions, async (c) => {
requestCost: null,
estimatedCost: false,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -3763,6 +3796,7 @@ chat.openapi(completions, async (c) => {
requestCost: null,
estimatedCost: false,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -3875,6 +3909,7 @@ chat.openapi(completions, async (c) => {
requestCost: null,
estimatedCost: false,
discount: null,
dataStorageCost: "0",
cached: false,
toolResults: null,
});
Expand Down Expand Up @@ -4124,6 +4159,12 @@ chat.openapi(completions, async (c) => {
estimatedCost: costs.estimatedCost,
discount: costs.discount,
pricingTier: costs.pricingTier,
dataStorageCost: calculateDataStorageCost(
calculatedPromptTokens,
cachedTokens,
calculatedCompletionTokens,
calculatedReasoningTokens,
),
cached: false,
tools,
toolResults,
Expand Down
22 changes: 22 additions & 0 deletions apps/gateway/src/lib/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ export function getUnifiedFinishReason(
return UnifiedFinishReason.UNKNOWN;
}

/**
* Calculate data storage cost based on token usage
* $0.01 per 1M tokens (total tokens = input + cached + output + reasoning)
*/
export function calculateDataStorageCost(
promptTokens: number | string | null | undefined,
cachedTokens: number | string | null | undefined,
completionTokens: number | string | null | undefined,
reasoningTokens: number | string | null | undefined,
): string {
const prompt = Number(promptTokens) || 0;
const cached = Number(cachedTokens) || 0;
const completion = Number(completionTokens) || 0;
const reasoning = Number(reasoningTokens) || 0;

const totalTokens = prompt + cached + completion + reasoning;

// $0.01 per 1M tokens
const cost = (totalTokens / 1_000_000) * 0.01;
return cost.toString();
}

/**
* Insert a log entry into the database.
* This function is extracted to prepare for future implementation using a message queue.
Expand Down
1 change: 1 addition & 0 deletions apps/playground/src/lib/api/v1.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ export interface paths {
inputCost: number;
outputCost: number;
requestCost: number;
dataStorageCost: number;
errorCount: number;
errorRate: number;
cacheCount: number;
Expand Down
2 changes: 1 addition & 1 deletion apps/ui/src/components/billing/plan-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export function PlanManagement() {
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span>90-day data retention</span>
<span>30-day data retention</span>
</div>
</div>
<div className="space-y-2">
Expand Down
17 changes: 12 additions & 5 deletions apps/ui/src/components/dashboard/dashboard-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,10 @@ export function DashboardClient({ initialActivityData }: DashboardClientProps) {
activityData.reduce((sum, day) => sum + day.inputCost, 0) || 0;
const totalOutputCost =
activityData.reduce((sum, day) => sum + day.outputCost, 0) || 0;
const totalDataStorageCost =
activityData.reduce((sum, day) => sum + day.dataStorageCost, 0) || 0;
const totalRequestCost =
activityData.reduce(
(sum, day) => sum + (day.cost - day.inputCost - day.outputCost),
0,
) || 0;
activityData.reduce((sum, day) => sum + day.requestCost, 0) || 0;
const totalSavings =
activityData.reduce((sum, day) => sum + day.discountSavings, 0) || 0;

Expand Down Expand Up @@ -331,7 +330,7 @@ export function DashboardClient({ initialActivityData }: DashboardClientProps) {
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Cost Estimate
Inference Cost
</CardTitle>
<CircleDollarSign className="text-muted-foreground h-4 w-4" />
</CardHeader>
Expand All @@ -357,6 +356,14 @@ export function DashboardClient({ initialActivityData }: DashboardClientProps) {
</>
)}
</p>
{totalDataStorageCost > 0 && (
<p className="text-muted-foreground text-xs mt-1 pt-1 border-t border-muted">
<span className="text-xs">
LLM Gateway: ${totalDataStorageCost.toFixed(4)}{" "}
storage
</span>
</p>
)}
</>
)}
</CardContent>
Expand Down
Loading
Loading