test_providers.py 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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 derive_features
  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. )
  38. for item in self.models
  39. ]
  40. class ProviderTests(unittest.IsolatedAsyncioTestCase):
  41. def test_feature_derivation_understands_free_and_structured(self) -> None:
  42. raw = {
  43. "id": "openai/gpt-oss-20b",
  44. "pricing": {"prompt": "0", "completion": "0", "request": "0"},
  45. "supported_parameters": ["response_format", "tools"],
  46. "architecture": {"input_modalities": ["text"], "output_modalities": ["text"]},
  47. }
  48. features = derive_features("openrouter", raw)
  49. self.assertIn("free", features)
  50. self.assertIn("structured output", features)
  51. self.assertIn("tool use", features)
  52. async def test_refresh_writes_snapshot_json(self) -> None:
  53. with tempfile.TemporaryDirectory() as tmp:
  54. config_path = Path(tmp) / "openclaw.json"
  55. cache_dir = Path(tmp) / "cache"
  56. config_path.write_text(
  57. """
  58. {
  59. "models": {
  60. "providers": {
  61. "openai": {"models": [{"id": "gpt-5.4", "name": "GPT-5.4", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]},
  62. "groq-cloud": {"models": [{"id": "openai/gpt-oss-20b", "name": "GPT OSS 20B", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]},
  63. "openrouter": {"models": [{"id": "openai/gpt-oss-120b", "name": "GPT OSS 120B", "reasoning": true, "input": ["text"], "contextWindow": 100, "maxTokens": 10}]}
  64. }
  65. },
  66. "agents": {"defaults": {"models": {}}}
  67. }
  68. """.strip(),
  69. encoding="utf-8",
  70. )
  71. repo = OpenClawConfigRepository(config_path)
  72. service = ModelDashboardService(
  73. repo,
  74. SnapshotCache(cache_dir),
  75. adapters={
  76. "openai": FakeAdapter("openai", [{"id": "gpt-5.4", "name": "GPT-5.4", "reasoning": True}]),
  77. "groq-cloud": FakeAdapter("groq-cloud", [{"id": "openai/gpt-oss-20b", "name": "GPT OSS 20B", "reasoning": True}]),
  78. "openrouter": FakeAdapter("openrouter", [{"id": "openai/gpt-oss-120b", "name": "GPT OSS 120B"}]),
  79. },
  80. )
  81. states = await service.refresh_all()
  82. self.assertIn("openai", states)
  83. self.assertTrue((cache_dir / "openai.json").exists())
  84. self.assertTrue((cache_dir / "groq-cloud.json").exists())
  85. self.assertTrue((cache_dir / "openrouter.json").exists())