Skip to content

Commit 2a6bd66

Browse files
ersinkocclaude
andcommitted
✨ feat(costs): add provider billing type (pay-per-use / subscription / free)
Core: - New BillingType = 'pay-per-use' | 'subscription' | 'free' - New ProviderBillingConfig interface (monthlyCostUsd, planName, notes) - calculateCost() returns 0 for subscription/free (no per-token cost) - UsageRecord gains billingType field for filtering Gateway: - CustomProvider gains billingType, subscriptionCostUsd, subscriptionPlan, billingNotes - CreateProviderInput + UpdateProviderInput include billing fields - UserProviderConfig inputs include billing fields - Migration 024: adds billing columns to custom_providers, auto-sets 'free' for local providers - 001_initial_schema.sql updated for fresh installs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8aeaac3 commit 2a6bd66

7 files changed

Lines changed: 100 additions & 6 deletions

File tree

packages/core/src/costs/calculator.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Functions for calculating and estimating LLM API costs.
55
*/
66

7-
import type { AIProvider, CostEstimate, ModelPricing } from './types.js';
7+
import type { AIProvider, BillingType, CostEstimate, ModelPricing } from './types.js';
88
import { MODEL_PRICING } from './model-pricing.js';
99

1010
// Pre-built lookup maps for O(1) exact-match pricing (built once at module load)
@@ -36,14 +36,23 @@ export function getModelPricing(provider: AIProvider, modelId: string): ModelPri
3636
}
3737

3838
/**
39-
* Calculate cost for a request
39+
* Calculate cost for a request.
40+
*
41+
* If `billingType` is 'subscription' or 'free', returns 0 (no per-token cost).
42+
* Subscription costs are tracked separately via provider billing config.
4043
*/
4144
export function calculateCost(
4245
provider: AIProvider,
4346
modelId: string,
4447
inputTokens: number,
45-
outputTokens: number
48+
outputTokens: number,
49+
billingType?: BillingType
4650
): number {
51+
// Subscription and free providers have no per-token cost
52+
if (billingType === 'subscription' || billingType === 'free') {
53+
return 0;
54+
}
55+
4756
const pricing = getModelPricing(provider, modelId);
4857

4958
if (!pricing) {

packages/core/src/costs/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
// Types
1919
export type {
2020
AIProvider,
21+
BillingType,
22+
ProviderBillingConfig,
2123
ModelPricing,
2224
UsageRecord,
2325
UsageSummary,

packages/core/src/costs/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22
* Cost Tracking Types
33
*/
44

5+
/**
6+
* Billing type for AI providers
7+
*
8+
* - `pay-per-use`: Standard API billing per token (OpenAI, Anthropic, etc.)
9+
* - `subscription`: Fixed monthly fee, no per-token cost (ChatGPT Plus, Claude Pro, etc.)
10+
* - `free`: No cost (local models, free tiers like Groq, Ollama, etc.)
11+
*/
12+
export type BillingType = 'pay-per-use' | 'subscription' | 'free';
13+
14+
/**
15+
* Provider billing configuration
16+
*/
17+
export interface ProviderBillingConfig {
18+
/** Billing model */
19+
billingType: BillingType;
20+
/** Monthly subscription cost in USD (only for subscription type) */
21+
monthlyCostUsd?: number;
22+
/** Subscription plan name (e.g., "ChatGPT Plus", "Claude Pro") */
23+
planName?: string;
24+
/** Notes (e.g., "Includes 5M tokens/month") */
25+
notes?: string;
26+
}
27+
528
/**
629
* AI Provider
730
*/
@@ -76,6 +99,8 @@ export interface UsageRecord {
7699
latencyMs: number;
77100
/** Request type */
78101
requestType: 'chat' | 'completion' | 'embedding' | 'image' | 'audio' | 'tool';
102+
/** Billing type for this request */
103+
billingType?: BillingType;
79104
/** Was cached */
80105
cached?: boolean;
81106
/** Error if failed */

packages/gateway/src/db/migrations/postgres/001_initial_schema.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,10 @@ CREATE TABLE IF NOT EXISTS custom_providers (
614614
api_key_setting TEXT,
615615
provider_type TEXT NOT NULL DEFAULT 'openai_compatible' CHECK(provider_type IN ('openai_compatible', 'custom')),
616616
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
617+
billing_type TEXT NOT NULL DEFAULT 'pay-per-use',
618+
subscription_cost_usd REAL,
619+
subscription_plan TEXT,
620+
billing_notes TEXT,
617621
config JSONB DEFAULT '{}',
618622
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
619623
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Provider billing type migration
2+
-- Adds billing type distinction: pay-per-use (API), subscription (flat fee), free (local/free tier)
3+
4+
-- Add billing columns to custom_providers
5+
ALTER TABLE custom_providers ADD COLUMN IF NOT EXISTS billing_type TEXT NOT NULL DEFAULT 'pay-per-use';
6+
ALTER TABLE custom_providers ADD COLUMN IF NOT EXISTS subscription_cost_usd REAL;
7+
ALTER TABLE custom_providers ADD COLUMN IF NOT EXISTS subscription_plan TEXT;
8+
ALTER TABLE custom_providers ADD COLUMN IF NOT EXISTS billing_notes TEXT;
9+
10+
-- Set known free providers
11+
UPDATE custom_providers SET billing_type = 'free'
12+
WHERE provider_id IN ('local', 'ollama', 'lmstudio', 'localai', 'vllm')
13+
AND billing_type = 'pay-per-use';

packages/gateway/src/db/repositories/model-configs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ describe('ModelConfigsRepository', () => {
717717
});
718718

719719
const params = mockAdapter.execute.mock.calls[0]![1] as unknown[];
720-
expect(params[8]).toBe('{"key":"value"}');
720+
expect(params[12]).toBe('{"key":"value"}');
721721
});
722722
});
723723

packages/gateway/src/db/repositories/model-configs.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export interface CustomProvider {
4040
apiKeySetting?: string;
4141
providerType: 'openai_compatible' | 'custom';
4242
isEnabled: boolean;
43+
billingType: 'pay-per-use' | 'subscription' | 'free';
44+
subscriptionCostUsd?: number;
45+
subscriptionPlan?: string;
46+
billingNotes?: string;
4347
config: Record<string, unknown>;
4448
createdAt: Date;
4549
updatedAt: Date;
@@ -79,6 +83,10 @@ export interface CreateProviderInput {
7983
apiKeySetting?: string;
8084
providerType?: 'openai_compatible' | 'custom';
8185
isEnabled?: boolean;
86+
billingType?: 'pay-per-use' | 'subscription' | 'free';
87+
subscriptionCostUsd?: number;
88+
subscriptionPlan?: string;
89+
billingNotes?: string;
8290
config?: Record<string, unknown>;
8391
}
8492

@@ -105,6 +113,9 @@ export interface CreateUserProviderConfigInput {
105113
isEnabled?: boolean;
106114
apiKeyEnv?: string;
107115
notes?: string;
116+
billingType?: 'pay-per-use' | 'subscription' | 'free';
117+
subscriptionCostUsd?: number;
118+
subscriptionPlan?: string;
108119
config?: Record<string, unknown>;
109120
}
110121

@@ -114,6 +125,9 @@ export interface UpdateUserProviderConfigInput {
114125
isEnabled?: boolean;
115126
apiKeyEnv?: string;
116127
notes?: string;
128+
billingType?: 'pay-per-use' | 'subscription' | 'free';
129+
subscriptionCostUsd?: number;
130+
subscriptionPlan?: string;
117131
config?: Record<string, unknown>;
118132
}
119133

@@ -123,6 +137,10 @@ export interface UpdateProviderInput {
123137
apiKeySetting?: string;
124138
providerType?: 'openai_compatible' | 'custom';
125139
isEnabled?: boolean;
140+
billingType?: 'pay-per-use' | 'subscription' | 'free';
141+
subscriptionCostUsd?: number;
142+
subscriptionPlan?: string;
143+
billingNotes?: string;
126144
config?: Record<string, unknown>;
127145
}
128146

@@ -159,6 +177,10 @@ interface CustomProviderRow {
159177
api_key_setting: string | null;
160178
provider_type: string;
161179
is_enabled: boolean;
180+
billing_type: string | null;
181+
subscription_cost_usd: number | null;
182+
subscription_plan: string | null;
183+
billing_notes: string | null;
162184
config: string;
163185
created_at: string;
164186
updated_at: string;
@@ -204,6 +226,7 @@ function rowToModelConfig(row: ModelConfigRow): UserModelConfig {
204226
}
205227

206228
function rowToProvider(row: CustomProviderRow): CustomProvider {
229+
const billingType = row.billing_type as CustomProvider['billingType'] | null;
207230
return {
208231
id: row.id,
209232
userId: row.user_id,
@@ -213,6 +236,10 @@ function rowToProvider(row: CustomProviderRow): CustomProvider {
213236
apiKeySetting: row.api_key_setting || undefined,
214237
providerType: row.provider_type as 'openai_compatible' | 'custom',
215238
isEnabled: row.is_enabled,
239+
billingType: billingType ?? 'pay-per-use',
240+
subscriptionCostUsd: row.subscription_cost_usd ?? undefined,
241+
subscriptionPlan: row.subscription_plan ?? undefined,
242+
billingNotes: row.billing_notes ?? undefined,
216243
config: parseJsonField(row.config, {}),
217244
createdAt: new Date(row.created_at),
218245
updatedAt: new Date(row.updated_at),
@@ -518,6 +545,10 @@ export class ModelConfigsRepository extends BaseRepository {
518545
provider_type = COALESCE($4, provider_type),
519546
is_enabled = COALESCE($5, is_enabled),
520547
config = COALESCE($6, config),
548+
billing_type = COALESCE($10, billing_type),
549+
subscription_cost_usd = COALESCE($11, subscription_cost_usd),
550+
subscription_plan = COALESCE($12, subscription_plan),
551+
billing_notes = COALESCE($13, billing_notes),
521552
updated_at = $7
522553
WHERE user_id = $8 AND provider_id = $9`,
523554
[
@@ -530,6 +561,10 @@ export class ModelConfigsRepository extends BaseRepository {
530561
now,
531562
userId,
532563
input.providerId,
564+
input.billingType ?? null,
565+
input.subscriptionCostUsd ?? null,
566+
input.subscriptionPlan ?? null,
567+
input.billingNotes ?? null,
533568
]
534569
);
535570

@@ -542,8 +577,10 @@ export class ModelConfigsRepository extends BaseRepository {
542577
await this.execute(
543578
`INSERT INTO custom_providers (
544579
id, user_id, provider_id, display_name,
545-
api_base_url, api_key_setting, provider_type, is_enabled, config, created_at, updated_at
546-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
580+
api_base_url, api_key_setting, provider_type, is_enabled,
581+
billing_type, subscription_cost_usd, subscription_plan, billing_notes,
582+
config, created_at, updated_at
583+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
547584
[
548585
id,
549586
userId,
@@ -553,6 +590,10 @@ export class ModelConfigsRepository extends BaseRepository {
553590
input.apiKeySetting || null,
554591
input.providerType || 'openai_compatible',
555592
input.isEnabled !== false,
593+
input.billingType || 'pay-per-use',
594+
input.subscriptionCostUsd ?? null,
595+
input.subscriptionPlan ?? null,
596+
input.billingNotes ?? null,
556597
JSON.stringify(input.config || {}),
557598
now,
558599
now,

0 commit comments

Comments
 (0)