@@ -176,12 +176,12 @@ def test_template_contains_llm(self):
176176 self .assertIn ("reasoning_effort" , config .CONFIG_TEMPLATE )
177177 self .assertIn ('"stream"' , config .CONFIG_TEMPLATE )
178178 self .assertIn ('"subagent_llm"' , config .CONFIG_TEMPLATE )
179- self .assertIn ('"context_windows "' , config .CONFIG_TEMPLATE )
179+ self .assertIn ('"context_window "' , config .CONFIG_TEMPLATE )
180180
181181
182- class TestContextWindowsConfig (unittest .TestCase ):
183- """Config-file context- window overrides: loaded from the
184- ``context_windows`` object, matched before the built-in table."""
182+ class TestContextWindowConfig (unittest .TestCase ):
183+ """Context- window resolution: ``llm.context_window`` in the config
184+ file, then built-in CONTEXT_WINDOWS table, then default ."""
185185
186186 def setUp (self ):
187187 self ._saved = {k : os .environ .get (k ) for k in ENV_KEYS }
@@ -195,119 +195,91 @@ def tearDown(self):
195195 else :
196196 os .environ [k ] = v
197197
198- def test_missing_file_returns_empty (self ):
199- self .assertEqual (config .load_context_windows_config ("/no/such/file.json" ), [])
200-
201- def test_empty_section_returns_empty (self ):
202- with tempfile .TemporaryDirectory () as d :
203- p = Path (d ) / "config.json"
204- p .write_text ('{"llm": {"model": "m"}}' , encoding = "utf-8" )
205- self .assertEqual (config .load_context_windows_config (p ), [])
206-
207- def test_bad_json_returns_empty (self ):
208- with tempfile .TemporaryDirectory () as d :
209- p = Path (d ) / "config.json"
210- p .write_text ("not {valid json" , encoding = "utf-8" )
211- self .assertEqual (config .load_context_windows_config (p ), [])
212-
213- def test_overrides_loaded_in_order (self ):
198+ def test_llm_context_window_loaded (self ):
199+ """llm.context_window in the config file is read by load_llm_config."""
214200 with tempfile .TemporaryDirectory () as d :
215201 p = Path (d ) / "config.json"
216- p .write_text (
217- '{"context_windows": {"deepseek-v4": 1000000, "gpt-5": 400000}}' ,
218- encoding = "utf-8" ,
219- )
220- self .assertEqual (
221- config .load_context_windows_config (p ),
222- [("deepseek-v4" , 1000000 ), ("gpt-5" , 400000 )],
223- )
202+ p .write_text ('{"llm": {"context_window": 256000}}' , encoding = "utf-8" )
203+ settings = config .load_llm_config (p )
204+ self .assertEqual (settings ["context_window" ], 256000 )
224205
225- def test_comment_keys_skipped (self ):
226- with tempfile .TemporaryDirectory () as d :
227- p = Path (d ) / "config.json"
228- p .write_text (
229- '{"context_windows": {"_comment": "hi", "kimi": 256000}}' ,
230- encoding = "utf-8" ,
231- )
232- self .assertEqual (config .load_context_windows_config (p ), [("kimi" , 256000 )])
206+ def test_llm_context_window_none_by_default (self ):
207+ """When unset, context_window defaults to None (resolve at runtime)."""
208+ settings = config .load_llm_config ("/no/such/file.json" )
209+ self .assertIsNone (settings ["context_window" ])
233210
234- def test_section_must_be_object (self ):
211+ def test_context_window_bool_rejected (self ):
212+ """True is an int subclass but not a valid context window."""
235213 with tempfile .TemporaryDirectory () as d :
236214 p = Path (d ) / "config.json"
237- p .write_text ('{"context_windows ": "nope" }' , encoding = "utf-8" )
215+ p .write_text ('{"llm ": {"context_window": true} }' , encoding = "utf-8" )
238216 with self .assertRaises (ValueError ):
239- config .load_context_windows_config (p )
217+ config .load_llm_config (p )
240218
241- def test_size_must_be_integer (self ):
219+ def test_context_window_zero_rejected (self ):
242220 with tempfile .TemporaryDirectory () as d :
243221 p = Path (d ) / "config.json"
244- p .write_text ('{"context_windows ": {"m ": "big" }}' , encoding = "utf-8" )
222+ p .write_text ('{"llm ": {"context_window ": 0 }}' , encoding = "utf-8" )
245223 with self .assertRaises (ValueError ):
246- config .load_context_windows_config (p )
224+ config .load_llm_config (p )
247225
248- def test_bool_size_rejected (self ):
249- """True is an int subclass but not a valid token count."""
226+ def test_context_window_negative_rejected (self ):
250227 with tempfile .TemporaryDirectory () as d :
251228 p = Path (d ) / "config.json"
252- p .write_text ('{"context_windows ": {"m ": true }}' , encoding = "utf-8" )
229+ p .write_text ('{"llm ": {"context_window ": -1 }}' , encoding = "utf-8" )
253230 with self .assertRaises (ValueError ):
254- config .load_context_windows_config (p )
231+ config .load_llm_config (p )
255232
256- def test_context_windows_zero_rejected (self ):
257- """A zero context window would cause a division-by-zero at runtime."""
233+ def test_context_window_string_rejected (self ):
258234 with tempfile .TemporaryDirectory () as d :
259235 p = Path (d ) / "config.json"
260- p .write_text ('{"context_windows ": {"m ": 0 }}' , encoding = "utf-8" )
236+ p .write_text ('{"llm ": {"context_window ": "big" }}' , encoding = "utf-8" )
261237 with self .assertRaises (ValueError ):
262- config .load_context_windows_config (p )
238+ config .load_llm_config (p )
263239
264- def test_context_windows_negative_rejected (self ):
240+ def test_get_context_window_llm_setting_wins (self ):
241+ """llm.context_window beats the built-in table."""
265242 with tempfile .TemporaryDirectory () as d :
266243 p = Path (d ) / "config.json"
267- p .write_text ('{"context_windows": {"m": -1}}' , encoding = "utf-8" )
268- with self .assertRaises (ValueError ):
269- config .load_context_windows_config (p )
244+ p .write_text ('{"llm": {"context_window": 999999}}' , encoding = "utf-8" )
245+ self .assertEqual (config .get_context_window_for_model ("gpt-5-mini" , p ), 999999 )
270246
271- def test_get_context_window_precedence (self ):
272- """Config-file override -> CONTEXT_WINDOWS -> default."""
273- with tempfile .TemporaryDirectory () as d :
274- p = Path (d ) / "config.json"
275- p .write_text (
276- '{"context_windows": {"deepseek-v4": 1000000, "gpt-4-turbo": 300000}}' ,
277- encoding = "utf-8" ,
278- )
279- self .assertEqual (config .get_context_window_for_model ("deepseek-v4-flash" , p ), 1000000 )
280- # config-file match beats the built-in table
281- self .assertEqual (config .get_context_window_for_model ("gpt-4-turbo" , p ), 300000 )
282- # built-in table still applies when no override matches
283- self .assertEqual (config .get_context_window_for_model ("gpt-5-mini" , p ), 128000 )
284- self .assertEqual (config .get_context_window_for_model ("kimi-k2.7-0613" , p ), 256000 )
285- # unknown model -> default
286- self .assertEqual (
287- config .get_context_window_for_model ("unknown-model" , p ),
288- config .DEFAULT_CONTEXT_WINDOW ,
289- )
247+ def test_get_context_window_falls_to_table (self ):
248+ """No llm.context_window -> built-in CONTEXT_WINDOWS table."""
249+ self .assertEqual (
250+ config .get_context_window_for_model ("deepseek-v4" , "/no/such/file.json" ),
251+ 1_000_000 ,
252+ )
253+ self .assertEqual (
254+ config .get_context_window_for_model ("gpt-5-mini" , "/no/such/file.json" ),
255+ 128_000 ,
256+ )
257+
258+ def test_get_context_window_falls_to_default (self ):
259+ """Unknown model with no config -> DEFAULT_CONTEXT_WINDOW."""
260+ self .assertEqual (
261+ config .get_context_window_for_model ("totally-unknown" , "/no/such/file.json" ),
262+ config .DEFAULT_CONTEXT_WINDOW ,
263+ )
290264
291265 def test_get_context_window_case_insensitive (self ):
292- with tempfile .TemporaryDirectory () as d :
293- p = Path (d ) / "config.json"
294- p .write_text ('{"context_windows": {"DeepSeek-V4": 1000000}}' , encoding = "utf-8" )
295- self .assertEqual (config .get_context_window_for_model ("deepseek-v4-flash" , p ), 1000000 )
266+ """Built-in table matching is case-insensitive."""
267+ self .assertEqual (
268+ config .get_context_window_for_model ("DeepSeek-V4" , "/no/such/file.json" ),
269+ 1_000_000 ,
270+ )
296271
297272 def test_get_context_window_provider_prefixed_name (self ):
298273 """Provider-prefixed model names (e.g. "ZhipuAI/GLM-5.2") match
299274 their table entry via case-insensitive substring match."""
300- # provider prefix + uppercase org, model name in the middle
301275 self .assertEqual (
302276 config .get_context_window_for_model ("ZhipuAI/GLM-5.2" , "/no/such/config.json" ),
303277 1_000_000 ,
304278 )
305- # lowercased provider prefix matches too
306279 self .assertEqual (
307280 config .get_context_window_for_model ("zhipuai/glm-5.2" , "/no/such/config.json" ),
308281 1_000_000 ,
309282 )
310- # plain model name still matches
311283 self .assertEqual (
312284 config .get_context_window_for_model ("glm-5.2" , "/no/such/config.json" ),
313285 1_000_000 ,
@@ -317,23 +289,99 @@ def test_get_context_window_provider_prefixed_name(self):
317289 config .get_context_window_for_model ("ZhipuAI/GLM-5.3" , "/no/such/config.json" ),
318290 config .DEFAULT_CONTEXT_WINDOW ,
319291 )
320- # provider prefix must not break other families either
321292 self .assertEqual (
322293 config .get_context_window_for_model (
323294 "DeepSeek/deepseek-v4-flash" , "/no/such/config.json"
324295 ),
325296 1_000_000 ,
326297 )
327298
328- def test_get_context_window_no_file (self ):
329- self .assertEqual (
330- config .get_context_window_for_model ("deepseek-v4" , "/no/such/file.json" ),
331- 1_000_000 ,
332- )
333- self .assertEqual (
334- config .get_context_window_for_model ("totally-unknown" , "/no/such/file.json" ),
335- config .DEFAULT_CONTEXT_WINDOW ,
336- )
299+ def test_resolve_context_window_from_settings (self ):
300+ """resolve_context_window uses settings['context_window'] first."""
301+ settings = {"model" : "gpt-5-mini" , "context_window" : 500_000 }
302+ self .assertEqual (config .resolve_context_window (settings ), 500_000 )
303+
304+ def test_resolve_context_window_falls_to_table (self ):
305+ """resolve_context_window falls to built-in table when context_window is None."""
306+ settings = {"model" : "deepseek-v4" , "context_window" : None }
307+ self .assertEqual (config .resolve_context_window (settings ), 1_000_000 )
308+
309+ def test_resolve_context_window_falls_to_default (self ):
310+ """resolve_context_window uses DEFAULT_CONTEXT_WINDOW for unknown models."""
311+ settings = {"model" : "unknown" , "context_window" : None }
312+ self .assertEqual (config .resolve_context_window (settings ), config .DEFAULT_CONTEXT_WINDOW )
313+
314+ def test_context_window_inherits_in_subagent (self ):
315+ """context_window set in llm inherits to subagent_llm when unset."""
316+ with tempfile .TemporaryDirectory () as d :
317+ p = Path (d ) / "config.json"
318+ p .write_text (
319+ '{"llm": {"model": "m", "context_window": 500000}}' ,
320+ encoding = "utf-8" ,
321+ )
322+ main = config .load_llm_config (p )
323+ sub = config .load_subagent_llm_config (p , main = main )
324+ self .assertEqual (sub ["context_window" ], 500000 )
325+
326+ def test_context_window_subagent_override (self ):
327+ """subagent_llm.context_window overrides the inherited value."""
328+ with tempfile .TemporaryDirectory () as d :
329+ p = Path (d ) / "config.json"
330+ p .write_text (
331+ '{"llm": {"model": "m", "context_window": 500000},'
332+ ' "subagent_llm": {"context_window": 200000}}' ,
333+ encoding = "utf-8" ,
334+ )
335+ main = config .load_llm_config (p )
336+ sub = config .load_subagent_llm_config (p , main = main )
337+ self .assertEqual (sub ["context_window" ], 200000 )
338+
339+ def test_context_window_profile_override (self ):
340+ """A models profile's context_window overrides the llm value
341+ when referenced via subagent_llm.profile."""
342+ with tempfile .TemporaryDirectory () as d :
343+ p = Path (d ) / "config.json"
344+ p .write_text (
345+ '{"llm": {"model": "m", "context_window": 500000},'
346+ ' "models": {"cheap": {"model": "cheap-m", "context_window": 300000}},'
347+ ' "subagent_llm": {"profile": "cheap"}}' ,
348+ encoding = "utf-8" ,
349+ )
350+ main = config .load_llm_config (p )
351+ sub = config .load_subagent_llm_config (p , main = main )
352+ self .assertEqual (sub ["context_window" ], 300000 )
353+
354+ def test_old_context_windows_section_emits_deprecation (self ):
355+ """A config file with the old top-level context_windows section
356+ triggers a DeprecationWarning on load_llm_config."""
357+ import warnings
358+
359+ with tempfile .TemporaryDirectory () as d :
360+ p = Path (d ) / "config.json"
361+ p .write_text (
362+ '{"llm": {"model": "m"}, "context_windows": {"deepseek-v4": 1000000}}' ,
363+ encoding = "utf-8" ,
364+ )
365+ with warnings .catch_warnings (record = True ) as w :
366+ warnings .simplefilter ("always" )
367+ config .load_llm_config (p )
368+ dep_warnings = [x for x in w if issubclass (x .category , DeprecationWarning )]
369+ self .assertEqual (len (dep_warnings ), 1 )
370+ self .assertIn ("context_windows" , str (dep_warnings [0 ].message ))
371+ self .assertIn ("deprecated" , str (dep_warnings [0 ].message ))
372+
373+ def test_no_deprecation_without_old_section (self ):
374+ """No warning when the config file has no context_windows section."""
375+ import warnings
376+
377+ with tempfile .TemporaryDirectory () as d :
378+ p = Path (d ) / "config.json"
379+ p .write_text ('{"llm": {"model": "m", "context_window": 500000}}' , encoding = "utf-8" )
380+ with warnings .catch_warnings (record = True ) as w :
381+ warnings .simplefilter ("always" )
382+ config .load_llm_config (p )
383+ dep_warnings = [x for x in w if issubclass (x .category , DeprecationWarning )]
384+ self .assertEqual (len (dep_warnings ), 0 )
337385
338386
339387class TestSubagentLlmConfig (unittest .TestCase ):
0 commit comments