From 03a791c9bfc8bd2433b87ff4c41d6943ba958b38 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 28 Sep 2026 14:01:14 -0700 Subject: [PATCH] =?UTF-8?q?easel-inference:=20bill=20the=20open=20models?= =?UTF-8?q?=20in=20braincells=20at=202=C3=97=20OpenRouter's=20cost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the relay now offers the six open models aesel's openrouter bridge has (deepseek v4.1 flash is the default) and settles each request from the `usage.cost` OpenRouter reports, at INFERENCE_MARKUP ร— BRAINCELLS_PER_USD. no cost falls back to the model's fixed rate. the free allowance and the wallet count the same braincell; free is drawn first. guards: max_tokens up to 32,000 with a proportional hold, a 2,000,000 braincell daily cap on the wallet (429 says when it resets), settlement keyed by hold id so a repeat changes nothing, and one plain 402. Co-Authored-By: Claude Opus 5.5 --- system/backend/easel-paid-credits.mjs | 83 ++++++++++--- system/backend/easel-policy.mjs | 27 +++-- system/backend/easel-stream.mjs | 4 +- system/netlify/functions/easel-credits.mjs | 2 +- system/netlify/functions/easel-inference.mjs | 25 ++-- system/tests/easel-braincells.test.mjs | 117 +++++++++++++++++++ system/tests/easel-paid-credits.test.mjs | 2 +- 7 files changed, 223 insertions(+), 37 deletions(-) create mode 100644 system/tests/easel-braincells.test.mjs diff --git a/system/backend/easel-paid-credits.mjs b/system/backend/easel-paid-credits.mjs index 860671f227..19a056b77f 100644 --- a/system/backend/easel-paid-credits.mjs +++ b/system/backend/easel-paid-credits.mjs @@ -1,7 +1,17 @@ -// Purchased credits are separate from the resetting free allowance. +// Purchased credits are separate from the resetting free allowance. Both are +// counted in the same braincell, and a hosted request draws on the free +// allowance first; only once today's is spent does it reserve from the wallet. import { randomUUID } from 'node:crypto'; import { connect } from './database.mjs'; +import { dayKey } from './ai-budget.mjs'; export const CREDIT_PACK = Object.freeze({ id:'braincells-1m-v1', amount:500, currency:'usd', credits:1_000_000, unit:'braincells' }); +// What a braincell is worth: the pack's price, so $5 buys 1,000,000. +export const BRAINCELLS_PER_USD = CREDIT_PACK.credits / (CREDIT_PACK.amount / 100); +// Hosted inference sells at twice what OpenRouter charges AC for it. +export const INFERENCE_MARKUP = 2; +// The most one handle may spend from its wallet in a UTC day ($10 at pack +// price). A runaway loop stops here rather than draining a balance overnight. +export const DAILY_PAID_BRAINCELL_CAP = 2_000_000; // The same pack sold through Apple in-app purchase; Apple keeps the price. export const IAP_PRODUCTS = Object.freeze({ 'computer.aesthetic.easel.braincells.1m': Object.freeze({ credits:1_000_000, unit:'braincells' }) }); const COLLECTION='ac-credit-wallets'; @@ -53,23 +63,54 @@ export function reservationSize(body,maxTokens,{validateMedia=true}={}) { if(validateMedia && /"type"\s*:\s*"(?:image|document|input_audio|video)"/.test(input))throw new Error('Braincells currently support hosted text and code requests.'); return Math.ceil(Buffer.byteLength(input,'utf8')*1.25)+maxTokens+4096; } -export async function reserve(user,amount,wallets,{id=randomUUID()}={}) { +// A hold is refused once today's settled spend reaches the cap. What a day has +// spent lives under `daily.`, one small key per day of use. +export async function reserve(user,amount,wallets,{id=randomUUID(),now=new Date(),cap=DAILY_PAID_BRAINCELL_CAP}={}) { if(!Number.isSafeInteger(amount)||amount<1)throw new Error('Invalid credit reservation'); - const result=await wallets.updateOne({_id:user,balance:{$gte:amount},[`holds.${id}`]:{$exists:false}},{$inc:{balance:-amount},$set:{[`holds.${id}`]:{amount,at:new Date()}}}); - return result.modifiedCount===1?{user,id,amount}:null; + const result=await wallets.updateOne({_id:user,balance:{$gte:amount},[`daily.${dayKey(now)}`]:{$not:{$gte:cap}},[`holds.${id}`]:{$exists:false}},{$inc:{balance:-amount},$set:{[`holds.${id}`]:{amount,at:now}}}); + return result.modifiedCount===1?{user,id,amount,day:dayKey(now)}:null; } -export async function settle(hold,spent,wallets) { - if(!hold)return; - if(!Number.isFinite(spent)||spent<0)throw new Error('Invalid metered usage'); - const charged=Math.min(hold.amount,Math.ceil(spent*(hold.rate||1))); - await wallets.updateOne({_id:hold.user,[`holds.${hold.id}.amount`]:hold.amount},{$inc:{balance:hold.amount-charged,spent:charged},$unset:{[`holds.${hold.id}`]:''},$set:{updatedAt:new Date()}}); +// Charge a finished request `braincells` against its hold and refund the rest. +// Keyed by the hold's id: the update only matches while that hold exists and it +// removes the hold, so a retried or duplicated settle changes nothing. +export async function settle(hold,braincells,wallets,{now=new Date()}={}) { + if(!hold)return false; + if(!Number.isFinite(braincells)||braincells<0)throw new Error('Invalid metered usage'); + const charged=Math.min(hold.amount,Math.ceil(braincells)); + const result=await wallets.updateOne({_id:hold.user,[`holds.${hold.id}.amount`]:hold.amount},{$inc:{balance:hold.amount-charged,spent:charged,[`daily.${hold.day||dayKey(now)}`]:charged},$unset:{[`holds.${hold.id}`]:''},$set:{updatedAt:now}}); + return result.modifiedCount===1; +} +// OpenRouter's reported cost, as braincells: twice the price, at pack value. +// Rounded to a millionth first so float noise (0.001 ร— 400,000) cannot tip a +// whole braincell. +export function braincellsFromCost(usd) { + return Math.ceil(Math.round(usd*INFERENCE_MARKUP*BRAINCELLS_PER_USD*1e6)/1e6); +} +// What a finished request costs in braincells. The provider's own cost when it +// reported one; otherwise the model's fixed rate over the weighted tokens. +export function usageBraincells({model,tokens=0,cost,inputBound=0}) { + if(Number.isFinite(cost)&&cost>=0)return braincellsFromCost(cost); + return Math.ceil(tokens*braincellRate(model,inputBound)); } -// Fixed braincell tariff, checked against OpenRouter's model catalog 2026-09-17. -// One balance works across models; these are consumption rates, not separate packs. +// OpenRouter's prices on 2026-09-28, USD per million tokens as input / output. +// Only the hold and the no-cost fallback read these; the bill is the cost the +// provider reports. +const OPEN_PRICES={ + 'deepseek/deepseek-v4.1-flash':[0.30,1.20], 'deepseek/deepseek-v4-pro':[0.78,1.57], + 'moonshotai/kimi-k3':[3.00,15.0], 'qwen/qwen3.7-plus':[0.32,1.28], + 'minimax/minimax-m3':[0.30,1.20], 'z-ai/glm-5.3-flash':[0.15,0.50], +}; +// Braincells per token, worst case: every token priced as output, marked up, +// rounded up to a hundredth. Flash comes to 0.48, Kimi K3 to 6. +const worstRate=([input,output])=>Math.ceil(Math.max(input,output)*INFERENCE_MARKUP*BRAINCELLS_PER_USD/1e4)/100; +// Fixed braincell tariff. The first seven were checked against OpenRouter's +// catalog 2026-09-17, relative to Luna; the open models derive from the prices +// above. One balance works across models; these are consumption rates. export const BRAINCELL_RATES=Object.freeze({ 'openai/gpt-5.6-luna':1, 'z-ai/glm-4.6':3, 'qwen/qwen3-coder':2, 'deepseek/deepseek-chat-v3.1':2, 'anthropic/claude-sonnet-4.6':15, 'anthropic/claude-opus-5':25, 'openai/gpt-5.4':13, + ...Object.fromEntries(Object.entries(OPEN_PRICES).map(([model,price])=>[model,worstRate(price)])), }); export function braincellRate(model,inputBound=0){ const base=BRAINCELL_RATES[model];if(!base)throw Error('This model has no braincell rate yet'); @@ -77,13 +118,23 @@ export function braincellRate(model,inputBound=0){ if(inputBound>=272000 && model==='openai/gpt-5.4')return 25; return base; } -export async function authorizePaidRequest({user,model,body,maxTokens}) { +export const OUT_OF_BRAINCELLS='Out of braincells โ€” buy more from the braincell meter in Aesel, or switch provider with /provider. Free braincells reset at midnight UTC.'; +// Hold the worst case this request could cost: its input and its whole output +// allowance at the model's rate, so a larger max_tokens holds more. +export async function authorizePaidRequest({user,model,body,maxTokens,now=new Date(),withWallets:using=withWallets}) { const inputBound=reservationSize(body,0); const rate=braincellRate(model,inputBound); - const amount=reservationSize(body,maxTokens)*rate; - const hold=await withWallets(wallets=>reserve(user,amount,wallets)); - if(!hold)throw Object.assign(new Error('Not enough braincells for this request. Open the braincell selector in Aesel to buy more, or start a shorter thread. Free braincells reset at midnight UTC.'),{statusCode:402}); - return {...hold,rate}; + const amount=Math.ceil(reservationSize(body,maxTokens)*rate); + return using(async wallets=>{ + const hold=await reserve(user,amount,wallets,{now}); + if(hold)return {...hold,rate,inputBound}; + const wallet=await wallets.findOne({_id:user},{projection:{[`daily.${dayKey(now)}`]:1}}); + if((Number(wallet?.daily?.[dayKey(now)])||0)>=DAILY_PAID_BRAINCELL_CAP){ + const minutes=Math.ceil((Date.parse(dayKey(now)+'T00:00:00Z')+86400000-now)/60000); + throw Object.assign(new Error(`That is today's limit of ${DAILY_PAID_BRAINCELL_CAP.toLocaleString('en-US')} bought braincells. It resets at midnight UTC, in ${Math.floor(minutes/60)}h ${minutes%60}m.`),{statusCode:429}); + } + throw Object.assign(new Error(OUT_OF_BRAINCELLS),{statusCode:402}); + }); } // Cumulative refund amounts make partial refunds and out-of-order redelivery diff --git a/system/backend/easel-policy.mjs b/system/backend/easel-policy.mjs index 496ac73273..0bc182339c 100644 --- a/system/backend/easel-policy.mjs +++ b/system/backend/easel-policy.mjs @@ -1,6 +1,14 @@ -// Explicit hosted selections. The account budget is token-based, not a dollar -// ceiling; premium models are never substituted for the inexpensive default. +// Explicit hosted selections. Every model is billed in braincells: at twice +// what OpenRouter reports it cost, or at the model's fixed rate when the +// provider reports no cost (easel-paid-credits.mjs). The older entries stay +// because installed clients still ask for them by name. export const EASEL_MODELS = { + "deepseek/deepseek-v4.1-flash": { label: "DeepSeek V4.1 Flash" }, + "deepseek/deepseek-v4-pro": { label: "DeepSeek V4 Pro" }, + "moonshotai/kimi-k3": { label: "Kimi K3" }, + "qwen/qwen3.7-plus": { label: "Qwen 3.7 Plus" }, + "minimax/minimax-m3": { label: "MiniMax M3" }, + "z-ai/glm-5.3-flash": { label: "GLM 5.3 Flash" }, "openai/gpt-5.6-luna": { label: "Luna" }, "anthropic/claude-opus-5": { label: "Opus (premium)" }, "z-ai/glm-4.6": { label: "glm" }, @@ -9,17 +17,22 @@ export const EASEL_MODELS = { "anthropic/claude-sonnet-4.6": { label: "sonnet (premium)" }, "openai/gpt-5.4": { label: "gpt (premium)" }, }; -export const DEFAULT_EASEL_MODEL = "openai/gpt-5.6-luna"; -const MAX_TOKENS = 8192; +// Best of the 2026-09-28 trials, and among the cheapest to run. +export const DEFAULT_EASEL_MODEL = "deepseek/deepseek-v4.1-flash"; +// What a request gets when it does not say, and the most it may ask for. A +// workspace turn writes whole files, so the ceiling is the client's 32,000; +// the braincell hold grows with it, so a long answer is paid for up front. +export const DEFAULT_MAX_TOKENS = 8192; +export const HOSTED_MAX_TOKENS = 32000; export function inferenceRequest(body) { if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Expected an inference request object."); const model = body.model === undefined ? DEFAULT_EASEL_MODEL : body.model; if (typeof model !== "string" || !Object.hasOwn(EASEL_MODELS, model)) throw new Error("Unsupported model. Select a model from /model."); - const wanted = body.max_tokens === undefined ? MAX_TOKENS : body.max_tokens; + const wanted = body.max_tokens === undefined ? DEFAULT_MAX_TOKENS : body.max_tokens; if (!Number.isSafeInteger(wanted) || wanted < 1) throw new Error("max_tokens must be a positive integer."); if (!Array.isArray(body.messages) || body.messages.length === 0) throw new Error("At least one message is required."); - return { model, maxTokens: Math.min(wanted, MAX_TOKENS) }; + return { model, maxTokens: Math.min(wanted, HOSTED_MAX_TOKENS) }; } export function inferenceBudgetFailure(budget, handle) { @@ -27,7 +40,7 @@ export function inferenceBudgetFailure(budget, handle) { return { statusCode: 503, message: "Hosted inference cannot verify your remaining allowance. Try again shortly." }; } if (budget.exhausted || budget.remaining <= 0) { - return { statusCode: 429, message: `@${handle} has used today's allowance (${budget.used}/${budget.budget} tokens). It resets at midnight UTC.` }; + return { statusCode: 429, message: `@${handle} has used today's free braincells (${budget.used}/${budget.budget}). They reset at midnight UTC.` }; } return null; } diff --git a/system/backend/easel-stream.mjs b/system/backend/easel-stream.mjs index 0e7f7e6b8c..7603424597 100644 --- a/system/backend/easel-stream.mjs +++ b/system/backend/easel-stream.mjs @@ -1,5 +1,7 @@ // Relay each provider chunk as soon as the consumer can accept it. Cancellation // travels back to fetch instead of leaving a paid generation running unseen. +// `onUsage(tokens, usage)` fires once: weighted tokens, and the provider's own +// usage block, whose `cost` (USD) OpenRouter reports in the final message_delta. export function relayInference(body, { onUsage = () => {}, abort = () => {} } = {}) { const reader = body.getReader(); const decoder = new TextDecoder(); @@ -10,7 +12,7 @@ export function relayInference(body, { onUsage = () => {}, abort = () => {} } = function finish() { if (finished) return; finished = true; - Promise.resolve().then(() => onUsage(spent)).catch(() => {}); + Promise.resolve().then(() => onUsage(spent, accumulated)).catch(() => {}); reader.releaseLock(); } function meter(bytes) { diff --git a/system/netlify/functions/easel-credits.mjs b/system/netlify/functions/easel-credits.mjs index fe28a16779..dfa0d90b2e 100644 --- a/system/netlify/functions/easel-credits.mjs +++ b/system/netlify/functions/easel-credits.mjs @@ -23,7 +23,7 @@ export function createHandler({ authorize, getHandleOrEmail, checkBudget, paidBa const valueUSD = cells => Math.round(cells * creditPack.amount / creditPack.credits * 1e6) / 1e8; const dollars = { currency: "USD", free: valueUSD(budget.remaining), purchased: valueUSD(purchased), total: valueUSD(budget.remaining + purchased) }; - return reply(200, { dollars, purchased, offer, handle, unit: "weighted_tokens", remaining: budget.remaining, + return reply(200, { dollars, purchased, offer, handle, unit: "braincells", remaining: budget.remaining, used: budget.used, limit: budget.budget, day: budget.day, resetsAt: new Date(Date.parse(budget.day + "T00:00:00Z") + 86400000).toISOString() }); } catch { return reply(503, { error: "Allowance unavailable" }); } diff --git a/system/netlify/functions/easel-inference.mjs b/system/netlify/functions/easel-inference.mjs index 4dcd0d6052..a4ac22d604 100644 --- a/system/netlify/functions/easel-inference.mjs +++ b/system/netlify/functions/easel-inference.mjs @@ -23,9 +23,12 @@ // so a handle has one allowance across everything AC buys for them rather // than one per endpoint. // -// Provider usage feeds the shared token allowance, with discounts for cached -// input. This is not a dollar limit or an atomic reservation; premium models -// cost more per allowance, and concurrent requests can overshoot the balance. +// Everything is counted in braincells. A request is billed at twice the cost +// OpenRouter reports for it (easel-paid-credits.mjs), or at the model's fixed +// rate when no cost comes back. Today's free allowance is drawn first and is +// not an atomic reservation, so concurrent requests can overshoot it; once it +// is spent, each request holds its worst case from the wallet and settles the +// real charge when the stream ends. import { stream } from "@netlify/functions"; import { relayInference } from "../../backend/easel-stream.mjs"; @@ -118,10 +121,10 @@ export const handler = stream(async (event) => { paidHold=await authorizePaidRequest({user:userSub,model,body,maxTokens}); } catch(error) { return fail(error.statusCode||503,error.message); } } - const settlePaid = async spent => { + const settlePaid = async braincells => { if(!paidHold)return; const {withWallets,settle}=await import("../../backend/easel-paid-credits.mjs"); - await withWallets(w=>settle(paidHold,spent,w)); + await withWallets(w=>settle(paidHold,braincells,w)); }; console.log(`๐ŸŽจ easel @${handle} โ€” ${MODELS[model].label}${budget ? ` ยท ${budget.remaining} left` : ""}`); @@ -164,13 +167,13 @@ export const handler = stream(async (event) => { // keeps the first token as fast as the provider makes it. const passthrough = relayInference(upstream.body, { abort: () => controller.abort(), - onUsage: async (spent) => { + onUsage: async (tokens, usage) => { const { recordUsage } = await import("../../backend/ai-budget.mjs"); - if(paidHold) await settlePaid(spent); - else { - const {braincellRate,reservationSize}=await import("../../backend/easel-paid-credits.mjs"); - await recordUsage(handle, Math.ceil(spent*braincellRate(model,reservationSize(body,0,{validateMedia:false}))), { model }); - } + const {usageBraincells,reservationSize}=await import("../../backend/easel-paid-credits.mjs"); + const braincells=usageBraincells({model,tokens,cost:usage?.cost, + inputBound:paidHold?.inputBound??reservationSize(body,0,{validateMedia:false})}); + if(paidHold) await settlePaid(braincells); + else await recordUsage(handle, braincells, { model }); }, }); diff --git a/system/tests/easel-braincells.test.mjs b/system/tests/easel-braincells.test.mjs new file mode 100644 index 0000000000..855bba388e --- /dev/null +++ b/system/tests/easel-braincells.test.mjs @@ -0,0 +1,117 @@ +// Hosted inference billed in braincells: OpenRouter's reported cost at 2ร—, +// the rate fallback, the daily wallet cap and settlement that cannot repeat. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {authorizePaidRequest,braincellRate,braincellsFromCost,BRAINCELLS_PER_USD,CREDIT_PACK,DAILY_PAID_BRAINCELL_CAP,INFERENCE_MARKUP,OUT_OF_BRAINCELLS,reserve,settle,usageBraincells} from '../backend/easel-paid-credits.mjs'; +import {DEFAULT_EASEL_MODEL,EASEL_MODELS,HOSTED_MAX_TOKENS,inferenceRequest} from '../backend/easel-policy.mjs'; +import {relayInference} from '../backend/easel-stream.mjs'; + +// Just enough of a Mongo collection for the wallet's conditional updates. +const get=(doc,path)=>path.split('.').reduce((v,k)=>v?.[k],doc); +function put(doc,path,value){const keys=path.split('.');const last=keys.pop();let at=doc;for(const k of keys)at=at[k]??={};if(value===undefined)delete at[last];else at[last]=value;} +function matches(doc,filter){ + return Object.entries(filter).every(([path,want])=>{ + const have=get(doc,path); + if(want&&typeof want==='object'){ + if('$gte' in want)return have>=want.$gte; + if('$not' in want)return !(have>=want.$not.$gte); + if('$exists' in want)return (have!==undefined)===want.$exists; + } + return have===want; + }); +} +function wallets(docs){ + return { + docs, + async findOne(filter){return docs.find(d=>matches(d,filter))||null;}, + async updateOne(filter,update){ + const doc=docs.find(d=>matches(d,filter));if(!doc)return {modifiedCount:0}; + for(const [path,n] of Object.entries(update.$inc||{}))put(doc,path,(get(doc,path)||0)+n); + for(const [path,v] of Object.entries(update.$set||{}))put(doc,path,v); + for(const path of Object.keys(update.$unset||{}))put(doc,path,undefined); + return {modifiedCount:1}; + }, + }; +} +const messages=[{role:'user',content:'hello'}]; + +test('the six open models are allowed, flash is the default, and workspace turns may ask for 32,000',()=>{ + for(const model of ['deepseek/deepseek-v4.1-flash','deepseek/deepseek-v4-pro','moonshotai/kimi-k3','qwen/qwen3.7-plus','minimax/minimax-m3','z-ai/glm-5.3-flash']){ + assert.ok(Object.hasOwn(EASEL_MODELS,model),model); + assert.ok(braincellRate(model)>0,model); + } + assert.equal(DEFAULT_EASEL_MODEL,'deepseek/deepseek-v4.1-flash'); + assert.equal(inferenceRequest({messages}).model,DEFAULT_EASEL_MODEL); + assert.equal(inferenceRequest({messages,max_tokens:32000}).maxTokens,32000); + assert.equal(inferenceRequest({messages,max_tokens:64000}).maxTokens,HOSTED_MAX_TOKENS); + assert.equal(inferenceRequest({messages,model:'openai/gpt-5.6-luna'}).model,'openai/gpt-5.6-luna','installed clients keep working'); +}); + +test('a reported cost is billed at twice the price, at pack value',()=>{ + assert.equal(INFERENCE_MARKUP,2); + assert.equal(BRAINCELLS_PER_USD,CREDIT_PACK.credits/(CREDIT_PACK.amount/100)); + assert.equal(BRAINCELLS_PER_USD,200_000); + assert.equal(braincellsFromCost(0.001),400,'no float noise tips a whole braincell'); + assert.equal(braincellsFromCost(0.0123456),4939); + assert.equal(braincellsFromCost(0),0); + assert.equal(usageBraincells({model:'moonshotai/kimi-k3',tokens:1_000_000,cost:0.01}),4000,'cost wins over tokens'); +}); + +test('without a reported cost, the model rate over weighted tokens applies',()=>{ + assert.equal(usageBraincells({model:'openai/gpt-5.6-luna',tokens:1000}),1000); + assert.equal(usageBraincells({model:'anthropic/claude-opus-5',tokens:4}),100); + assert.equal(usageBraincells({model:'deepseek/deepseek-v4.1-flash',tokens:1000}),480); + assert.equal(usageBraincells({model:'openai/gpt-5.6-luna',tokens:1000,cost:'0.1'}),1000,'a non-number cost is not trusted'); + // Reservation rates are worst case: every token at the output price, marked up. + assert.equal(braincellRate('moonshotai/kimi-k3'),15*INFERENCE_MARKUP*BRAINCELLS_PER_USD/1e6); +}); + +test('the relay hands the provider cost to the meter once',async()=>{ + const events=[{type:'message_start',message:{usage:{input_tokens:100,output_tokens:1}}},{type:'message_delta',usage:{output_tokens:50,cost:0.00042}}]; + const body=new Response(events.map(e=>`data: ${JSON.stringify(e)}\n\n`).join('')).body; + const calls=[];await new Response(relayInference(body,{onUsage:(tokens,usage)=>calls.push({tokens,usage})})).text(); + await new Promise(r=>setImmediate(r)); + assert.equal(calls.length,1); + assert.equal(calls[0].tokens,150); + assert.equal(calls[0].usage.cost,0.00042); + assert.equal(usageBraincells({model:DEFAULT_EASEL_MODEL,tokens:calls[0].tokens,cost:calls[0].usage.cost}),168); +}); + +test('settlement is keyed by the hold, so a repeated settle cannot charge twice',async()=>{ + const w=wallets([{_id:'u',balance:10_000}]); + const now=new Date('2026-09-28T12:00:00Z'); + const hold=await reserve('u',1000,w,{now}); + assert.equal(w.docs[0].balance,9000); + assert.equal(await settle(hold,250,w,{now}),true); + assert.equal(await settle(hold,250,w,{now}),false); + await Promise.all([settle(hold,900,w,{now}),settle(hold,900,w,{now})]); + assert.equal(w.docs[0].balance,9750); + assert.equal(w.docs[0].daily['2026-09-28'],250); + assert.equal(w.docs[0].holds[hold.id],undefined); + const capped=await reserve('u',100,w,{now});await settle(capped,5000,w,{now}); + assert.equal(w.docs[0].balance,9650,'never more than the hold'); +}); + +test('a day of bought braincells is capped, and the refusal says when it resets',async()=>{ + const now=new Date('2026-09-28T21:30:00Z'); + const w=wallets([{_id:'rich',balance:10_000_000,daily:{'2026-09-28':DAILY_PAID_BRAINCELL_CAP}},{_id:'poor',balance:10}]); + const using=fn=>fn(w); + const body={messages}; + await assert.rejects(authorizePaidRequest({user:'rich',model:DEFAULT_EASEL_MODEL,body,maxTokens:1000,now,withWallets:using}), + e=>e.statusCode===429&&/2,000,000/.test(e.message)&&/midnight UTC, in 2h 30m/.test(e.message)); + await assert.rejects(authorizePaidRequest({user:'poor',model:DEFAULT_EASEL_MODEL,body,maxTokens:1000,now,withWallets:using}), + e=>e.statusCode===402&&e.message===OUT_OF_BRAINCELLS&&/\/provider/.test(e.message)); + // Yesterday's spend does not count against today. + const tomorrow=new Date('2026-09-29T00:00:01Z'); + const hold=await authorizePaidRequest({user:'rich',model:DEFAULT_EASEL_MODEL,body,maxTokens:1000,now:tomorrow,withWallets:using}); + assert.ok(hold.amount>0&&hold.day==='2026-09-29'); +}); + +test('the hold grows with max_tokens and with the model rate',async()=>{ + const w=wallets([{_id:'u',balance:100_000_000}]);const using=fn=>fn(w);const body={messages}; + const small=await authorizePaidRequest({user:'u',model:DEFAULT_EASEL_MODEL,body,maxTokens:1000,withWallets:using}); + const large=await authorizePaidRequest({user:'u',model:DEFAULT_EASEL_MODEL,body,maxTokens:32000,withWallets:using}); + const kimi=await authorizePaidRequest({user:'u',model:'moonshotai/kimi-k3',body,maxTokens:32000,withWallets:using}); + assert.ok(small.amount