Model switch lost the conversation: on 10 Oct a 1 MB thread was compacted by the newly selected small model into 2201 characters. The compaction summary for a bridge model is now written by a subscription model (ozet_kopru.ozet_modeli, default gpt-6.1-sol); if that request is refused before it starts, the target model is used as before. A summary far shorter than the conversation warrants is not accepted: Codex gets response.failed and keeps the history. The line under each added model no longer repeats its name. Teams show their members, fallback chains their order, single models their provider and the traits readable from the name; the text fits one phone row (30 characters). Also normalizes tool call ids in the streamed response so Codex persists ids the subscription backend accepts (fc_ -> ctc_), and bumps the bridge protocol to 6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
7.5 KiB
Python
111 lines
7.5 KiB
Python
"""The shipped IDE must use the same public OmniRoute catalog as every member."""
|
||
import io
|
||
import json
|
||
import unittest
|
||
from unittest.mock import patch
|
||
import ide_catalog as c
|
||
|
||
|
||
def row(mid, name, source="omniroute", capabilities=("chat",)):
|
||
return {"id": mid, "turkservis": {"display_name": name, "source": source,
|
||
"capabilities": list(capabilities)}}
|
||
|
||
|
||
class MemberCatalog(unittest.TestCase):
|
||
def test_metadata_classifies_only_public_models(self):
|
||
public = [row("plain", "Plain"), row("team", "Team")]
|
||
metadata = [{"id": "plain", "kind": "model"}, {"id": "team", "kind": "combo"},
|
||
{"id": "internal", "kind": "combo"}]
|
||
replies = [io.BytesIO(json.dumps({"data": public}).encode()),
|
||
io.BytesIO(json.dumps({"models": metadata}).encode())]
|
||
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
|
||
patch.object(c, "anahtar_oku", return_value="test-key"), \
|
||
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
|
||
patch.object(c.urllib.request, "urlopen", side_effect=replies):
|
||
status, _, _, selected = c.yukle()
|
||
self.assertEqual(status, "tamam")
|
||
self.assertEqual({m["id"]: m["kind"] for m in selected}, {"plain": "model", "team": "combo"})
|
||
|
||
def test_empty_catalog_is_a_refresh_failure(self):
|
||
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
|
||
patch.object(c, "anahtar_oku", return_value="test-key"), \
|
||
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
|
||
patch.object(c.urllib.request, "urlopen", return_value=io.BytesIO(b'{"data": []}')):
|
||
with self.assertRaises(ValueError):
|
||
c.yukle()
|
||
|
||
def test_public_omniroute_models_do_not_need_private_local_configuration(self):
|
||
rows = [row("9router-nano-x", "9R · nano/x"), row("agnes-flat", "Agnes Flat"),
|
||
row("Ajan-Arac", "Ajan-Arac"), row("image", "image", capabilities=("image",))]
|
||
self.assertEqual({m["id"] for m in c.sec(rows, [])},
|
||
{"9router-nano-x", "agnes-flat", "Ajan-Arac"})
|
||
|
||
def test_direct_route_is_never_added_beside_omniroute(self):
|
||
rows = [row("9router-nano-x-direct", "9R · nano/x", "9router"),
|
||
row("9router-nano-x", "9R · nano/x")]
|
||
self.assertEqual([m["id"] for m in c.sec(rows, ["nano/x"], "hepsi")], ["9router-nano-x"])
|
||
|
||
def test_model_order_uses_combo_metadata_and_preserves_every_model(self):
|
||
models = [{"slug": "gpt-z", "display_name": "GPT Z", "priority": 1},
|
||
{"slug": "ide/bekci-kurnaz", "display_name": "kurnaz", "priority": 2},
|
||
{"slug": "ide/Z-Team", "display_name": "Z-Team", "priority": 5},
|
||
{"slug": "ide/9router-cc-opus", "display_name": "9R · cc/opus", "priority": 3},
|
||
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 4}]
|
||
extras = [{"slug": m["slug"], "kind": "combo" if m["slug"].endswith("Team") else "model"}
|
||
for m in models if m["slug"].startswith("ide/")]
|
||
c.sirala_codex(models, extras)
|
||
self.assertEqual([m["slug"] for m in models],
|
||
["gpt-z", "ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus"])
|
||
self.assertEqual([m["priority"] for m in models], [1, 2, 3, 4, 5])
|
||
|
||
def test_subscription_models_keep_their_own_order_priority_and_name(self):
|
||
models = [{"slug": "ide/GPT-6-Astra", "display_name": "GPT-6-Astra", "priority": 3},
|
||
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2},
|
||
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 0},
|
||
{"slug": "gpt-6.1-sol", "display_name": "GPT-6.1-Sol", "priority": 1},
|
||
{"slug": "codex-auto-review", "display_name": "Codex Auto Review", "priority": 43}]
|
||
models.append({"slug": "ide/GPT-6-1-Sol", "display_name": "GPT-6-1-Sol", "priority": 2})
|
||
extras = [{"slug": "ide/GPT-6-Astra", "kind": "combo"}, {"slug": "ide/A-Team", "kind": "combo"},
|
||
{"slug": "ide/GPT-6-1-Sol", "kind": "combo"}]
|
||
originals = [dict(m) for m in models if not m["slug"].startswith("ide/")]
|
||
c.sirala_codex(models, extras)
|
||
self.assertEqual([m for m in models if not m["slug"].startswith("ide/")], originals)
|
||
self.assertEqual([m["slug"] for m in models],
|
||
["gpt-6-astra", "gpt-6.1-sol", "codex-auto-review", "ide/A-Team", "ide/GPT-6-1-Sol",
|
||
"ide/GPT-6-Astra"])
|
||
self.assertEqual([m["priority"] for m in models[3:]], [44, 45, 46])
|
||
self.assertEqual([m["display_name"] for m in models[4:]], ["GPT-6-1-Sol (AITURK)", "GPT-6-Astra (AITURK)"])
|
||
|
||
def test_description_line_tells_what_the_entry_is_and_fits_a_phone_row(self):
|
||
yedek = {"kind": "combo", "model": "Yedek", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
|
||
{"kind": "model", "model": "nano/abliteration-ai/abliterated-model-large-v2"}]}
|
||
takim = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "combo", "model": "Takim-Hizli", "strategy": "fusion", "members": [
|
||
{"kind": "model", "model": "ag/gemini-3.8-flash-high"}, {"kind": "model", "model": "cc/claude-sonnet-5-5"}]},
|
||
yedek]}}
|
||
self.assertEqual(c.aciklama("Takim-Hizli", "Takim-Hizli", "combo", takim), "Takım: gemini+sonnet")
|
||
sirali = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": "cx/gpt-6-astra"}, {"kind": "model", "model": "cc/claude-opus-5-5"},
|
||
{"kind": "model", "model": "cc/claude-sonnet-5-5"}, {"kind": "model", "model": "ag/gemini-3.8-flash-high"}]}}
|
||
self.assertEqual(c.aciklama("Ajan-Arac", "Ajan-Arac", "combo", sirali), "Sıralı: astra>opus>sonnet +1")
|
||
# An unresolved first member stays visible instead of being hidden behind the fallback.
|
||
eksik = {"topology": {"kind": "combo", "strategy": "priority", "members": [{"kind": "combo", "model": None}, yedek]}}
|
||
self.assertEqual(c.aciklama("GPT-6-Sol", "GPT-6-Sol", "combo", eksik), "Sıralı: ?>glm>abliterated")
|
||
donen = {"topology": {"kind": "combo", "strategy": "round-robin", "members": [yedek, yedek, yedek]}}
|
||
self.assertEqual(c.aciklama("Takim-Sira", "Takim-Sira", "combo", donen), "Dönüşümlü: 3 üye")
|
||
self.assertEqual(c.aciklama("X", "X", "combo", None), "Takım")
|
||
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"), "Tek model · Claude")
|
||
self.assertEqual(c.aciklama("9router-nano-qwen-qwen3.8-27b-uncensored-thinking",
|
||
"9R · nano/qwen3.8-27b-uncensored:thinking", "model"),
|
||
"NanoGPT · sansürsüz · düşünen")
|
||
self.assertEqual(c.aciklama("bekci-kurnaz", "kurnaz", "model"), "Tek model · Bekçi")
|
||
uzun = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||
{"kind": "model", "model": f"x/model{chr(97 + i) * 9}"} for i in range(8)]}}
|
||
self.assertLessEqual(len(c.aciklama("U", "U", "combo", uzun)), c.ACIKLAMA_SINIRI)
|
||
for ad in ("Takim-Hizli", "9R · cc/claude-opus-5-5"):
|
||
self.assertNotIn(ad, c.aciklama(ad, ad, "combo" if "Takim" in ad else "model", takim))
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|