@@ -36,31 +36,37 @@ def _isolated_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
3636 monkeypatch .setattr ("autointent.generation._cache.user_cache_dir" , lambda * _ : str (tmp_path ))
3737
3838
39+ MODEL_NAME = "test-model"
40+ BASE_URL : str | None = None
41+
42+
3943def test_set_get_memory_roundtrip () -> None :
4044 cache = StructuredOutputCache (use_cache = True )
4145 result = CacheModel (name = "a" , value = 1 )
42- cache .set (MESSAGES , CacheModel , PARAMS , result )
43- assert cache .get (MESSAGES , CacheModel , PARAMS ) == result
46+ cache .set (MESSAGES , CacheModel , PARAMS , result , MODEL_NAME , BASE_URL )
47+ assert cache .get (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL ) == result
4448
4549
4650def test_disabled_cache_is_noop () -> None :
4751 cache = StructuredOutputCache (use_cache = False )
48- cache .set (MESSAGES , CacheModel , PARAMS , CacheModel (name = "a" , value = 1 ))
49- assert cache .get (MESSAGES , CacheModel , PARAMS ) is None
52+ cache .set (MESSAGES , CacheModel , PARAMS , CacheModel (name = "a" , value = 1 ), MODEL_NAME , BASE_URL )
53+ assert cache .get (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL ) is None
5054
5155
5256def test_get_misses_for_unknown_key () -> None :
5357 cache = StructuredOutputCache (use_cache = True )
54- assert cache .get (MESSAGES , CacheModel , PARAMS ) is None
58+ assert cache .get (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL ) is None
5559
5660
5761def test_get_loads_from_disk_in_fresh_instance () -> None :
5862 """A second instance has empty memory and must read the entry back from disk."""
59- StructuredOutputCache (use_cache = True ).set (MESSAGES , CacheModel , PARAMS , CacheModel (name = "x" , value = 9 ))
63+ StructuredOutputCache (use_cache = True ).set (
64+ MESSAGES , CacheModel , PARAMS , CacheModel (name = "x" , value = 9 ), MODEL_NAME , BASE_URL
65+ )
6066
6167 fresh = StructuredOutputCache (use_cache = True )
6268 fresh ._memory_cache .clear () # force the disk path even if eager load changes
63- loaded = fresh .get (MESSAGES , CacheModel , PARAMS )
69+ loaded = fresh .get (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL )
6470 assert isinstance (loaded , CacheModel )
6571 assert loaded .value == 9
6672 # disk hit populates the memory cache for next time
@@ -69,7 +75,7 @@ def test_get_loads_from_disk_in_fresh_instance() -> None:
6975
7076def test_memory_type_mismatch_evicts () -> None :
7177 cache = StructuredOutputCache (use_cache = True )
72- key = cache ._get_cache_key (MESSAGES , CacheModel , PARAMS )
78+ key = cache ._get_cache_key (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL )
7379 cache ._memory_cache [key ] = OtherModel (text = "wrong" )
7480 assert cache ._check_memory_cache (key , CacheModel ) is None
7581 assert key not in cache ._memory_cache
@@ -79,26 +85,28 @@ def test_memory_type_mismatch_evicts() -> None:
7985async def test_async_set_get_roundtrip () -> None :
8086 cache = StructuredOutputCache (use_cache = True )
8187 result = CacheModel (name = "async" , value = 7 )
82- await cache .set_async (MESSAGES , CacheModel , PARAMS , result )
83- assert await cache .get_async (MESSAGES , CacheModel , PARAMS ) == result
88+ await cache .set_async (MESSAGES , CacheModel , PARAMS , result , MODEL_NAME , BASE_URL )
89+ assert await cache .get_async (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL ) == result
8490
8591
8692@pytest .mark .asyncio
8793async def test_async_get_loads_from_disk () -> None :
88- await StructuredOutputCache (use_cache = True ).set_async (MESSAGES , CacheModel , PARAMS , CacheModel (name = "d" , value = 3 ))
94+ await StructuredOutputCache (use_cache = True ).set_async (
95+ MESSAGES , CacheModel , PARAMS , CacheModel (name = "d" , value = 3 ), MODEL_NAME , BASE_URL
96+ )
8997
9098 fresh = StructuredOutputCache (use_cache = True )
9199 fresh ._memory_cache .clear ()
92- loaded = await fresh .get_async (MESSAGES , CacheModel , PARAMS )
100+ loaded = await fresh .get_async (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL )
93101 assert isinstance (loaded , CacheModel )
94102 assert loaded .value == 3
95103
96104
97105@pytest .mark .asyncio
98106async def test_async_disabled_cache_is_noop () -> None :
99107 cache = StructuredOutputCache (use_cache = False )
100- await cache .set_async (MESSAGES , CacheModel , PARAMS , CacheModel (name = "a" , value = 1 ))
101- assert await cache .get_async (MESSAGES , CacheModel , PARAMS ) is None
108+ await cache .set_async (MESSAGES , CacheModel , PARAMS , CacheModel (name = "a" , value = 1 ), MODEL_NAME , BASE_URL )
109+ assert await cache .get_async (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL ) is None
102110
103111
104112# --- Regression tests for the on-disk-cache bugs (#326 eager load, #327 eviction) ---
@@ -109,10 +117,12 @@ async def test_async_disabled_cache_is_noop() -> None:
109117
110118def test_eager_load_populates_memory_from_disk () -> None :
111119 """A fresh instance eagerly batch-loads existing on-disk entries into memory (#326)."""
112- StructuredOutputCache (use_cache = True ).set (MESSAGES , CacheModel , PARAMS , CacheModel (name = "x" , value = 9 ))
120+ StructuredOutputCache (use_cache = True ).set (
121+ MESSAGES , CacheModel , PARAMS , CacheModel (name = "x" , value = 9 ), MODEL_NAME , BASE_URL
122+ )
113123
114124 fresh = StructuredOutputCache (use_cache = True )
115- key = fresh ._get_cache_key (MESSAGES , CacheModel , PARAMS )
125+ key = fresh ._get_cache_key (MESSAGES , CacheModel , PARAMS , MODEL_NAME , BASE_URL )
116126
117127 # populated at construction by the eager load, before any get() call
118128 assert key in fresh ._memory_cache
@@ -137,7 +147,7 @@ def test_disk_type_mismatch_evicts_entry() -> None:
137147 """A type-mismatched disk entry is evicted (rmtree) instead of crashing on unlink (#327)."""
138148 cache = StructuredOutputCache (use_cache = True )
139149 # plant a CacheModel at the key the cache derives for OtherModel inputs
140- key = cache ._get_cache_key (MESSAGES , OtherModel , PARAMS )
150+ key = cache ._get_cache_key (MESSAGES , OtherModel , PARAMS , MODEL_NAME , BASE_URL )
141151 cache ._save_to_disk (key , CacheModel (name = "x" , value = 1 ))
142152 cache ._memory_cache .clear ()
143153
@@ -149,9 +159,84 @@ def test_disk_type_mismatch_evicts_entry() -> None:
149159async def test_async_disk_type_mismatch_evicts_entry () -> None :
150160 """Async type-mismatched disk entry is evicted (rmtree) instead of crashing on unlink (#327)."""
151161 cache = StructuredOutputCache (use_cache = True )
152- key = cache ._get_cache_key (MESSAGES , OtherModel , PARAMS )
162+ key = cache ._get_cache_key (MESSAGES , OtherModel , PARAMS , MODEL_NAME , BASE_URL )
153163 await cache ._save_to_disk_async (key , CacheModel (name = "x" , value = 1 ))
154164 cache ._memory_cache .clear ()
155165
156166 assert await cache ._load_from_disk_async (key , OtherModel ) is None
157167 assert not _get_structured_output_cache_path (key ).exists ()
168+
169+
170+ # --- Regression tests for model-identity cache collision (#334) ---
171+
172+
173+ def test_different_model_names_do_not_collide () -> None :
174+ """Two generators with different model_name must NOT share a cache entry (#334)."""
175+ result_a = CacheModel (name = "from-model-a" , value = 1 )
176+ result_b = CacheModel (name = "from-model-b" , value = 2 )
177+
178+ cache = StructuredOutputCache (use_cache = True )
179+ cache .set (MESSAGES , CacheModel , PARAMS , result_a , model_name = "model-a" , base_url = None )
180+ cache .set (MESSAGES , CacheModel , PARAMS , result_b , model_name = "model-b" , base_url = None )
181+
182+ hit_a = cache .get (MESSAGES , CacheModel , PARAMS , model_name = "model-a" , base_url = None )
183+ hit_b = cache .get (MESSAGES , CacheModel , PARAMS , model_name = "model-b" , base_url = None )
184+
185+ assert hit_a == result_a , "model-a should get its own cached value"
186+ assert hit_b == result_b , "model-b must NOT get model-a's value"
187+
188+
189+ def test_different_base_urls_do_not_collide () -> None :
190+ """Two generators with different base_url must NOT share a cache entry (#334)."""
191+ result_x = CacheModel (name = "from-url-x" , value = 10 )
192+ result_y = CacheModel (name = "from-url-y" , value = 20 )
193+
194+ cache = StructuredOutputCache (use_cache = True )
195+ cache .set (MESSAGES , CacheModel , PARAMS , result_x , model_name = "gpt-4o" , base_url = "http://host-x/v1" )
196+ cache .set (MESSAGES , CacheModel , PARAMS , result_y , model_name = "gpt-4o" , base_url = "http://host-y/v1" )
197+
198+ hit_x = cache .get (MESSAGES , CacheModel , PARAMS , model_name = "gpt-4o" , base_url = "http://host-x/v1" )
199+ hit_y = cache .get (MESSAGES , CacheModel , PARAMS , model_name = "gpt-4o" , base_url = "http://host-y/v1" )
200+
201+ assert hit_x == result_x , "host-x should get its own cached value"
202+ assert hit_y == result_y , "host-y must NOT get host-x's value"
203+
204+
205+ def test_same_identity_still_hits_cache () -> None :
206+ """Same model_name + base_url + inputs must continue to yield a cache hit (#334)."""
207+ result = CacheModel (name = "same" , value = 42 )
208+
209+ cache = StructuredOutputCache (use_cache = True )
210+ cache .set (MESSAGES , CacheModel , PARAMS , result , model_name = "gpt-4o" , base_url = "http://host/v1" )
211+
212+ hit = cache .get (MESSAGES , CacheModel , PARAMS , model_name = "gpt-4o" , base_url = "http://host/v1" )
213+ assert hit == result
214+
215+
216+ @pytest .mark .asyncio
217+ async def test_async_different_model_names_do_not_collide () -> None :
218+ """Async paths: two model names must NOT collide (#334)."""
219+ result_a = CacheModel (name = "async-a" , value = 1 )
220+ result_b = CacheModel (name = "async-b" , value = 2 )
221+
222+ cache = StructuredOutputCache (use_cache = True )
223+ await cache .set_async (MESSAGES , CacheModel , PARAMS , result_a , model_name = "async-model-a" , base_url = None )
224+ await cache .set_async (MESSAGES , CacheModel , PARAMS , result_b , model_name = "async-model-b" , base_url = None )
225+
226+ hit_a = await cache .get_async (MESSAGES , CacheModel , PARAMS , model_name = "async-model-a" , base_url = None )
227+ hit_b = await cache .get_async (MESSAGES , CacheModel , PARAMS , model_name = "async-model-b" , base_url = None )
228+
229+ assert hit_a == result_a
230+ assert hit_b == result_b
231+
232+
233+ @pytest .mark .asyncio
234+ async def test_async_same_identity_still_hits_cache () -> None :
235+ """Async paths: same identity must still yield a hit (#334)."""
236+ result = CacheModel (name = "async-same" , value = 99 )
237+
238+ cache = StructuredOutputCache (use_cache = True )
239+ await cache .set_async (MESSAGES , CacheModel , PARAMS , result , model_name = "gpt-4o" , base_url = None )
240+
241+ hit = await cache .get_async (MESSAGES , CacheModel , PARAMS , model_name = "gpt-4o" , base_url = None )
242+ assert hit == result
0 commit comments