diff --git a/examples/ai-sdk-wrapper-usage.ts b/examples/ai-sdk-wrapper-usage.ts index 4073ade..0e0a130 100644 --- a/examples/ai-sdk-wrapper-usage.ts +++ b/examples/ai-sdk-wrapper-usage.ts @@ -25,7 +25,7 @@ async function main() { userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", event, inputDebit: biller.tag("PREMIUM_CALL"), - outputDebit: mul(outputTokens(), 0.0001), + outputDebit: biller.expr(mul(outputTokens(), 0.0001)), }); }, }); diff --git a/examples/ai-token-stream-expr-usage.ts b/examples/ai-token-stream-expr-usage.ts index cbbae71..6683498 100644 --- a/examples/ai-token-stream-expr-usage.ts +++ b/examples/ai-token-stream-expr-usage.ts @@ -19,7 +19,7 @@ async function* tokenUsageFromAIStream(): AsyncGenerator< inputTokens: 150, outputTokens: 0, inputDebit: biller.expr("PER_TOKEN_INPUT"), - outputDebit: mul(biller.tag("EXTRA_FEE"), outputTokens()), + outputDebit: biller.expr(mul(biller.tag("EXTRA_FEE"), outputTokens())), }; yield { @@ -27,8 +27,8 @@ async function* tokenUsageFromAIStream(): AsyncGenerator< model: "gpt-4", inputTokens: 0, outputTokens: 75, - inputDebit: mul(biller.tag("PREMIUM_CALL"), inputTokens()), - outputDebit: mul(biller.tag("EXTRA_FEE"), outputTokens()), + inputDebit: biller.expr(mul(biller.tag("PREMIUM_CALL"), inputTokens())), + outputDebit: biller.expr(mul(biller.tag("EXTRA_FEE"), outputTokens())), }; } diff --git a/examples/basic-usage-expr.ts b/examples/basic-usage-expr.ts index 1cd5963..14e6503 100644 --- a/examples/basic-usage-expr.ts +++ b/examples/basic-usage-expr.ts @@ -6,17 +6,30 @@ config({ path: ".env.local" }); async function main() { await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: mul(biller.tag("PREMIUM_CALL"), 3), + debit: biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)), }); await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: mul(biller.tag("EXTRA_FEE"), 3), + debit: biller.expr(mul(biller.tag("EXTRA_FEE"), 3)), }); await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: add(biller.expr("COMPLEX_FEE"), mul(biller.tag("PREMIUM_CALL"), 5)), + debit: biller.expr( + add(biller.expr("COMPLEX_FEE"), mul(biller.tag("PREMIUM_CALL"), 5)) + ), + }); + + // biller.expr() also accepts raw amounts and tags + await biller.basicUsageEventConsumer({ + userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", + debit: biller.expr(250), + }); + + await biller.basicUsageEventConsumer({ + userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", + debit: biller.expr(biller.tag("EXTRA_FEE")), }); console.log("Basic usage expression events consumed successfully"); diff --git a/packages/scrawn/src/core/pricing/index.ts b/packages/scrawn/src/core/pricing/index.ts index 211e245..4dfa885 100644 --- a/packages/scrawn/src/core/pricing/index.ts +++ b/packages/scrawn/src/core/pricing/index.ts @@ -32,6 +32,7 @@ export type { OutputTokensExpr, ExprRef, PriceExpr, + ExprValue, ExprInput, } from "./types.js"; diff --git a/packages/scrawn/src/core/pricing/types.ts b/packages/scrawn/src/core/pricing/types.ts index af1e2bd..be77a45 100644 --- a/packages/scrawn/src/core/pricing/types.ts +++ b/packages/scrawn/src/core/pricing/types.ts @@ -96,6 +96,24 @@ export type PriceExpr = | OutputTokensExpr | ExprRef; +/** + * A branded PriceExpr returned by biller.expr(). + * Use biller.expr() to wrap inline expressions, tags, or amounts. + * Raw OpExpr values (e.g., mul(tag("X"), 3)) are not accepted by debit + * — they must be wrapped in biller.expr(). + */ +declare const EXPR_BRAND: unique symbol; +export type ExprValue = PriceExpr & { + readonly [EXPR_BRAND]: true; +}; + +/** @internal — casts a PriceExpr to ExprValue at compile time, no-op at runtime. */ +export function toExprValue( + expr: PriceExpr +): ExprValue { + return expr as ExprValue; +} + /** * Input type for DSL builder functions. * Accepts either a PriceExpr or a raw number (interpreted as cents). diff --git a/packages/scrawn/src/core/scrawn.ts b/packages/scrawn/src/core/scrawn.ts index 12a75e4..fe0274e 100644 --- a/packages/scrawn/src/core/scrawn.ts +++ b/packages/scrawn/src/core/scrawn.ts @@ -15,7 +15,13 @@ import type { AuthMethodName, AllCredentials, } from "./types/auth.js"; -import type { TagExpr, PriceExpr, ExprRef } from "./pricing/types.js"; +import type { + TagExpr, + PriceExpr, + ExprRef, + ExprValue, +} from "./pricing/types.js"; +import { toExprValue } from "./pricing/types.js"; import { ApiKeyAuth } from "./auth/apiKeyAuth.js"; import { ScrawnLogger } from "../utils/logger.js"; import { matchPath } from "../utils/pathMatcher.js"; @@ -305,40 +311,45 @@ export class Scrawn< } /** - * Create a type-safe reference to a persisted expression. + * Create a type-safe reference to a persisted expression, inline expression, + * tag, or raw amount. All expressions should go through `biller.expr()`. * * Expression names are compile-time checked against known expressions * synced from the Scrawn server. The backend resolves the stored * expression string and evaluates it at runtime. * - * Also accepts inline PriceExpr as a passthrough for a consistent - * `biller.expr()` entry point for all expressions. + * Also accepts inline PriceExpr, TagExpr, and raw numbers as a unified + * `biller.expr()` entry point. * - * @param nameOrExpr - The persisted expression name or an inline PriceExpr - * @returns An ExprRef (if name) or the original PriceExpr (passthrough) + * @param value - A persisted expression name, inline PriceExpr, TagExpr, or raw number + * @returns A PriceExpr representing the expression * * @example * ```typescript * // Reference a persisted expression - * biller.basicUsageEventConsumer({ - * userId: 'u123', - * debitExpr: biller.expr("MY_EXPR"), - * }); + * biller.expr("MY_EXPR") * - * // Inline expression passthrough - * biller.basicUsageEventConsumer({ - * userId: 'u123', - * debitExpr: biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)), - * }); + * // Inline expression + * biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)) + * + * // Wrap a tag + * biller.expr(biller.tag("EXTRA_FEE")) + * + * // Wrap a raw amount + * biller.expr(250) * ``` */ - expr(name: T): PriceExpr; - expr(expr: PriceExpr): PriceExpr; - expr(value: string | PriceExpr): PriceExpr { + expr(amount: number): ExprValue; + expr(name: T): ExprValue; + expr(expr: PriceExpr): ExprValue; + expr(value: string | number | PriceExpr): ExprValue { if (typeof value === "string") { - return { kind: "exprRef", name: value } as PriceExpr; + return toExprValue({ kind: "exprRef", name: value } as PriceExpr); + } + if (typeof value === "number") { + return toExprValue({ kind: "amount", value } as PriceExpr); } - return value; + return toExprValue(value); } /** diff --git a/packages/scrawn/src/core/types/event.ts b/packages/scrawn/src/core/types/event.ts index c32fc68..365de56 100644 --- a/packages/scrawn/src/core/types/event.ts +++ b/packages/scrawn/src/core/types/event.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { PriceExpr } from "../pricing/types.js"; +import type { PriceExpr, TagExpr, ExprValue } from "../pricing/types.js"; import type { ScrawnError } from "../errors/index.js"; import { isValidExpr, containsTokenExpr } from "../pricing/validate.js"; @@ -122,7 +122,10 @@ export const EventPayloadSchema = z.object({ * debit: biller.expr("SAVED_RATE") * ``` */ -export type Debit = number | PriceExpr; +export type Debit = + | number + | TagExpr + | ExprValue; /** * Payload structure for event tracking.