diff --git a/deco-ai-gateway/server/db/mcps.code-workspace b/deco-ai-gateway/server/db/mcps.code-workspace new file mode 100644 index 00000000..a1485d8f --- /dev/null +++ b/deco-ai-gateway/server/db/mcps.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../.." + }, + { + "path": "../../../../mesh" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/deco-ai-gateway/server/db/schema.sql b/deco-ai-gateway/server/db/schema.sql index 79b42254..50cf6d1a 100644 --- a/deco-ai-gateway/server/db/schema.sql +++ b/deco-ai-gateway/server/db/schema.sql @@ -50,7 +50,8 @@ CREATE TABLE IF NOT EXISTS llm_gateway_connections ( encryption_iv TEXT, -- 12-byte Initialization Vector (hex) encryption_tag TEXT, -- 16-byte auth tag for integrity verification (hex) billing_mode TEXT NOT NULL DEFAULT 'prepaid', -- 'prepaid' (buy credits) or 'postpaid' (pay per use) - is_subscription BOOLEAN NOT NULL DEFAULT FALSE, -- FALSE = wallet (credit never resets), TRUE = subscription (resets monthly) + is_subscription BOOLEAN NOT NULL DEFAULT FALSE, -- Kept for backwards compat; use limit_period instead + limit_period TEXT CHECK (limit_period IN ('daily', 'weekly', 'monthly')), -- NULL = no reset, 'monthly' etc = OpenRouter auto-reset usage_markup_pct NUMERIC(5,2) NOT NULL DEFAULT 15, -- Surcharge % on top of OpenRouter cost (e.g. 30 = 30%) max_limit_usd NUMERIC(10,4) DEFAULT NULL, -- Maximum spending limit cap (NULL = no cap) alert_enabled BOOLEAN NOT NULL DEFAULT FALSE, -- Whether low-balance email alerts are on @@ -324,6 +325,31 @@ BEGIN END IF; END $$; +-- Migration: Add limit_period column (replaces is_subscription logic) +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'llm_gateway_connections' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'llm_gateway_connections' + AND column_name = 'limit_period' + ) THEN + ALTER TABLE llm_gateway_connections + ADD COLUMN limit_period TEXT CHECK (limit_period IN ('daily', 'weekly', 'monthly')); + + -- Migrate existing is_subscription=true rows to limit_period='monthly' + UPDATE llm_gateway_connections + SET limit_period = 'monthly' + WHERE is_subscription = TRUE AND limit_period IS NULL; + + RAISE NOTICE 'Migration: Added column limit_period and migrated is_subscription data'; + END IF; +END $$; + -- Migration: Add organization_id index to payments (if not exists) CREATE INDEX IF NOT EXISTS idx_llm_gw_pay_org ON llm_gateway_payments(organization_id); diff --git a/deco-ai-gateway/server/lib/config-cache.ts b/deco-ai-gateway/server/lib/config-cache.ts index 91c2ec3e..5b674aa5 100644 --- a/deco-ai-gateway/server/lib/config-cache.ts +++ b/deco-ai-gateway/server/lib/config-cache.ts @@ -5,6 +5,7 @@ import { isSupabaseConfigured, type LlmGatewayConnectionRow, type BillingMode, + type LimitPeriod, } from "./supabase-client.ts"; import { logger } from "./logger.ts"; import { DEFAULT_MARKUP_PCT } from "./constants.ts"; @@ -80,12 +81,13 @@ export async function saveApiKey(params: { openrouterKeyName: string; openrouterKeyHash: string; billingMode?: BillingMode; - isSubscription?: boolean; + limitPeriod?: LimitPeriod | null; usageMarkupPct?: number; maxLimitUsd?: number | null; }): Promise { const { ciphertext, iv, tag } = encrypt(params.apiKey); const now = new Date().toISOString(); + const limitPeriod = params.limitPeriod ?? null; const row: LlmGatewayConnectionRow = { connection_id: params.connectionId, @@ -97,7 +99,8 @@ export async function saveApiKey(params: { encryption_iv: iv, encryption_tag: tag, billing_mode: params.billingMode ?? "prepaid", - is_subscription: params.isSubscription ?? false, + is_subscription: limitPeriod === "monthly", + limit_period: limitPeriod, usage_markup_pct: params.usageMarkupPct ?? DEFAULT_MARKUP_PCT, max_limit_usd: params.maxLimitUsd ?? null, alert_enabled: false, diff --git a/deco-ai-gateway/server/lib/confirm-payment-service.ts b/deco-ai-gateway/server/lib/confirm-payment-service.ts index 8497338a..a67eabd0 100644 --- a/deco-ai-gateway/server/lib/confirm-payment-service.ts +++ b/deco-ai-gateway/server/lib/confirm-payment-service.ts @@ -72,11 +72,11 @@ export async function confirmPaymentForConnection( }; } - const isSubscription = row.is_subscription ?? false; + const limitPeriod = row.limit_period ?? null; await updateKeyLimit( row.openrouter_key_hash, payment.new_limit_usd, - isSubscription ? "monthly" : null, + limitPeriod, false, ); await markPaymentCompleted(payment.id); diff --git a/deco-ai-gateway/server/lib/provisioning.ts b/deco-ai-gateway/server/lib/provisioning.ts index f5eab473..d82ff5c4 100644 --- a/deco-ai-gateway/server/lib/provisioning.ts +++ b/deco-ai-gateway/server/lib/provisioning.ts @@ -3,6 +3,7 @@ import { isSupabaseConfigured, findExistingOrgConnection, type BillingMode, + type LimitPeriod, } from "./supabase-client.ts"; import { decrypt } from "./encryption.ts"; import { updateKeyLimit, type LimitReset } from "./openrouter-keys.ts"; @@ -61,7 +62,7 @@ async function createOpenRouterKey( organizationId: string, organizationName: string | undefined, billingMode: BillingMode, - isSubscription: boolean, + limitPeriod: LimitPeriod | null, ): Promise<{ key: string; hash: string; name: string }> { const managementKey = process.env.OPENROUTER_MANAGEMENT_KEY; if (!managementKey) { @@ -90,7 +91,7 @@ async function createOpenRouterKey( billingMode === "prepaid" ? DEFAULT_LIMIT_USD : DEFAULT_POSTPAID_LIMIT_USD, - limit_reset: isSubscription ? "monthly" : undefined, + limit_reset: limitPeriod ?? undefined, }), }); @@ -140,7 +141,7 @@ async function provisionOrReuseKey( meshUrl: string, organizationName: string | undefined, billingMode: BillingMode, - isSubscription: boolean, + limitPeriod: LimitPeriod | null, ): Promise { const existingOrgLock = orgProvisioningLocks.get(organizationId); if (existingOrgLock) { @@ -157,7 +158,7 @@ async function provisionOrReuseKey( meshUrl, organizationName, billingMode, - isSubscription, + limitPeriod, ); } @@ -195,7 +196,7 @@ async function provisionOrReuseKey( openrouterKeyName: existingOrgRow.openrouter_key_name ?? "", openrouterKeyHash: existingOrgRow.openrouter_key_hash ?? "", billingMode: existingOrgRow.billing_mode, - isSubscription: existingOrgRow.is_subscription, + limitPeriod: existingOrgRow.limit_period ?? null, usageMarkupPct: existingOrgRow.usage_markup_pct, maxLimitUsd: existingOrgRow.max_limit_usd, }); @@ -215,10 +216,10 @@ async function provisionOrReuseKey( organizationId, organizationName, billingMode, - isSubscription, + limitPeriod, ); - const limitReset: LimitReset | null = isSubscription ? "monthly" : null; + const limitReset: LimitReset | null = limitPeriod ?? null; const defaultLimit = billingMode === "prepaid" ? DEFAULT_LIMIT_USD @@ -229,7 +230,7 @@ async function provisionOrReuseKey( keyHash: hash, billingMode, limitUsd: defaultLimit, - isSubscription, + limitPeriod, }); const limitResult = await updateKeyLimit( hash, @@ -255,7 +256,7 @@ async function provisionOrReuseKey( openrouterKeyName: name, openrouterKeyHash: hash, billingMode, - isSubscription, + limitPeriod, }); logger.info("Key provisioned and persisted", { @@ -289,7 +290,7 @@ export async function ensureApiKey( meshUrl: string, organizationName?: string, billingMode: BillingMode = "prepaid", - isSubscription = false, + limitPeriod: LimitPeriod | null = null, ): Promise { logger.debug("ensureApiKey called", { connectionId, organizationId }); @@ -337,7 +338,7 @@ export async function ensureApiKey( meshUrl, organizationName, billingMode, - isSubscription, + limitPeriod, ); } catch (error) { logger.error("Failed to provision OpenRouter API key", { diff --git a/deco-ai-gateway/server/lib/supabase-client.ts b/deco-ai-gateway/server/lib/supabase-client.ts index 929175c7..d96cfc06 100644 --- a/deco-ai-gateway/server/lib/supabase-client.ts +++ b/deco-ai-gateway/server/lib/supabase-client.ts @@ -2,6 +2,7 @@ import { createClient, type SupabaseClient } from "@supabase/supabase-js"; import { logger } from "./logger.ts"; export type BillingMode = "prepaid" | "postpaid"; +export type LimitPeriod = "daily" | "weekly" | "monthly"; export interface LlmGatewayConnectionRow { connection_id: string; @@ -14,6 +15,7 @@ export interface LlmGatewayConnectionRow { encryption_tag: string | null; billing_mode: BillingMode; is_subscription: boolean; + limit_period: LimitPeriod | null; usage_markup_pct: number; max_limit_usd: number | null; alert_enabled: boolean; @@ -177,6 +179,7 @@ export async function deleteConnectionConfig( export interface BillingConfig { billingMode: BillingMode; isSubscription: boolean; + limitPeriod: LimitPeriod | null; usageMarkupPct: number; maxLimitUsd: number | null; } @@ -199,6 +202,11 @@ export async function updateBillingConfig( if (config.isSubscription !== undefined) { updates.is_subscription = config.isSubscription; } + if ("limitPeriod" in config) { + updates.limit_period = config.limitPeriod ?? null; + // Keep is_subscription in sync for backwards compat + updates.is_subscription = config.limitPeriod === "monthly"; + } if (config.usageMarkupPct !== undefined) { updates.usage_markup_pct = config.usageMarkupPct; } diff --git a/deco-ai-gateway/server/tools/credits.ts b/deco-ai-gateway/server/tools/credits.ts index 0477f5a1..9439f59d 100644 --- a/deco-ai-gateway/server/tools/credits.ts +++ b/deco-ai-gateway/server/tools/credits.ts @@ -10,20 +10,33 @@ export const createGatewayCreditsTool = (env: Env) => createTool({ id: "GATEWAY_CREDITS", description: - "Returns the available credit balance for this organization's AI Gateway. " + - "Use this to check how much credit remains before making LLM calls.", + "Returns the current balance or usage for this organization's AI Gateway. " + + "For prepaid mode: shows available credit. " + + "For postpaid mode: shows usage vs limit (if set) or raw usage.", inputSchema: z.object({}).strict(), outputSchema: z .object({ available: z .number() .nullable() - .describe("Remaining credit balance in USD (null = unlimited)"), + .describe( + "Remaining credit in USD (prepaid) or remaining limit headroom (postpaid with limit). Null = unlimited.", + ), total: z .number() .nullable() - .describe("Total credit limit in USD (null = unlimited)"), + .describe("Total credit/limit in USD (null = unlimited)"), used: z.number().describe("Total amount spent in USD"), + percentUsed: z + .number() + .nullable() + .describe( + "Percentage of limit used (0-100). Null when no limit is set.", + ), + limitPeriod: z + .enum(["daily", "weekly", "monthly"]) + .nullable() + .describe("Reset period for the limit (null = no reset / wallet)"), billingMode: z .enum(["prepaid", "postpaid"]) .describe("Billing mode for this organization"), @@ -48,6 +61,8 @@ export const createGatewayCreditsTool = (env: Env) => available: DEFAULT_LIMIT_USD, total: DEFAULT_LIMIT_USD, used: 0, + percentUsed: 0, + limitPeriod: null, billingMode: (row?.billing_mode ?? "prepaid") as | "prepaid" | "postpaid", @@ -59,11 +74,23 @@ export const createGatewayCreditsTool = (env: Env) => const billingMode = (row.billing_mode ?? "prepaid") as | "prepaid" | "postpaid"; + const limitPeriod = (row.limit_period ?? null) as + | "daily" + | "weekly" + | "monthly" + | null; + + const percentUsed = + d.limit != null && d.limit > 0 + ? Math.min(100, Math.round((d.usage / d.limit) * 100 * 10) / 10) + : null; return { available: d.limit_remaining, total: d.limit, used: d.usage, + percentUsed, + limitPeriod, billingMode, keyDisabled: d.disabled, }; diff --git a/deco-ai-gateway/server/tools/index.ts b/deco-ai-gateway/server/tools/index.ts index c2c6b741..dfa001d5 100644 --- a/deco-ai-gateway/server/tools/index.ts +++ b/deco-ai-gateway/server/tools/index.ts @@ -28,7 +28,7 @@ async function ensureKeyLimitMatchesBillingMode( if (!verifiedConnections.has(connectionId)) { const billingMode = row.billing_mode ?? "prepaid"; - const isSubscription = row.is_subscription ?? false; + const limitPeriod = row.limit_period ?? null; const details = await getKeyDetails(row.openrouter_key_hash); const expectedDefault = @@ -41,12 +41,12 @@ async function ensureKeyLimitMatchesBillingMode( connectionId, billingMode, defaultLimit: expectedDefault, - isSubscription, + limitPeriod, }); await updateKeyLimit( row.openrouter_key_hash, expectedDefault, - isSubscription ? "monthly" : null, + limitPeriod, false, ); } diff --git a/deco-ai-gateway/server/tools/set-limit.ts b/deco-ai-gateway/server/tools/set-limit.ts index 154c4be7..14bedf04 100644 --- a/deco-ai-gateway/server/tools/set-limit.ts +++ b/deco-ai-gateway/server/tools/set-limit.ts @@ -5,6 +5,9 @@ import { loadPendingPayment, savePendingPayment, markPaymentExpired, + updateBillingConfig, + type BillingMode, + type LimitPeriod, } from "../lib/supabase-client.ts"; import { getKeyDetails, updateKeyLimit } from "../lib/openrouter-keys.ts"; import { @@ -51,6 +54,7 @@ const outputSchema = z .object({ summary: z.string(), billing_mode: z.enum(["prepaid", "postpaid"]), + limit_period: z.enum(["daily", "weekly", "monthly"]).nullable(), checkout_url: z.string().nullable(), amount_usd: z.number().nullable(), markup_pct: z.number(), @@ -74,7 +78,19 @@ export const createSetLimitTool = (env: Env) => .number() .positive() .describe( - "Desired new spending limit in USD. Must be greater than the current limit. Examples: 5, 10, 50", + "Desired new spending limit in USD. In prepaid mode must be greater than the current limit. In postpaid mode can be set to any positive value. Examples: 5, 10, 50", + ), + limit_period: z + .enum(["daily", "weekly", "monthly", "none"]) + .optional() + .describe( + "Limit reset period. Only applies to postpaid mode. 'none' removes the reset. Omit to keep the current period.", + ), + billing_mode: z + .enum(["prepaid", "postpaid"]) + .optional() + .describe( + "Optional explicit billing mode. Useful for migrating legacy connections where DB mode is stale.", ), return_url: z .string() @@ -87,7 +103,12 @@ export const createSetLimitTool = (env: Env) => .strict(), outputSchema, execute: async ({ context }) => { - const { limit_usd: newLimit, return_url: returnUrl } = context; + const { + limit_usd: newLimit, + limit_period: limitPeriodInput, + billing_mode: billingModeInput, + return_url: returnUrl, + } = context; const connectionId = env.MESH_REQUEST_CONTEXT?.connectionId; const organizationId = env.MESH_REQUEST_CONTEXT?.organizationId; @@ -108,31 +129,42 @@ export const createSetLimitTool = (env: Env) => const keyDetails = await getKeyDetails(row.openrouter_key_hash); const currentLimit = keyDetails.limit ?? 0; - if (newLimit <= currentLimit) { - throw new Error( - `New limit ($${newLimit.toFixed(2)}) must be greater than current limit ($${currentLimit.toFixed(2)}).`, - ); - } - const effectiveCap = row.max_limit_usd ?? HARD_CAP_USD; if (newLimit > effectiveCap) { throw new Error( `New limit ($${newLimit.toFixed(2)}) exceeds the maximum allowed ($${effectiveCap.toFixed(2)}).`, ); } - - const billingMode = row.billing_mode ?? "prepaid"; const markupPct = row.usage_markup_pct ?? 0; - const isSubscription = row.is_subscription ?? false; - if (billingMode === "postpaid") { + // Resolve limit_period: explicit input overrides DB value; "none" clears it + const currentLimitPeriod = row.limit_period ?? null; + const resolvedLimitPeriod: LimitPeriod | null = + limitPeriodInput === undefined + ? currentLimitPeriod + : limitPeriodInput === "none" + ? null + : (limitPeriodInput as LimitPeriod); + + const billingModeFromDb = row.billing_mode ?? "prepaid"; + const effectiveBillingMode: BillingMode = + billingModeInput ?? + (limitPeriodInput !== undefined ? "postpaid" : billingModeFromDb); + + if (effectiveBillingMode === "prepaid" && newLimit <= currentLimit) { + throw new Error( + `New limit ($${newLimit.toFixed(2)}) must be greater than current limit ($${currentLimit.toFixed(2)}).`, + ); + } + + if (effectiveBillingMode === "postpaid") { return handlePostpaid( row.openrouter_key_hash, currentLimit, newLimit, connectionId, markupPct, - isSubscription, + resolvedLimitPeriod, ); } @@ -154,26 +186,32 @@ async function handlePostpaid( newLimit: number, connectionId: string, markupPct: number, - isSubscription: boolean, + limitPeriod: LimitPeriod | null, ): Promise> { - await updateKeyLimit( - keyHash, - newLimit, - isSubscription ? "monthly" : null, - false, - ); + await updateKeyLimit(keyHash, newLimit, limitPeriod, false); + + await updateBillingConfig(connectionId, { + billingMode: "postpaid", + isSubscription: limitPeriod === "monthly", + limitPeriod, + }); logger.info("Postpaid limit updated directly", { connectionId, currentLimit, newLimit, markupPct, + limitPeriod, }); + const periodLabel = limitPeriod + ? { daily: "daily", weekly: "weekly", monthly: "monthly" }[limitPeriod] + : null; + const lines = [ `Spending limit updated (postpaid mode).`, `Previous limit: $${currentLimit.toFixed(2)}`, - `New limit: $${newLimit.toFixed(2)}`, + `New limit: $${newLimit.toFixed(2)}${periodLabel ? ` (resets ${periodLabel})` : ""}`, ]; if (markupPct > 0) { lines.push(`Usage markup: ${markupPct}%`); @@ -182,6 +220,7 @@ async function handlePostpaid( return { summary: lines.join("\n"), billing_mode: "postpaid", + limit_period: limitPeriod, checkout_url: null, amount_usd: null, markup_pct: markupPct, @@ -286,6 +325,7 @@ async function handlePrepaid( return { summary: lines.join("\n"), billing_mode: "prepaid", + limit_period: null, checkout_url: checkoutUrl, amount_usd: chargeUsd, markup_pct: markupPct, diff --git a/deco-ai-gateway/server/tools/usage.ts b/deco-ai-gateway/server/tools/usage.ts index 186154af..b8339fb1 100644 --- a/deco-ai-gateway/server/tools/usage.ts +++ b/deco-ai-gateway/server/tools/usage.ts @@ -30,6 +30,7 @@ export const createGatewayUsageTool = (env: Env) => }), billing: z.object({ mode: z.enum(["prepaid", "postpaid"]), + limitPeriod: z.enum(["daily", "weekly", "monthly"]).nullable(), }), limit: z.object({ total: z.number().nullable(), @@ -106,6 +107,11 @@ export const createGatewayUsageTool = (env: Env) => const d = await getKeyDetails(row.openrouter_key_hash); const billingMode = row.billing_mode ?? "prepaid"; + const limitPeriod = (row.limit_period ?? null) as + | "daily" + | "weekly" + | "monthly" + | null; const estimation: CreditEstimation | null = estimateCreditDuration({ limitRemaining: d.limit_remaining, @@ -116,33 +122,61 @@ export const createGatewayUsageTool = (env: Env) => keyCreatedAt: d.created_at, }); - const limitLine = d.limit - ? `Limit: $${d.limit.toFixed(4)} | Remaining: $${(d.limit_remaining ?? 0).toFixed(4)} | Reset: ${d.limit_reset ?? "none"}` - : "Limit: none"; + const percentUsed = + d.limit != null && d.limit > 0 + ? Math.min(100, Math.round((d.usage / d.limit) * 100 * 10) / 10) + : null; - let forecastLabel: string; - if (estimation) { - forecastLabel = estimationSummary(estimation); - } else if (d.limit == null) { - forecastLabel = "No spending limit set — usage is unlimited."; - } else if ((d.limit_remaining ?? 0) <= 0) { - forecastLabel = "Credit exhausted."; + let summaryLines: string[]; + if (billingMode === "postpaid") { + if (d.limit != null) { + const periodLabel = limitPeriod ?? "no reset"; + const usageVsLimitLine = + percentUsed == null + ? `Usage: $${d.usage.toFixed(4)} of $${d.limit.toFixed(2)}` + : `Usage: $${d.usage.toFixed(4)} of $${d.limit.toFixed(2)} — ${percentUsed}% used`; + summaryLines = [ + `Key: ${d.name} | Status: ${d.disabled ? "disabled" : "active"}`, + `Billing: postpaid | Limit: $${d.limit.toFixed(2)} (${periodLabel})`, + usageVsLimitLine, + `Period usage — Daily: $${d.usage_daily.toFixed(4)} | Weekly: $${d.usage_weekly.toFixed(4)} | Monthly: $${d.usage_monthly.toFixed(4)}`, + ]; + if (d.limit_reset) { + summaryLines.push(`Next reset: ${d.limit_reset}`); + } + } else { + summaryLines = [ + `Key: ${d.name} | Status: ${d.disabled ? "disabled" : "active"}`, + `Billing: postpaid | No spending limit configured`, + `Usage — Total: $${d.usage.toFixed(4)} | Daily: $${d.usage_daily.toFixed(4)} | Weekly: $${d.usage_weekly.toFixed(4)} | Monthly: $${d.usage_monthly.toFixed(4)}`, + ]; + } } else { - forecastLabel = "No usage yet — estimation not available."; - } + const limitLine = d.limit + ? `Limit: $${d.limit.toFixed(4)} | Remaining: $${(d.limit_remaining ?? 0).toFixed(4)}` + : "Limit: none"; + + let forecastLabel: string; + if (estimation) { + forecastLabel = estimationSummary(estimation); + } else if (d.limit == null) { + forecastLabel = "No spending limit set — usage is unlimited."; + } else if ((d.limit_remaining ?? 0) <= 0) { + forecastLabel = "Credit exhausted."; + } else { + forecastLabel = "No usage yet — estimation not available."; + } - const lines = [ - `Key: ${d.name}`, - `Status: ${d.disabled ? "disabled" : "active"}`, - ...(billingMode === "postpaid" ? ["Billing: postpaid"] : []), - `Usage — Total: $${d.usage.toFixed(6)} | Daily: $${d.usage_daily.toFixed(6)} | Weekly: $${d.usage_weekly.toFixed(6)} | Monthly: $${d.usage_monthly.toFixed(6)}`, - `BYOK — Total: $${d.byok_usage.toFixed(6)} | Daily: $${d.byok_usage_daily.toFixed(6)} | Weekly: $${d.byok_usage_weekly.toFixed(6)} | Monthly: $${d.byok_usage_monthly.toFixed(6)}`, - limitLine, - `Forecast: ${forecastLabel}`, - ]; + summaryLines = [ + `Key: ${d.name} | Status: ${d.disabled ? "disabled" : "active"}`, + `Usage — Total: $${d.usage.toFixed(6)} | Daily: $${d.usage_daily.toFixed(6)} | Weekly: $${d.usage_weekly.toFixed(6)} | Monthly: $${d.usage_monthly.toFixed(6)}`, + limitLine, + `Forecast: ${forecastLabel}`, + ]; + } return { - summary: lines.join("\n"), + summary: summaryLines.join("\n"), key: { name: d.name, label: d.label, @@ -153,6 +187,7 @@ export const createGatewayUsageTool = (env: Env) => }, billing: { mode: billingMode, + limitPeriod, }, limit: { total: d.limit,