/** * metering — Billing-aware wrappers for embedder and chat functions * * Wraps EmbedderFunction and ChatFunction with: * 1. Pre-check: `check_billing_quota(meter_slug, entity_id, estimated_amount)` * 2. Execute the underlying function * 3. Post-record: `record_usage(meter_slug, entity_id, actual_amount)` * * When the quota check fails, the wrapper returns null (graceful degradation) * instead of throwing, so the search pipeline can fall back to text-only. * * Token counts: * - Chat: real provider counts via ChatResult.usage (from OllamaAdapter.stream()) * - Embedding: real provider counts via EmbeddingResult.promptTokens (from /api/embed) * * The billing functions live in the tenant database and are called via the * Graphile `withPgClient` callback. Function locations (schema, names) are * resolved from `billing_module` metaschema and cached by `config-cache.ts`. */ import type { BillingConfig, InferenceLogConfig, PgClient } from './config-cache'; import type { ChatFunction, ChatMessage, ChatOptions, EmbedderFunction } from './types'; /** * Callback matching Graphile's withPgClient signature. * Acquires a pg client, calls the callback, then releases the client. */ export type WithPgClient = (pgSettings: Record, callback: (pgClient: PgClient) => Promise) => Promise; export interface MeteringContext { /** Callback to acquire a tenant database client */ withPgClient: WithPgClient; /** pgSettings from the GraphQL context (for role/claims) */ pgSettings: Record; /** Billing function references from the billing_module */ billing: BillingConfig; /** Entity ID to meter against (from JWT claims) */ entityId: string; /** Per-request correlation ID (from request.id pgSetting) */ requestId: string | null; /** Database UUID from JWT claims */ databaseId: string; /** Actor (user) ID from JWT claims */ actorId: string | null; /** Inference log table config (null if inference_log_module not provisioned) */ inferenceLog: InferenceLogConfig | null; } export interface MeteringOptions { /** Meter slug for embedding operations (default: model name from build config) */ embeddingMeterSlug?: string; /** Meter slug for chat completion operations (default: model name from build config) */ chatMeterSlug?: string; /** Whether to skip metering entirely (e.g. for local dev). Default: false */ skipMetering?: boolean; /** Embedding model name (for inference log) */ embeddingModel?: string; /** Chat model name (for inference log) */ chatModel?: string; /** Provider name (for inference log) */ provider?: string; } export interface MeterResult { /** The result from the underlying function, or null if quota exceeded */ result: T | null; /** Whether the call was metered */ metered: boolean; /** Whether the call was skipped due to quota limits */ quotaExceeded: boolean; /** Latency of the underlying function call in ms */ latencyMs: number; } export interface InferenceLogEntry { databaseId: string; entityId: string; actorId: string | null; model: string; provider: string | null; service: 'llm' | 'embedding' | 'tts' | 'stt' | 'ocr' | 'image_gen' | 'search' | 'compute'; operation: string; inputTokens: number; outputTokens: number; totalTokens: number; cacheReadTokens: number | null; cacheWriteTokens: number | null; latencyMs: number; ragEnabled: boolean; chunksRetrieved: number | null; embeddingModel: string | null; embeddingLatencyMs: number | null; status: 'success' | 'quota_exceeded' | 'provider_error' | 'timeout'; errorType: string | null; rawUsage: Record | null; } /** * Write a row to the usage_log_inference table. * Delegates to the shared BillingClient from express-context. */ export declare function logInferenceUsage(ctx: MeteringContext, entry: InferenceLogEntry): Promise; /** * Wrap an embedder with billing quota check + usage recording. * * The returned MeterResult contains `quotaExceeded: true` when the pre-check * fails, enabling the caller to fall back to text-only search. */ export declare function meteredEmbed(embedder: EmbedderFunction, text: string, ctx: MeteringContext | null, options?: MeteringOptions): Promise>; /** * Wrap a chat completion call with billing quota check + usage recording. */ export declare function meteredChat(chat: ChatFunction, messages: ChatMessage[], ctx: MeteringContext | null, chatOptions?: ChatOptions, meteringOptions?: MeteringOptions): Promise>; export declare class QuotaExceededError extends Error { readonly code = "QUOTA_EXCEEDED"; readonly meterSlug: string; readonly entityId: string; constructor(meterSlug: string, entityId: string); }