From 52e7bb41a5952b3b78bd604bb0883b58028072b0 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 1 Oct 2026 21:50:18 -0700 Subject: [PATCH] Accept bounded chalk PNGs in hosted inference --- system/backend/easel-input-images.mjs | 32 ++++++++++++++++++++ system/backend/easel-paid-credits.mjs | 10 +++--- system/backend/easel-policy.mjs | 2 ++ system/netlify/functions/easel-inference.mjs | 2 +- system/tests/easel-input-images.test.mjs | 25 +++++++++++++++ system/tests/easel-paid-credits.test.mjs | 3 ++ 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 system/backend/easel-input-images.mjs create mode 100644 system/tests/easel-input-images.test.mjs diff --git a/system/backend/easel-input-images.mjs b/system/backend/easel-input-images.mjs new file mode 100644 index 0000000000..90e58ed804 --- /dev/null +++ b/system/backend/easel-input-images.mjs @@ -0,0 +1,32 @@ +// The Chalk client submits small, static canvas PNGs. Remote URLs and other +// media remain unsupported; dimensions and bytes bound the paid reservation. +export function imageInputBound(body) { + let pixels=0,count=0; + JSON.stringify({system:body.system,messages:body.messages,tools:body.tools},(_,value)=>{ + if(!value||typeof value!=='object')return value; + if(['document','input_audio','video','image_url'].includes(value.type))throw Error('Unsupported hosted media. Use a bounded PNG image.'); + if(value.type!=='image')return value; + const s=value.source; + if(++count>16||s?.type!=='base64'||s.media_type!=='image/png'||typeof s.data!=='string'||s.data.length>700000||!s.data.length||s.data.length%4||!/^[A-Za-z0-9+/]+={0,2}$/.test(s.data))throw Error('Hosted images must be bounded base64 PNGs.'); + const png=Buffer.from(s.data,'base64'); + if(png.length<45||png.subarray(0,8).toString('hex')!=='89504e470d0a1a0a'||png.readUInt32BE(8)!==13||png.toString('ascii',12,16)!=='IHDR')throw Error('Invalid hosted PNG.'); + const width=png.readUInt32BE(16),height=png.readUInt32BE(20); + if(!width||!height||width>768||height>768)throw Error('Hosted PNG dimensions must be at most 768 by 768.'); + let ended=false,data=false; + for(let at=8;atpng.length)throw Error('Invalid hosted PNG.'); + const size=png.readUInt32BE(at),kind=png.toString('ascii',at+4,at+8); + if(at+size+12>png.length||kind==='acTL')throw Error('Hosted PNG must be static and complete.'); + if(kind==='IDAT')data=true; + at+=size+12; + if(kind==='IEND'){ended=size===0&&at===png.length;break;} + } + if(!ended||!data)throw Error('Invalid hosted PNG.'); + // Deliberately conservative: one token per pixel plus per-image overhead, + // in addition to the existing serialized-byte bound. Actual cost settles + // from provider usage; unused reserved credit returns to the wallet. + pixels+=width*height+4096; + return value; + }); + return pixels; +} diff --git a/system/backend/easel-paid-credits.mjs b/system/backend/easel-paid-credits.mjs index 677e98bd15..d581fa8472 100644 --- a/system/backend/easel-paid-credits.mjs +++ b/system/backend/easel-paid-credits.mjs @@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto'; import { connect } from './database.mjs'; import { dayKey } from './ai-budget.mjs'; +import { imageInputBound } from './easel-input-images.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); @@ -56,12 +57,13 @@ export async function revokeGrant(grant,wallets) { const result=await wallets.updateOne({_id:grant.user},[{$set:{balance:{$subtract:['$balance',{$max:[0,{$subtract:[grant.credits,{$ifNull:[`$${path}`,0]}]}]}]},[path]:{$max:[grant.credits,{$ifNull:[`$${path}`,0]}]},updatedAt:'$$NOW'}}]); return result.modifiedCount===1; } -export function reservationSize(body,maxTokens,{validateMedia=true}={}) { +export function reservationSize(body,maxTokens) { // UTF-8 bytes upper-bound text tokens, including JSON tool definitions. - // Unsupported media is rejected below; it cannot hide unbounded token cost. + // Static PNG pixels have a separate conservative bound. Other media remains + // unsupported; it cannot hide unbounded token cost. const input=JSON.stringify({system:body.system,messages:body.messages,tools:body.tools}); - 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; + const images=imageInputBound(body); + return Math.ceil(Buffer.byteLength(input,'utf8')*1.25)+images+maxTokens+4096; } // Pending work counts until settled, including work crossing midnight. This // conservative bound avoids spending the same daily capacity concurrently. diff --git a/system/backend/easel-policy.mjs b/system/backend/easel-policy.mjs index 68983ad55f..c783142bde 100644 --- a/system/backend/easel-policy.mjs +++ b/system/backend/easel-policy.mjs @@ -2,6 +2,7 @@ // 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. +import {imageInputBound} from './easel-input-images.mjs'; export const EASEL_MODELS = { "deepseek/deepseek-v4.1-flash": { label: "DeepSeek V4.1 Flash" }, "deepseek/deepseek-v4-pro": { label: "DeepSeek V4 Pro" }, @@ -32,6 +33,7 @@ export function inferenceRequest(body) { 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."); + imageInputBound(body); return { model, maxTokens: Math.min(wanted, HOSTED_MAX_TOKENS), reasoning: reasoningRequest(body.reasoning), thinking: thinkingRequest(body.thinking) }; } diff --git a/system/netlify/functions/easel-inference.mjs b/system/netlify/functions/easel-inference.mjs index d3330fd496..602d3ce27c 100644 --- a/system/netlify/functions/easel-inference.mjs +++ b/system/netlify/functions/easel-inference.mjs @@ -179,7 +179,7 @@ export const handler = stream(async (event) => { const { recordUsage } = await import("../../backend/ai-budget.mjs"); 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})}); + inputBound:paidHold?.inputBound??reservationSize(body,0)}); if(paidHold) await settlePaid(braincells); else await recordUsage(handle, braincells, { model }); }, diff --git a/system/tests/easel-input-images.test.mjs b/system/tests/easel-input-images.test.mjs new file mode 100644 index 0000000000..d504946917 --- /dev/null +++ b/system/tests/easel-input-images.test.mjs @@ -0,0 +1,25 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {imageInputBound} from '../backend/easel-input-images.mjs'; +import {inferenceRequest} from '../backend/easel-policy.mjs'; +const data='iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aKxkAAAAASUVORK5CYII='; +const image={type:'image',source:{type:'base64',media_type:'image/png',data}}; +const body=content=>({messages:[{role:'user',content}]}); +test('static PNGs add a pixel reservation and pass the shared free/paid request policy',()=>{ + assert.equal(imageInputBound(body('hello')),0); + assert.equal(imageInputBound(body([image])),4097); + assert.ok(inferenceRequest(body([image])).model); +}); +test('invalid, oversized, remote, animated and unbounded media fail before inference',()=>{ + const large=Buffer.from(data,'base64');large.writeUInt32BE(769,16); + const animation=Buffer.from(data,'base64');animation.write('acTL',37); + for(const value of [{...image,source:{type:'url',url:'https://example.com/image.png'}}, + {...image,source:{...image.source,data:'garbage'}}, + {...image,source:{...image.source,data:'a'.repeat(700004)}}, + {...image,source:{...image.source,data:large.toString('base64')}}, + {...image,source:{...image.source,data:animation.toString('base64')}}, + {type:'document'},{type:'video'},{type:'input_audio'},{type:'image_url'}]){ + assert.throws(()=>inferenceRequest(body([value]))); + } + assert.throws(()=>imageInputBound(body(Array(17).fill(image)))); +}); diff --git a/system/tests/easel-paid-credits.test.mjs b/system/tests/easel-paid-credits.test.mjs index b09f480c3e..4f075e575c 100644 --- a/system/tests/easel-paid-credits.test.mjs +++ b/system/tests/easel-paid-credits.test.mjs @@ -11,6 +11,9 @@ test('only a matching, confirmed server offer can grant credit',()=>{ test('reservation covers byte-heavy input and rejects unmetered media',()=>{ assert.ok(reservationSize({messages:[{content:'é'.repeat(100)}]},100)>reservationSize({messages:[{content:'a'.repeat(100)}]},100)); assert.throws(()=>reservationSize({messages:[{content:[{type:'image',source:{}}]}]},100)); + const image={type:'image',source:{type:'base64',media_type:'image/png',data:'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aKxkAAAAASUVORK5CYII='}}; + const body={messages:[{role:'user',content:[image]}]}; + assert.equal(reservationSize(body,100),Math.ceil(Buffer.byteLength(JSON.stringify(body))*1.25)+1+4096+100+4096); }); const event=body=>({httpMethod:'POST',headers:{authorization:'Bearer test'},body:JSON.stringify(body)}); function handler(options={}){return createHandler({stripe:{checkout:{sessions:{create:async()=>({id:'cs_test_new',url:'https://checkout.stripe.com/test'}),retrieve:async()=>paid}},webhooks:{constructEvent(){throw Error('invalid')}}},verifyUser:async()=>({sub:'user1'}),handleFor:async()=>'@tester',secret:'test',fulfill:async()=>true,...options});} -- 2.51.2