aboutsummaryrefslogtreecommitdiffstats
path: root/test_llamachat.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-09 15:26:10 +0200
committerDanilo M. <danix@danix.xyz>2026-08-09 15:26:10 +0200
commit379d9211fab61db2da9b34d6f385fc240e00a061 (patch)
tree0e1ae4d703798d3aac51904e40b1b4c66f09fdd9 /test_llamachat.py
parent86c5cdb4bb94b8d6da93c42ac48ec1abb6fa87ef (diff)
downloadllamachat-379d9211fab61db2da9b34d6f385fc240e00a061.tar.gz
llamachat-379d9211fab61db2da9b34d6f385fc240e00a061.zip
feat: store per-model metadata in models.ini
Cloud models have no presets.ini entry, so context size, vision and prices are recorded per model in a file the app writes. A cancelled dialog leaves a marker so a model tried once never asks again, which is distinct from an absent section meaning never asked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'test_llamachat.py')
-rwxr-xr-xtest_llamachat.py98
1 files changed, 98 insertions, 0 deletions
diff --git a/test_llamachat.py b/test_llamachat.py
index 29f305f..8b53d55 100755
--- a/test_llamachat.py
+++ b/test_llamachat.py
@@ -1380,6 +1380,103 @@ def test_config_providers():
print("ok config provider table")
+def test_models_store():
+ """models.ini round-trips per-model metadata and the cancel record."""
+ from llamachat import models
+
+ with tempfile.TemporaryDirectory() as tmp:
+ path = Path(tmp) / "models.ini"
+ store = models.ModelStore(path)
+
+ # Nothing recorded yet.
+ assert store.get("together:Qwen/Qwen2.5") is None
+ assert store.was_offered("together:Qwen/Qwen2.5") is False
+
+ store.save(
+ "together:Qwen/Qwen2.5",
+ models.ModelInfo(
+ ctx_size=32768, vision=False, price_in=1.2, price_out=1.2
+ ),
+ )
+ # A cancelled dialog records that it was offered, nothing more.
+ store.mark_skipped("together:Llama-Vision-Free")
+
+ # Reread from disk, not from memory: this is the round trip.
+ fresh = models.ModelStore(path)
+ info = fresh.get("together:Qwen/Qwen2.5")
+ assert info.ctx_size == 32768
+ assert info.vision is False
+ assert info.price_in == 1.2
+ assert info.price_out == 1.2
+ assert fresh.was_offered("together:Qwen/Qwen2.5") is True
+
+ assert fresh.get("together:Llama-Vision-Free") is None
+ assert fresh.was_offered("together:Llama-Vision-Free") is True
+
+ # Partial entries are legal: prices may be left blank.
+ fresh.save("together:cheap", models.ModelInfo(ctx_size=8192))
+ again = models.ModelStore(path)
+ partial = again.get("together:cheap")
+ assert partial.ctx_size == 8192
+ assert partial.price_in is None
+ assert partial.vision is None
+
+ # A model id with a colon must survive being an ini section name.
+ again.save("together:org/name:v2", models.ModelInfo(ctx_size=4096))
+ assert models.ModelStore(path).get("together:org/name:v2").ctx_size == 4096
+
+ # Real ids carry slashes, dots and mixed case. Section names are
+ # case sensitive, unlike keys, so the id must come back verbatim.
+ for real in (
+ "together:Qwen/Qwen2.5-72B-Instruct-Turbo",
+ "together:deepseek-ai/DeepSeek-V3",
+ "together:meta-llama/Llama-3.3-70B",
+ ):
+ again.save(real, models.ModelInfo(ctx_size=128000, vision=True))
+ back = models.ModelStore(path).get(real)
+ assert back.ctx_size == 128000, real
+ assert back.vision is True, real
+
+ # vision=False must not be written as "False" and read back as None:
+ # bool is an int subclass, so the write order matters.
+ again.save("together:novision", models.ModelInfo(vision=False))
+ assert models.ModelStore(path).get("together:novision").vision is False
+
+ # Cancelling a dialog over a model we already know must not erase it.
+ again.mark_skipped("together:Qwen/Qwen2.5")
+ assert models.ModelStore(path).get("together:Qwen/Qwen2.5").ctx_size == 32768
+
+ # An all-blank save is still an answer: asked, learned nothing. It
+ # must not read back as never offered, or the dialog reopens forever.
+ # configparser does round-trip a keyless section, so the marker is
+ # belt and braces against an empty section being dropped by hand.
+ again.save("together:blank", models.ModelInfo())
+ reread = models.ModelStore(path)
+ assert reread.get("together:blank") is None
+ assert reread.was_offered("together:blank") is True
+
+ # A hand-edited file must degrade, not raise: "32k" is not an int.
+ # The [DEFAULT] value is deliberately a *valid* int, so this catches
+ # the leak itself rather than an unparseable value hiding it.
+ path.write_text(
+ "[DEFAULT]\nctx_size = 999\n\n"
+ "[together:junk]\nctx_size = 32k\nprice_in = free\n\n"
+ "[together:empty]\n",
+ encoding="utf-8",
+ )
+ edited = models.ModelStore(path)
+ assert edited.get("together:junk") is None
+ assert edited.was_offered("together:junk") is True
+ # Would be ctx_size 999, inherited from [DEFAULT], if the store used
+ # configparser's real default section.
+ assert edited.get("together:empty") is None
+
+ # A file that is not ini at all reads as empty rather than raising.
+ path.write_text("this is not an ini file\n", encoding="utf-8")
+ assert models.ModelStore(path).was_offered("together:junk") is False
+ print("ok models.ini storage")
+
+
class _FakeResponse:
"""Enough of an http.client response for urlopen's context manager."""
@@ -2123,6 +2220,7 @@ if __name__ == "__main__":
test_model_ids_and_filtering()
test_key_resolution()
test_config_providers()
+ test_models_store()
test_search_tool_schema()
test_search_results_sanitising()
test_tool_call_accumulation()