okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
8a01b9c
main
1"""A provider's declared image edge holds for every image it is sent. 2 3A profile's `imageEdge` stands in for Claude's 2000px limit, which only the 4real Anthropic upstream declares. 5""" 6 7import base64 8import hashlib 9import json 10import re 11import sqlite3 12import struct 13import unittest 14import urllib .error 15import zlib 16 17from harness import ( 18LOOPBACK_REMOTE , 19Albedo , 20Provider , 21exclusive , 22operation_id , 23python , 24text , 25) 26 27 28def png (width ,height ,metadata = b"" ): 29def chunk (kind ,data ): 30body = kind + data 31return struct .pack (">I" ,len (data ))+ body + struct .pack (">I" ,zlib .crc32 (body )) 32 33rows = b"" .join (b"\x00" + b"\x80\x40\x20" * width for _ in range (height )) 34return ( 35b"\x89PNG\r\n\x1a\n" 36+ chunk (b"IHDR" ,struct .pack (">IIBBBBB" ,width ,height ,8 ,2 ,0 ,0 ,0 )) 37+ (chunk (b"npAD" ,metadata )if metadata else b"" ) 38+ chunk (b"IDAT" ,zlib .compress (rows )) 39+ chunk (b"IEND" ,b"" ) 40 ) 41 42 43WIDE = png (1200 ,10 ) 44WIDE_DATA = base64 .b64encode (WIDE ).decode () 45NOTE = "An earlier image was scaled from 1200x10 to 1000x" 46 47 48def widths (request ): 49"""The pixel width of every PNG the request carries.""" 50found = re .findall (r"data:image/png;base64,([A-Za-z0-9+/=]+)" ,json .dumps (request )) 51return [struct .unpack (">I" ,base64 .b64decode (data )[16 :20 ])[0 ]for data in found ] 52 53 54def cell_result (output ): 55"""The python tool's result: plain text, or the text part beside images.""" 56body = output ["output" ] 57if isinstance (body ,str ): 58return json .loads (body ) 59 [text ]= [part ["text" ]for part in body if part .get ("type" )== "input_text" ] 60return json .loads (text ) 61 62 63class ImageLimitsTest (unittest .TestCase ): 64@exclusive 65def test_large_valid_image_remains_available_after_durable_reload (self ): 66# A valid ancillary chunk makes encoded length exceed 5 MiB while the 67# decoded PNG remains below the advertised file-size boundary. 68image = png (2 ,3 ,b"x" * (4 * 1024 * 1024 )) 69data = base64 .b64encode (image ).decode () 70self .assertLess (len (image ),5 * 1024 * 1024 ) 71self .assertGreater (len (data ),5 * 1024 * 1024 ) 72provider = Provider (lambda _ :text ("image accepted" )) 73self .addCleanup (provider .close ) 74with Albedo (provider ,protocol = "responses" )as app : 75session = app .session () 76app .api ( 77f"/sessions/ { session } /inputs/ { operation_id () } " , 78 { 79"kind" :"message" , 80"text" :"look" , 81"images" : [{"mime_type" :"image/png" ,"data" :data }], 82 }, 83method = "PUT" , 84 ).close () 85app .idle (session ) 86app .restart () 87app .prompt (session ,"look again" ).close () 88app .idle (session ) 89sent = json .dumps (provider .requests [- 1 ]["request" ]) 90encoded = re .findall (r"data:image/png;base64,([A-Za-z0-9+/=]+)" ,sent ) 91self .assertEqual (len (encoded ),1 ) 92self .assertEqual (base64 .b64decode (encoded [0 ]),image ) 93 94def test_a_declared_edge_refuses_new_images_and_fits_history (self ): 95def reply (request ): 96inputs = request ["input" ] 97if inputs [- 1 ].get ("type" )== "function_call_output" : 98return text ("done" ) 99prompts = [ 100item .get ("content" ) 101for item in inputs 102if item .get ("role" )== "user" 103and "<system-note" not in json .dumps (item .get ("content" )) 104 ] 105if prompts [- 1 ]== "show" : 106return python ( 107"import base64\nshow_image(base64.b64decode('" + WIDE_DATA + "'))" 108 ) 109return text ("ok" ) 110 111provider = Provider (reply ) 112loose ,strict = f"loose- { provider . route } " ,f"strict- { provider . route } " 113profile = { 114"baseUrl" :provider .url , 115"apiKey" :"fixture-key" , 116"model" :"fixture-model" , 117"protocol" :"responses" , 118 } 119try : 120with Albedo ( 121provider , 122protocol = "responses" , 123providers = {loose :profile ,strict :dict (profile ,imageEdge = 1000 )}, 124 )as app : 125session = app .session () 126attach = { 127"kind" :"message" , 128"text" :"look" , 129"images" : [{"mime_type" :"image/png" ,"data" :WIDE_DATA }], 130 } 131 132def switch (name ): 133with app .api (f"/sessions/ { session } ?view=configuration" )as response : 134json .load (response ) 135revision = response .headers ["ETag" ] 136app .api ( 137f"/sessions/ { session } ?view=configuration" , 138 {"provider_profile" :name ,"model" :"fixture-model" }, 139method = "PATCH" , 140headers = {"If-Match" :revision }, 141 ).close () 142 143def attach_image (): 144return app .api ( 145f"/sessions/ { session } /inputs/ { operation_id () } " , 146attach , 147method = "PUT" , 148 ) 149 150def send (content ): 151start = len (provider .requests ) 152app .prompt (session ,content ).close () 153app .idle (session ) 154return [r ["request" ]for r in provider .requests [start :]] 155 156attach_image ().close () 157app .idle (session ) 158self .assertEqual (widths (provider .requests [- 1 ]["request" ]), [1200 ]) 159 160switch (strict ) 161sent = len (provider .requests ) 162with self .assertRaises (urllib .error .HTTPError )as refused : 163attach_image ().close () 164self .assertEqual (refused .exception .code ,409 ) 165self .assertIn ( 166"1000px edge limit" ,json .load (refused .exception )["detail" ] 167 ) 168self .assertEqual (len (provider .requests ),sent ) 169 170strict_requests = send ("show" )+ send ("again" ) 171self .assertEqual (len (strict_requests ),3 ) 172for request in strict_requests : 173self .assertEqual (widths (request ), [1000 ]) 174self .assertEqual (json .dumps (request ).count (NOTE ),1 ) 175output = next ( 176item 177for item in strict_requests [1 ]["input" ] 178if item .get ("type" )== "function_call_output" 179 ) 180# show_image refuses it at the call, so the cell fails there. 181result = cell_result (output ) 182self .assertEqual (result ["status" ],"error" ) 183self .assertIn ( 184"ValueError: 1200x10 image is over this model's 1000px edge limit" , 185result ["output" ], 186 ) 187self .assertNotIn ("image_errors" ,result ) 188entries = app .history (session )["items" ] 189notes = [ 190part ["text" ] 191for entry in entries 192if entry ["kind" ]== "image_fit" 193for part in entry ["content" ] 194if part ["kind" ]== "text" 195 ] 196self .assertEqual (len (notes ),1 ,notes ) 197self .assertIn (NOTE ,notes [0 ]) 198original = [ 199entry 200for entry in entries 201if entry ["kind" ]== "user" 202and any ( 203part ["kind" ]== "text" and part ["text" ]== "look" 204for part in entry ["content" ] 205 ) 206 ] 207self .assertEqual (len (original ),1 ) 208 [image ]= [ 209part ["image" ] 210for part in original [0 ]["content" ] 211if part ["kind" ]== "image" 212 ] 213self .assertEqual (image ["width" ],1200 ) 214self .assertEqual (image ["height" ],10 ) 215self .assertEqual (image ["original_bytes" ],len (WIDE )) 216 217source = hashlib .sha256 (WIDE_DATA .encode ()).hexdigest () 218database = f"file: { app . home / 'albedo.sqlite' } ?mode=ro" 219 220def rows (session ,needle ): 221with sqlite3 .connect (database ,uri = True )as db : 222return [ 223seq 224for (seq ,)in db .execute ( 225"SELECT seq FROM transcript WHERE session=? " 226"AND instr(payload, CAST(? AS BLOB))>0 ORDER BY seq" , 227 (session ,needle ), 228 ) 229 ] 230 231# The original row keeps its image; the fit is a row of its own. 232self .assertEqual (len (rows (session ,source )),2 ) 233self .assertEqual (len (rows (session ,"image_fit" )),1 ) 234 235# A fit is permanent: a looser provider keeps the copy. 236switch (loose ) 237 [back ]= send ("back" ) 238self .assertEqual (widths (back ), [1000 ]) 239self .assertEqual (json .dumps (back ).count (NOTE ),1 ) 240 241# A fork from before the fit starts from the original. 242checkpoint = next ( 243entry ["checkpoint_id" ] 244for entry in entries 245if entry ["kind" ]== "assistant" 246 ) 247branch = operation_id () 248app .api ( 249f"/sessions/ { branch } " , 250 { 251"kind" :"fork" , 252"source_session_id" :session , 253"checkpoint_id" :checkpoint , 254 }, 255method = "PUT" , 256headers = {"If-None-Match" :"*" }, 257 ).close () 258start = len (provider .requests ) 259app .prompt (branch ,"branch" ).close () 260app .idle (branch ) 261 [forked ]= [r ["request" ]for r in provider .requests [start :]] 262self .assertEqual (widths (forked ), [1200 ]) 263self .assertNotIn (NOTE ,json .dumps (forked )) 264finally : 265provider .close () 266 267def test_an_image_cut_short_is_refused_where_it_is_attached (self ): 268# A PNG cut off by a capped read kept a readable header, so it was 269# attached, and every later request carried an image the provider 270# could not decode: the session failed until it was forked. 271cut = png (2 ,3 ,b"x" * 1000 )[:500 ] 272cut_data = base64 .b64encode (cut ).decode () 273 274def reply (request ): 275if request ["input" ][- 1 ].get ("type" )== "function_call_output" : 276return text ("done" ) 277return python ( 278"import base64\nshow_image(base64.b64decode('" + cut_data + "'))" 279 ) 280 281provider = Provider (reply ) 282self .addCleanup (provider .close ) 283with Albedo (provider ,protocol = "responses" )as app : 284session = app .session () 285with self .assertRaises (urllib .error .HTTPError )as refused : 286app .api ( 287f"/sessions/ { session } /inputs/ { operation_id () } " , 288 { 289"kind" :"message" , 290"text" :"look" , 291"images" : [{"mime_type" :"image/png" ,"data" :cut_data }], 292 }, 293method = "PUT" , 294 ).close () 295self .assertEqual (refused .exception .code ,400 ) 296 297app .prompt (session ,"show" ).close () 298app .idle (session ) 299request = provider .requests [- 1 ]["request" ] 300output = next ( 301item 302for item in request ["input" ] 303if item .get ("type" )== "function_call_output" 304 ) 305result = cell_result (output ) 306self .assertEqual (result ["status" ],"error" ) 307self .assertIn ( 308"ValueError: this 500-byte image/png stops before its IEND chunk" , 309result ["output" ], 310 ) 311self .assertNotIn ("data:image/png" ,json .dumps (request )) 312 313def test_a_remote_image_is_shown_under_the_same_limits (self ): 314cell = ( 315LOOPBACK_REMOTE 316+ """try: 317print(await rem.show_image("small.png")) 318await rem.show_image("wide.png") 319finally: 320await rem.close() 321""" 322 ) 323 324def reply (request ): 325if request ["input" ][- 1 ].get ("type" )== "function_call_output" : 326return text ("done" ) 327return python (cell ) 328 329provider = Provider (reply ) 330profile = { 331"baseUrl" :provider .url , 332"apiKey" :"fixture-key" , 333"model" :"fixture-model" , 334"protocol" :"responses" , 335"imageEdge" :1000 , 336 } 337try : 338with Albedo ( 339provider , 340protocol = "responses" , 341providers = {f"strict- { provider . route } " :profile }, 342 )as app : 343 (app .workspace / "small.png" ).write_bytes (png (10 ,10 )) 344 (app .workspace / "wide.png" ).write_bytes (WIDE ) 345session = app .session () 346app .prompt (session ,"remote" ).close () 347app .idle (session ) 348 [request ]= [ 349r ["request" ] 350for r in provider .requests 351if r ["request" ]["input" ][- 1 ].get ("type" )== "function_call_output" 352 ] 353output = next ( 354item 355for item in request ["input" ] 356if item .get ("type" )== "function_call_output" 357 ) 358result = cell_result (output ) 359# The small image came over the connection and went on to the 360# model; the wide one failed the cell at its call. 361self .assertIn ("attached image/png" ,result ["output" ]) 362self .assertEqual (result ["status" ],"error" ) 363self .assertIn ( 364"ValueError: 1200x10 image is over this model's 1000px edge limit" , 365result ["output" ], 366 ) 367self .assertEqual (widths (request ), [10 ]) 368finally : 369provider .close ()