117 lines · 5.2 KB · TypeScript
RawDownload
/** * Billing gate predicates for LLM calls. * * The openscience CLI wallet only pays for *managed-proxy* calls. Everything else — * a user's own key (BYOK) or a first-party OAuth subscription (Claude Pro/Max, * Sign in with ChatGPT, Copilot) — costs the wallet nothing and must never be * blocked by it or reported for billing. * * Which class a call falls into is decided by the *credential*, not the * provider id: the same providerID (e.g. "anthropic") can be BYOK, managed * (a synced thk_* proxy token), or OAuth. `resolveCredentialSource` inspects * the resolved key and returns the authoritative class; the gate predicates * then key off that so the pre-flight balance check and the post-step usage * report agree on exactly what is billable. */ import { Auth } from "@/auth"import { Config } from "@/config/config"import { Env } from "@/env"import { Provider } from "@/provider/provider"import { OpenScience } from "@/openscience" export type CredentialSource = "byok" | "managed" | "oauth-free"export type BillingMode = "managed" | "byok" /** The user-facing LLM spend toggle (Settings → Spend). `undefined` = auto-detect *  from the resolved credential (legacy behaviour; `null` in the config file — *  the toggle set back to auto — normalizes to the same thing). */export async function llmBillingMode(): Promise<BillingMode | undefined> {  return (await Config.get()).billing?.llm ?? undefined} /** The user-facing compute spend toggle. Defaults to "byok" (own GPU providers). */export async function computeBillingMode(): Promise<BillingMode> {  return (await Config.get()).billing?.compute ?? "byok"} /** First-party providers whose OAuth path runs on the user's own subscription *  and never debits the CLI wallet. */const OAUTH_FREE_PROVIDERS = new Set([  "anthropic",  "openai",  "openai-codex",  "github-copilot",  "github-copilot-enterprise",]) /** The synthesized Codex OAuth provider (see provider/provider.ts). */export function isCodexOAuthProvider(providerID: string): boolean {  return providerID === "openai-codex"} /** * Classify the credential backing a call as one of: *   - "managed"    — a thk_* Atlas proxy token or a dashboard-synced secret. *                    Debits the wallet + is reported for billing. *   - "byok"       — the user's own api key (auth.json or shell env). *   - "oauth-free" — a first-party OAuth subscription (Claude/ChatGPT/Copilot). * * Only "managed" is billable. Managed detection wins over OAuth/BYOK because a * synced proxy token can be attached to any provider id. */export async function resolveCredentialSource(providerID: string, _modelID: string): Promise<CredentialSource> {  // Explicit BYOK toggle: the user opted out of managed billing for LLM calls, so  // classify as the user's own account (byok / oauth-free) and never fire the wallet  // gate. The Atlas proxy still meters any managed key server-side, so this cannot  // create a free-managed-inference loophole — it only governs the client pre-flight.  if ((await llmBillingMode()) === "byok") {    const auth = await Auth.get(providerID).catch(() => undefined)    return auth?.type === "oauth" ? "oauth-free" : "byok"  }   const provider = await Provider.getProvider(providerID).catch(() => undefined)   // 1) Managed: a thk_* proxy token. Classified by VALUE, not by how the  //    credential arrived: the dashboard sync also delivers the user's own  //    keys (OPENROUTER_API_KEY etc.), and treating "arrived via sync" as  //    "managed" wallet-gated and billed BYOK keys — exactly what this  //    module's contract forbids. It was also boot-order dependent (the  //    synced-secret set is empty until an in-process sync runs).  const optionKey = provider?.options?.["apiKey"]  const resolvedKey = typeof provider?.key === "string" ? provider.key : undefined  const explicitKey = typeof optionKey === "string" ? optionKey : undefined  if (OpenScience.isManagedKeyValue(resolvedKey) || OpenScience.isManagedKeyValue(explicitKey)) {    return "managed"  }  const envKeys = provider?.env ?? []  for (const key of envKeys) {    if (OpenScience.isManagedKeyValue(Env.get(key))) return "managed"  }   // 2) OAuth-free: a first-party OAuth subscription (user's own account).  const auth = await Auth.get(providerID).catch(() => undefined)  if (auth?.type === "oauth") return "oauth-free"  if (OAUTH_FREE_PROVIDERS.has(providerID) && !resolvedKey && !explicitKey && !auth) return "oauth-free"   // 3) BYOK: the user's own key (or the zero-cost public demo). Never billable.  return "byok"} /** * Whether the pre-flight CLI-wallet balance check should run for this call. * Only managed-proxy credentials draw down the wallet, so BYOK and OAuth-free * calls must skip the check entirely — an empty wallet never blocks them. */export function requiresWalletBalance(source: CredentialSource): boolean {  return source === "managed"} /** * Whether a completed step should be reported to /api/cli/usage for billing. * Only managed-proxy credentials are billed; BYOK and OAuth-free calls run on * the user's own account and are never reported. */export function shouldReportUsage(source: CredentialSource): boolean {  return source === "managed"}