194 lines · 8.3 KB
import { env } from "cloudflare:workers";
import { stripeEnabled } from "../billing/config";
import { MARKUP, METER_PRICES } from "~/lib/pricing";
import {
  commitUsage,
  PlatformLimitError,
  QuotaExceededError,
  releaseUsage,
  remainingQuota,
  reserveUsage,
} from "./quota";
import type { Reservation } from "./quota";
import { isRecorded, recordUsage } from "./record";
import { captureServerEvent } from "../analytics";
import { addCounter, counterTotals } from "../counters";

export type AiMeterContext = {
  accountId: string;
  repoId?: string | null;
  /** The feature making the calls, e.g. "mergefix" or "clef". */
  feature: string;
  /** A durable id for the unit of work (run id); call keys are `ai:<scope>:<index>`. */
  scope: string;
};

export type AiTokens = { inputTokens: number; outputTokens: number };

/** Tokens held for a call before it starts (or what is left, if less); afterCall settles it at the real count. */
export const AI_TOKEN_HOLD = 8_000;
const AI_TOKEN_HOLD_TTL_MS = 15 * 60 * 1000;
const DEFAULT_DAILY_CEILING_CENTS = 10_000;

/** The ceiling in cents, null when off, or NaN when misconfigured (which refuses every call). */
export function dailyAiCeilingCents(raw = env.AI_DAILY_SPEND_CEILING_CENTS): number | null {
  // Unset: on for Stripe-billed instances, off for self-hosted ones that never opted in.
  if (raw === undefined || raw.trim() === "") return stripeEnabled() ? DEFAULT_DAILY_CEILING_CENTS : null;
  if (raw.trim().toLowerCase() === "off") return null;
  const cents = Number(raw);
  return Number.isFinite(cents) && cents >= 0 ? cents : Number.NaN;
}