okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""The reloadable prompt-cache TTL table: layers, merge precedence, live reload. 2 3The table is daemon-wide state read from files, so this is where its behaviour 4is visible: the shipped default served with every entry, lookups resolving 5through match order, a local override replacing an entry by id and winning 6with a new more-specific one — without a restart — and a malformed file 7keeping the last good table. A turn's usage carries the steps its cached 8count fades through by that table, which the chat footer counts down from and 9which must survive a restart. It writes extensions.json and cache-ttl.json, so 10tests that change those layers are exclusive, on a home of their own. 11Read-only default lookups and authentication share the daemon without overrides. 12 13The remote layer's fetch is background (like the models catalog's), so the 14fixture provider serves the remote table on a loopback URL and the test waits 15for it to land; `/reload` fetches it synchronously. No live network is reached. 16""" 17 18from typing import Any 19import json 20from datetime import datetime 21import os 22import time 23import unittest 24import urllib .error 25import urllib .request 26 27from harness import ROOT ,Albedo ,Provider ,exclusive ,text 28 29 30def milliseconds (timestamp ): 31return round ( 32datetime .fromisoformat (timestamp .replace ("Z" ,"+00:00" )).timestamp ()* 1000 33 ) 34 35 36SHIPPED = json .loads ((ROOT / "priv" / "cache-ttl.json" ).read_text ()) 37 38# `/reload` fetches the models catalog before the TTL table, so it needs a 39# fixture url too — the default would reach the live models.dev. 40MODELS_CATALOG = { 41"fixture" : { 42"id" :"fixture" , 43"name" :"Fixture" , 44"models" : { 45"fixture-model" : {"id" :"fixture-model" ,"limit" : {"context" :8000 }} 46 }, 47 } 48} 49 50REMOTE_TABLE :dict [str ,Any ]= { 51"version" :1 , 52"entries" : [ 53# Replaces the shipped deepseek entry by id, in place. 54 { 55"id" :"deepseek" , 56"match" : {"host" :"api.deepseek.com" }, 57"policy" :"fixed" , 58"clock" :"response" , 59"evidence" :"measured" , 60"source" :"fixture" , 61"note" :"remote override" , 62 }, 63# A new id, which goes before every default entry. 64 { 65"id" :"fixture-gateway" , 66"match" : {"host" :"gateway.fixture" }, 67"policy" :"evict" , 68"survival" : {"typical" :600 }, 69"evidence" :"measured" , 70"source" :"fixture" , 71"note" :"new remote entry" , 72 }, 73 ], 74} 75 76LOCAL_OVERRIDE :dict [str ,Any ]= { 77"version" :1 , 78"entries" : [ 79# Replaces the remote or default deepseek entry by id, in place. 80 { 81"id" :"deepseek" , 82"match" : {"host" :"api.deepseek.com" }, 83"policy" :"fixed" , 84"clock" :"request" , 85"evidence" :"measured" , 86"source" :"local" , 87"note" :"local override" , 88 }, 89# A new id, more specific than anything shipped, which must win. 90 { 91"id" :"laptop-gateway" , 92"match" : {"host" :"gateway.laptop" }, 93"policy" :"refresh" , 94"tiers" : [{"seconds" :600 ,"write" :1.5 }], 95"evidence" :"measured" , 96"source" :"local" , 97"note" :"new local entry" , 98 }, 99 ], 100} 101 102 103def write_atomic (path ,payload ): 104temporary = path .with_suffix (".tmp" ) 105temporary .write_text (json .dumps (payload )) 106temporary .replace (path ) 107 108 109# exclusive: configures daemon-wide TTL/catalog layers and writes cache files 110@exclusive 111class CacheTtlTests (unittest .TestCase ): 112def setUp (self ): 113self .provider = Provider (lambda _request :text ("ok" ),catalog = REMOTE_TABLE ) 114self .addCleanup (self .provider .close ) 115self .catalog = Provider (lambda _request :text ("ok" ),catalog = MODELS_CATALOG ) 116self .addCleanup (self .catalog .close ) 117 118def prepare (app ): 119app .write_extensions ( 120 { 121"models" : {"url" :self .catalog .url + "/models.json" }, 122"cacheTtl" : { 123"url" :self .provider .url + "/cache-ttl.json" , 124"refreshHours" :24 , 125 }, 126 } 127 ) 128 129self .app = Albedo ( 130self .provider , 131prepare = prepare , 132providers = { 133"fixture" : { 134"baseUrl" :self .provider .url , 135"apiKey" :"fixture-key" , 136"model" :"fixture-model" , 137"protocol" :"chat_completions" , 138 }, 139 }, 140 ) 141self .app .__enter__ () 142self .addCleanup (self .app .__exit__ ,None ,None ,None ) 143 144def table (self ): 145with self .app .api ("/models?view=cache-policy&limit=200" )as response : 146return json .load (response ) 147 148def resolved (self ,query ): 149with self .app .api ("/models?view=cache-policy" + query )as response : 150return json .load (response )["matched" ] 151 152def layers (self ,table ): 153return {layer ["name" ]:layer for layer in table ["layers" ]} 154 155def entries (self ,table ): 156return {entry ["id" ]:entry for entry in table ["entries" ]} 157 158def test_local_override_replaces_and_adds_without_restart (self ): 159write_atomic (self .app .home / "cache-ttl.json" ,LOCAL_OVERRIDE ) 160table = self .table () 161local = self .layers (table )["local" ] 162self .assertTrue (local ["loaded" ]) 163self .assertEqual (local ["path" ],str (self .app .home / "cache-ttl.json" )) 164entries = self .entries (table ) 165# Replaced by id: same entry, now from the local layer. 166self .assertEqual (entries ["deepseek" ]["layer" ],"local" ) 167self .assertEqual (entries ["deepseek" ]["note" ],"local override" ) 168self .assertEqual (entries ["deepseek" ]["clock" ],"request" ) 169# New ids go before all earlier-layer entries and win the lookup. 170self .assertEqual (table ["entries" ][0 ]["id" ],"laptop-gateway" ) 171self .assertEqual (self .resolved ("&host=gateway.laptop" )["id" ],"laptop-gateway" ) 172# The replaced default keeps answering, with the override's values. 173self .assertEqual ( 174self .resolved ("&host=api.deepseek.com" )["note" ],"local override" 175 ) 176# Untouched defaults still resolve. 177self .assertEqual ( 178self .resolved ("&extension=claude" )["id" ],"claude-subscription" 179 ) 180 181def test_three_layers_keep_replacements_in_place_and_new_ids_first (self ): 182write_atomic (self .app .home / "cache-ttl-remote.json" ,REMOTE_TABLE ) 183gateway = { 184** REMOTE_TABLE ["entries" ][1 ], 185"match" : {"host" :"gateway.*" }, 186"note" :"local gateway replacement" , 187 } 188local = {"entries" : [* LOCAL_OVERRIDE ["entries" ],gateway ]} 189write_atomic (self .app .home / "cache-ttl.json" ,local ) 190table = self .table () 191self .assertEqual ( 192 [entry ["id" ]for entry in table ["entries" ]], 193 ["laptop-gateway" ,"fixture-gateway" ] 194+ [entry ["id" ]for entry in SHIPPED ["entries" ]], 195 ) 196self .assertTrue (self .layers (table )["remote" ]["loaded" ]) 197self .assertEqual (self .entries (table )["fixture-gateway" ]["layer" ],"local" ) 198self .assertEqual (self .entries (table )["deepseek" ]["note" ],"local override" ) 199# A fresh local rule wins before the replaced, more general gateway rule. 200self .assertEqual (self .resolved ("&host=gateway.laptop" )["id" ],"laptop-gateway" ) 201self .assertEqual ( 202self .resolved ("&host=gateway.fixture" )["note" ], 203"local gateway replacement" , 204 ) 205self .assertEqual (self .resolved ("&host=api.deepseek.com" )["clock" ],"request" ) 206 207def test_duplicate_ids_reject_the_layer_and_retain_last_good_entries (self ): 208duplicate = {"entries" : [LOCAL_OVERRIDE ["entries" ][0 ]]* 2 } 209write_atomic (self .app .home / "cache-ttl.json" ,duplicate ) 210table = self .table () 211self .assertFalse (self .layers (table )["local" ]["loaded" ]) 212failure = self .layers (table )["local" ]["error" ] 213self .assertEqual (set (failure ), {"code" ,"detail" }) 214self .assertEqual (failure ["code" ],"cache_policy_layer_failed" ) 215self .assertIn ("duplicate id: deepseek" ,failure ["detail" ]) 216self .assertLessEqual (len (failure ["detail" ]),4096 ) 217self .assertFalse (any (entry ["layer" ]== "local" for entry in table ["entries" ])) 218write_atomic (self .app .home / "cache-ttl.json" ,LOCAL_OVERRIDE ) 219self .assertTrue (self .layers (self .table ())["local" ]["loaded" ]) 220write_atomic (self .app .home / "cache-ttl.json" ,duplicate ) 221for _ in range (3 ): 222table = self .table () 223self .assertFalse (self .layers (table )["local" ]["loaded" ]) 224self .assertIn ( 225"duplicate id: deepseek" ,self .layers (table )["local" ]["error" ]["detail" ] 226 ) 227self .assertEqual (self .entries (table )["deepseek" ]["note" ],"local override" ) 228write_atomic (self .app .home / "cache-ttl.json" ,LOCAL_OVERRIDE ) 229self .assertTrue (self .layers (self .table ())["local" ]["loaded" ]) 230 231def test_duplicate_remote_ids_are_rejected_before_replacement (self ): 232session = self .app .session () 233with self .app .api (f"/sessions/ { session } /reload" , {"target" :"both" }): 234pass 235path = self .app .home / "cache-ttl-remote.json" 236before = path .read_bytes () 237self .provider .catalog = {"entries" : [REMOTE_TABLE ["entries" ][0 ]]* 2 } 238with self .app .api ( 239f"/sessions/ { session } /reload" , {"target" :"both" } 240 )as response : 241outcome = json .load (response )["cache_policy" ] 242self .assertEqual (outcome ["state" ],"failed" ) 243self .assertIn ("duplicate id" ,outcome ["failure" ]["detail" ]) 244self .assertEqual (path .read_bytes (),before ) 245self .assertEqual ( 246self .entries (self .table ())["deepseek" ]["note" ],"remote override" 247 ) 248 249def test_same_revision_remote_replacement_invalidates_parsed_layer (self ): 250session = self .app .session () 251path = self .app .home / "cache-ttl-remote.json" 252write_atomic (path ,REMOTE_TABLE ) 253# A future mtime prevents background refresh. Restoring it after reload 254# keeps the cached revision unchanged without depending on scheduling. 255replacement = json .loads (json .dumps (REMOTE_TABLE )) 256replacement ["entries" ][0 ]["note" ]= "remote revised!" 257self .assertEqual (len (json .dumps (replacement )),len (json .dumps (REMOTE_TABLE ))) 258self .provider .catalog = replacement 259 260timestamp = int (time .time ())+ 60 261os .utime (path , (timestamp ,timestamp )) 262self .assertEqual ( 263self .entries (self .table ())["deepseek" ]["note" ],"remote override" 264 ) 265with self .app .api (f"/sessions/ { session } /reload" , {"target" :"both" }): 266pass 267os .utime (path , (timestamp ,timestamp )) 268self .assertEqual ( 269self .entries (self .table ())["deepseek" ]["note" ],"remote revised!" 270 ) 271 272def test_invalid_override_shadows_previous_entry_before_decoding (self ): 273write_atomic ( 274self .app .home / "cache-ttl.json" , 275 {"entries" : [{** LOCAL_OVERRIDE ["entries" ][0 ],"policy" :"invalid" }]}, 276 ) 277table = self .table () 278self .assertTrue (self .layers (table )["local" ]["loaded" ]) 279self .assertNotIn ("deepseek" ,self .entries (table )) 280self .assertIsNone (self .resolved ("&host=api.deepseek.com" )) 281self .assertEqual ( 282self .resolved ("&extension=claude" )["id" ],"claude-subscription" 283 ) 284 285def test_malformed_local_file_keeps_the_last_good_table (self ): 286write_atomic (self .app .home / "cache-ttl.json" ,LOCAL_OVERRIDE ) 287self .assertTrue (self .layers (self .table ())["local" ]["loaded" ]) 288 (self .app .home / "cache-ttl.json" ).write_text ("{ this is not json" ) 289table = self .table () 290local = self .layers (table )["local" ] 291self .assertFalse (local ["loaded" ]) 292self .assertEqual (set (local ["error" ]), {"code" ,"detail" }) 293self .assertEqual (local ["error" ]["code" ],"cache_policy_layer_failed" ) 294self .assertIn ("not valid JSON" ,local ["error" ]["detail" ]) 295self .assertLessEqual (len (local ["error" ]["detail" ]),4096 ) 296entries = self .entries (table ) 297self .assertEqual (entries ["deepseek" ]["layer" ],"local" ) 298self .assertEqual (entries ["deepseek" ]["note" ],"local override" ) 299self .assertEqual (self .resolved ("&host=gateway.laptop" )["id" ],"laptop-gateway" ) 300# Fixing the file is picked up live, without a restart. 301write_atomic (self .app .home / "cache-ttl.json" ,LOCAL_OVERRIDE ) 302self .assertTrue (self .layers (self .table ())["local" ]["loaded" ]) 303 304def test_remote_url_is_fetched_and_merged (self ): 305deadline = time .monotonic ()+ 20 306table = self .table () 307while ( 308not self .layers (table )["remote" ]["loaded" ]and time .monotonic ()< deadline 309 ): 310time .sleep (0.1 ) 311table = self .table () 312remote = self .layers (table )["remote" ] 313self .assertTrue (remote ["loaded" ]) 314self .assertEqual (remote ["path" ],str (self .app .home / "cache-ttl-remote.json" )) 315entries = self .entries (table ) 316# Replaced by id, in the default entry's place, ahead of general rules. 317ids = [entry ["id" ]for entry in table ["entries" ]] 318self .assertEqual (entries ["deepseek" ]["layer" ],"remote" ) 319self .assertEqual (entries ["deepseek" ]["note" ],"remote override" ) 320self .assertEqual (entries ["deepseek" ]["policy" ],"fixed" ) 321self .assertLess (ids .index ("claude-subscription" ),ids .index ("deepseek" )) 322# The remote layer's new id sits in front of the default entries. 323self .assertEqual (ids [0 ],"fixture-gateway" ) 324self .assertEqual ( 325self .resolved ("&host=gateway.fixture" )["id" ],"fixture-gateway" 326 ) 327 328def test_session_reload_refetches_the_remote_copy (self ): 329session = self .app .session () 330self .assertFalse ((self .app .home / "cache-ttl-remote.json" ).exists ()) 331with self .app .api ( 332f"/sessions/ { session } /reload" , {"target" :"both" } 333 )as response : 334outcome = json .load (response ) 335self .assertEqual (outcome ["session" ]["state" ],"applied" ) 336self .assertEqual (outcome ["cache_policy" ]["state" ],"refreshed" ) 337table = self .table () 338self .assertTrue (self .layers (table )["remote" ]["loaded" ]) 339self .assertEqual (self .entries (table )["deepseek" ]["note" ],"remote override" ) 340 341def test_a_turns_usage_says_when_its_cache_fades_across_a_restart (self ): 342# A provider that only evicts: the count is unknown past typical 343# survival and gone past the bound, counted from the response's end. 344write_atomic ( 345self .app .home / "cache-ttl.json" , 346 { 347"version" :1 , 348"entries" : [ 349 { 350"id" :"fixture-evict" , 351"match" : {"host" :"127.0.0.1" }, 352"policy" :"evict" , 353"survival" : {"typical" :600 ,"max" :3600 }, 354"evidence" :"measured" , 355 } 356 ], 357 }, 358 ) 359session = self .app .session () 360self .app .prompt (session ,"hello" ).close () 361self .app .idle (session ) 362with self .app .api (f"/sessions/ { session } /context?view=requests" )as response : 363finished = milliseconds (json .load (response )["items" ][0 ]["ended_at" ]) 364fading = [(finished + 600_000 ,None ), (finished + 3_600_000 ,0 )] 365 366def latest_fade (): 367with self .app .api (f"/sessions/ { session } ?tail=0" )as response : 368fade = json .load (response )["usage" ]["cache_fade" ] 369return [(milliseconds (step ["at" ]),step ["cached_tokens" ])for step in fade ] 370 371self .assertEqual (latest_fade (),fading ) 372# The steps are stored with the usage, so a client attaching to a 373# restarted daemon still counts down from the same call. 374self .app .restart () 375self .assertEqual (latest_fade (),fading ) 376 377 378class DefaultCacheTtlTests (unittest .TestCase ): 379table = CacheTtlTests .table 380resolved = CacheTtlTests .resolved 381layers = CacheTtlTests .layers 382entries = CacheTtlTests .entries 383 384def setUp (self ): 385self .app = Albedo () 386self .app .__enter__ () 387self .addCleanup (self .app .__exit__ ,None ,None ,None ) 388 389def test_shipped_table_is_served_without_overrides (self ): 390table = self .table () 391self .assertTrue (self .layers (table )["default" ]["loaded" ]) 392self .assertIn ("priv/cache-ttl.json" ,self .layers (table )["default" ]["path" ]) 393self .assertFalse (self .layers (table )["local" ]["loaded" ]) 394# Every shipped entry, each tagged with the layer it came from. 395self .assertEqual ( 396set (self .entries (table )), {e ["id" ]for e in SHIPPED ["entries" ]} 397 ) 398for entry in table ["entries" ]: 399self .assertEqual (entry ["layer" ],"default" ) 400shipped = {entry ["id" ]:entry for entry in SHIPPED ["entries" ]} 401for entry in table ["entries" ]: 402for field ,value in shipped [entry ["id" ]].items (): 403if field == "match" : 404expected = { 405name : ([pattern ]if isinstance (pattern ,str )else pattern ) 406for name ,pattern in value .items () 407 } 408for name in ("extension" ,"host" ,"model" ): 409self .assertEqual (entry [field ][name ],expected .get (name )) 410elif field == "survival" : 411for name ,number in value .items (): 412self .assertEqual (entry [field ][name ],number ) 413elif field == "tiers" : 414for observed ,tier in zip (entry [field ],value ,strict = True ): 415for name ,number in tier .items (): 416self .assertEqual (observed [name ],number ) 417else : 418self .assertEqual (entry [field ],value ) 419self .assertIsNone (self .resolved ("&extension=never-heard-of-it" )) 420 421def test_route_requires_authentication (self ): 422request = urllib .request .Request (self .app .base + "/models?view=cache-policy" ) 423with self .assertRaises (urllib .error .HTTPError )as refused : 424urllib .request .urlopen (request ,timeout = 20 ) 425self .assertEqual (refused .exception .code ,401 )