test_providers.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from __future__ import annotations
  2. import asyncio
  3. import tempfile
  4. from dataclasses import asdict
  5. from pathlib import Path
  6. import unittest
  7. from model_selector.cache import SnapshotCache
  8. from model_selector.providers import ProviderModel, derive_features, provider_model_to_config_entry
  9. from model_selector.service import ModelDashboardService
  10. from model_selector.config import OpenClawConfigRepository
  11. class FakeAdapter:
  12. def __init__(self, provider_key: str, models: list[dict]) -> None:
  13. self.provider_key = provider_key
  14. self.models = models
  15. async def fetch(self):
  16. from model_selector.config import provider_label
  17. from model_selector.providers import ProviderModel
  18. return [
  19. ProviderModel(
  20. provider_key=self.provider_key,
  21. provider_label=provider_label(self.provider_key),
  22. model_id=item["id"],
  23. name=item.get("name", item["id"]),
  24. source="live",
  25. reasoning=item.get("reasoning"),
  26. context_window=item.get("context_window"),
  27. max_tokens=item.get("max_tokens"),
  28. pricing_prompt=item.get("pricing_prompt"),
  29. pricing_completion=item.get("pricing_completion"),
  30. pricing_request=item.get("pricing_request"),
  31. input_modalities=tuple(item.get("input_modalities", ("text",))),
  32. output_modalities=tuple(item.get("output_modalities", ("text",))),
  33. supported_parameters=tuple(item.get("supported_parameters", ())),
  34. active=item.get("active"),
  35. features=tuple(item.get("features", ())),
  36. raw=item,
  37. default_parameters=item.get("default_parameters"),
  38. )
  39. for item in self.models
  40. ]
  41. class ProviderTests(unittest.IsolatedAsyncioTestCase):
  42. def test_feature_derivation_understands_free_and_structured(self) -> None:
  43. raw = {
  44. "id": "openai/gpt-oss-20b",
  45. "pricing": {"prompt": "0", "completion": "0", "request": "0"},
  46. "supported_parameters": ["response_format", "tools"],
  47. "architecture": {"input_modalities": ["text"], "output_modalities": ["text"]},
  48. }
  49. features = derive_features("openrouter", raw)
  50. self.assertIn("free", features)
  51. self.assertIn("structured output", features)
  52. self.assertIn("tool use", features)
  53. def test_openrouter_reasoning_surfaces_in_config_entry(self) -> None:
  54. model = ProviderModel(
  55. provider_key="openrouter",
  56. provider_label="OpenRouter",
  57. model_id="~anthropic/claude-sonnet-latest",
  58. name="Claude Sonnet",
  59. source="live",
  60. reasoning=True,
  61. context_window=200000,
  62. max_tokens=64000,
  63. pricing_prompt=None,
  64. pricing_completion=None,
  65. pricing_request=None,
  66. input_modalities=("text", "image", "file"),
  67. output_modalities=("text",),
  68. supported_parameters=("reasoning", "response_format"),
  69. active=None,
  70. features=("reasoning",),
  71. raw={"id": "~anthropic/claude-sonnet-latest"},
  72. default_parameters={"reasoning": {"effort": "high"}},
  73. )
  74. entry = provider_model_to_config_entry(model)
  75. self.assertTrue(entry["reasoning"])
  76. self.assertEqual(entry["thinking"], {"effort": "high"})
  77. self.assertEqual(entry["input"], ["text", "image"])
  78. async def test_refresh_writes_snapshot_json(self) -> None:
  79. with tempfile.TemporaryDirectory() as tmp:
  80. config_path = Path(tmp) / "openclaw.json"
  81. cache_dir = Path(tmp) / "cache"
  82. config_path.write_text(
  83. """
  84. {
  85. "models": {
  86. "providers": {
  87. "openai": {"models": [{"id": "gpt-5.4", "name": "GPT-5.4", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]},
  88. "groq-cloud": {"models": [{"id": "openai/gpt-oss-20b", "name": "GPT OSS 20B", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]},
  89. "openrouter": {"models": [{"id": "openai/gpt-oss-120b", "name": "GPT OSS 120B", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]}
  90. }
  91. },
  92. "agents": {"defaults": {"models": {}}}
  93. }
  94. """.strip(),
  95. encoding="utf-8",
  96. )
  97. repo = OpenClawConfigRepository(config_path)
  98. service = ModelDashboardService(
  99. repo,
  100. SnapshotCache(cache_dir),
  101. adapters={
  102. "openai": FakeAdapter("openai", [{"id": "gpt-5.4", "name": "GPT-5.4", "reasoning": True}]),
  103. "groq-cloud": FakeAdapter("groq-cloud", [{"id": "openai/gpt-oss-20b", "name": "GPT OSS 20B", "reasoning": True}]),
  104. "openrouter": FakeAdapter("openrouter", [{"id": "openai/gpt-oss-120b", "name": "GPT OSS 120B"}]),
  105. },
  106. )
  107. states = await service.refresh_all()
  108. self.assertIn("openai", states)
  109. self.assertTrue((cache_dir / "openai.json").exists())
  110. self.assertTrue((cache_dir / "groq-cloud.json").exists())
  111. self.assertTrue((cache_dir / "openrouter.json").exists())