/** * 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 { return (await Config.get()).billing?.llm ?? undefined } /** The user-facing compute spend toggle. Defaults to "byok" (own GPU providers). */ export async function computeBillingMode(): Promise { 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 { // 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" }