config.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. from __future__ import annotations
  2. import json
  3. import shutil
  4. import tempfile
  5. from dataclasses import dataclass
  6. from pathlib import Path
  7. from typing import Any, Iterable
  8. PROVIDER_LABELS = {
  9. "openai": "OpenAI",
  10. "groq-cloud": "Groq",
  11. "openrouter": "OpenRouter",
  12. }
  13. @dataclass(frozen=True)
  14. class ConfiguredModel:
  15. provider_key: str
  16. provider_label: str
  17. model_key: str
  18. model_id: str
  19. name: str
  20. reasoning: bool | None
  21. inputs: tuple[str, ...]
  22. context_window: int | None
  23. max_tokens: int | None
  24. alias: str | None
  25. @property
  26. def full_key(self) -> str:
  27. return f"{self.provider_key}/{self.model_id}"
  28. def provider_label(provider_key: str) -> str:
  29. return PROVIDER_LABELS.get(provider_key, provider_key.replace("-", " ").title())
  30. class OpenClawConfigRepository:
  31. def __init__(self, config_path: Path) -> None:
  32. self.config_path = config_path
  33. def load(self) -> dict[str, Any]:
  34. with self.config_path.open("r", encoding="utf-8") as handle:
  35. return json.load(handle)
  36. def save(self, config: dict[str, Any]) -> None:
  37. self.config_path.parent.mkdir(parents=True, exist_ok=True)
  38. if self.config_path.exists():
  39. backup_path = self.config_path.with_suffix(self.config_path.suffix + ".bak")
  40. shutil.copy2(self.config_path, backup_path)
  41. with tempfile.NamedTemporaryFile(
  42. "w",
  43. encoding="utf-8",
  44. dir=self.config_path.parent,
  45. delete=False,
  46. ) as handle:
  47. json.dump(config, handle, indent=2, ensure_ascii=False)
  48. handle.write("\n")
  49. temp_path = Path(handle.name)
  50. temp_path.replace(self.config_path)
  51. def configured_models(self, config: dict[str, Any] | None = None) -> list[ConfiguredModel]:
  52. data = config if config is not None else self.load()
  53. providers = data.get("models", {}).get("providers", {})
  54. aliases = data.get("agents", {}).get("defaults", {}).get("models", {})
  55. models: list[ConfiguredModel] = []
  56. for provider_key, provider in providers.items():
  57. provider_models = provider.get("models", []) or []
  58. for model in provider_models:
  59. model_id = str(model.get("id", ""))
  60. full_key = f"{provider_key}/{model_id}"
  61. alias = aliases.get(full_key, {}).get("alias")
  62. models.append(
  63. ConfiguredModel(
  64. provider_key=provider_key,
  65. provider_label=provider_label(provider_key),
  66. model_key=full_key,
  67. model_id=model_id,
  68. name=str(model.get("name", model_id)),
  69. reasoning=model.get("reasoning"),
  70. inputs=tuple(model.get("input", []) or ()),
  71. context_window=model.get("contextWindow"),
  72. max_tokens=model.get("maxTokens"),
  73. alias=alias,
  74. )
  75. )
  76. return models
  77. def update_alias(self, full_key: str, alias: str | None) -> dict[str, Any]:
  78. config = self.load()
  79. model_aliases = self._aliases_section(config)
  80. if alias:
  81. model_aliases[full_key] = {"alias": alias}
  82. else:
  83. model_aliases.pop(full_key, None)
  84. self.save(config)
  85. return config
  86. def add_model(self, provider_key: str, model_entry: dict[str, Any]) -> dict[str, Any]:
  87. config = self.load()
  88. providers = self._providers_section(config)
  89. provider = providers.setdefault(provider_key, {})
  90. models = provider.setdefault("models", [])
  91. model_id = str(model_entry.get("id", ""))
  92. existing = next((item for item in models if str(item.get("id", "")) == model_id), None)
  93. if existing is None:
  94. models.append(model_entry)
  95. else:
  96. existing.clear()
  97. existing.update(model_entry)
  98. self.save(config)
  99. return config
  100. def remove_model(self, full_key: str) -> dict[str, Any]:
  101. provider_key, model_id = self._split_model_key(full_key)
  102. config = self.load()
  103. providers = self._providers_section(config)
  104. provider = providers.get(provider_key)
  105. if provider:
  106. models = provider.setdefault("models", [])
  107. provider["models"] = [item for item in models if str(item.get("id", "")) != model_id]
  108. self._aliases_section(config).pop(full_key, None)
  109. self.save(config)
  110. return config
  111. def ensure_copy(self, destination: Path) -> None:
  112. destination.parent.mkdir(parents=True, exist_ok=True)
  113. shutil.copy2(self.config_path, destination)
  114. def _providers_section(self, config: dict[str, Any]) -> dict[str, Any]:
  115. return config.setdefault("models", {}).setdefault("providers", {})
  116. def _aliases_section(self, config: dict[str, Any]) -> dict[str, Any]:
  117. return config.setdefault("agents", {}).setdefault("defaults", {}).setdefault("models", {})
  118. def _split_model_key(self, full_key: str) -> tuple[str, str]:
  119. if "/" not in full_key:
  120. raise ValueError("model key must be provider/model")
  121. provider_key, model_id = full_key.split("/", 1)
  122. return provider_key, model_id
  123. def iter_configured_model_keys(config: dict[str, Any]) -> Iterable[str]:
  124. repo = OpenClawConfigRepository(Path("/dev/null"))
  125. for model in repo.configured_models(config):
  126. yield model.full_key