test_openclaw_current_app_defaults.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. from __future__ import annotations
  2. import gc
  3. from dataclasses import replace
  4. from pathlib import Path
  5. import pytest
  6. pytest.importorskip("fastapi.testclient")
  7. from fastapi import FastAPI
  8. from agency_swarm.integrations import openclaw as openclaw_mod
  9. from agency_swarm.integrations.openclaw import attach_openclaw_to_fastapi, build_openclaw_responses_model
  10. from agency_swarm.utils.model_utils import get_usage_tracking_model_name
  11. from tests.integration.fastapi._openclaw_test_support import (
  12. _build_openclaw_config,
  13. reset_openclaw_current_app_defaults,
  14. )
  15. @pytest.fixture(autouse=True)
  16. def _reset_openclaw_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
  17. reset_openclaw_current_app_defaults(monkeypatch)
  18. def _clear_openclaw_env(monkeypatch: pytest.MonkeyPatch) -> None:
  19. for name in [
  20. "APP_TOKEN",
  21. "OPENCLAW_DEFAULT_MODEL",
  22. "OPENCLAW_GATEWAY_TOKEN",
  23. "OPENCLAW_PROVIDER_MODEL",
  24. "OPENCLAW_PROXY_API_KEY",
  25. "OPENCLAW_PROXY_BASE_URL",
  26. "OPENCLAW_PROXY_HOST",
  27. "OPENCLAW_PROXY_PORT",
  28. "PORT",
  29. ]:
  30. monkeypatch.delenv(name, raising=False)
  31. def test_reset_openclaw_defaults_drains_stale_app_finalizers(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
  32. _clear_openclaw_env(monkeypatch)
  33. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:8000/openclaw/v1")
  34. stale_app = FastAPI()
  35. attach_openclaw_to_fastapi(
  36. stale_app,
  37. replace(
  38. _build_openclaw_config(tmp_path / "stale"),
  39. default_model="openclaw:stale",
  40. provider_model="openai/gpt-4o",
  41. ),
  42. )
  43. stale_app = None
  44. reset_openclaw_current_app_defaults(monkeypatch)
  45. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:8000/openclaw/v1")
  46. first_app = FastAPI()
  47. attach_openclaw_to_fastapi(
  48. first_app,
  49. replace(
  50. _build_openclaw_config(tmp_path / "first"),
  51. default_model="openclaw:first",
  52. provider_model="openai/gpt-4o",
  53. ),
  54. )
  55. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:9000/openclaw/v1")
  56. second_app = FastAPI()
  57. attach_openclaw_to_fastapi(
  58. second_app,
  59. replace(
  60. _build_openclaw_config(tmp_path / "second"),
  61. port=9000,
  62. default_model="openclaw:second",
  63. provider_model="openai/gpt-5.4-mini",
  64. ),
  65. )
  66. gc.collect()
  67. first_model = build_openclaw_responses_model(base_url="http://127.0.0.1:8000/openclaw/v1", api_key="first-token")
  68. second_model = build_openclaw_responses_model(
  69. base_url="http://127.0.0.1:9000/openclaw/v1",
  70. api_key="second-token",
  71. )
  72. assert first_model.model == "openclaw:first"
  73. assert second_model.model == "openclaw:second"
  74. def test_build_openclaw_model_uses_current_app_proxy_defaults(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
  75. _clear_openclaw_env(monkeypatch)
  76. monkeypatch.setenv("OPENCLAW_PROXY_PORT", "9000")
  77. monkeypatch.setenv("APP_TOKEN", "app-token")
  78. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  79. monkeypatch.setenv("OPENCLAW_GATEWAY_TOKEN", "gateway-token")
  80. config = replace(
  81. _build_openclaw_config(tmp_path),
  82. port=9000,
  83. default_model="openclaw:custom",
  84. provider_model="anthropic/claude-sonnet-4-5",
  85. )
  86. attach_openclaw_to_fastapi(FastAPI(), config)
  87. model = build_openclaw_responses_model(base_url="http://127.0.0.1:9000/openclaw/v1")
  88. assert model.model == "openclaw:custom"
  89. assert get_usage_tracking_model_name(model) == "anthropic/claude-sonnet-4-5"
  90. assert model._client.api_key == "app-token"
  91. @pytest.mark.parametrize(
  92. ("server_url", "base_url"),
  93. [
  94. ("http://localhost:9000", "http://127.0.0.1:9000/openclaw/v1"),
  95. ("https://example.com/{stage}", "https://example.com/prod/openclaw/v1"),
  96. ],
  97. )
  98. def test_attach_openclaw_uses_app_server_urls_for_current_app_defaults(
  99. monkeypatch: pytest.MonkeyPatch,
  100. tmp_path: Path,
  101. server_url: str,
  102. base_url: str,
  103. ) -> None:
  104. _clear_openclaw_env(monkeypatch)
  105. monkeypatch.setenv("APP_TOKEN", "app-token")
  106. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  107. monkeypatch.setenv("OPENCLAW_GATEWAY_TOKEN", "gateway-token")
  108. app = FastAPI(servers=[{"url": server_url}])
  109. attach_openclaw_to_fastapi(
  110. app,
  111. replace(
  112. _build_openclaw_config(tmp_path),
  113. default_model="openclaw:custom",
  114. provider_model="anthropic/claude-sonnet-4-5",
  115. ),
  116. )
  117. model = build_openclaw_responses_model(base_url=base_url)
  118. assert model.model == "openclaw:custom"
  119. assert get_usage_tracking_model_name(model) == "anthropic/claude-sonnet-4-5"
  120. assert model._client.api_key == "app-token"
  121. def test_attach_openclaw_ignores_relative_server_urls_for_current_app_defaults(
  122. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  123. ) -> None:
  124. _clear_openclaw_env(monkeypatch)
  125. monkeypatch.setenv("APP_TOKEN", "app-token")
  126. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  127. app = FastAPI(servers=[{"url": "/api"}])
  128. attach_openclaw_to_fastapi(app, _build_openclaw_config(tmp_path))
  129. model = build_openclaw_responses_model(base_url="https://example.com/api/openclaw/v1")
  130. assert model.model == "openclaw:main"
  131. assert get_usage_tracking_model_name(model) == "openclaw:main"
  132. assert model._client.api_key == "proxy-token"
  133. def test_attach_openclaw_rejects_conflicting_current_app_defaults(
  134. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  135. ) -> None:
  136. _clear_openclaw_env(monkeypatch)
  137. monkeypatch.setenv("OPENCLAW_PROXY_PORT", "8000")
  138. attach_openclaw_to_fastapi(
  139. FastAPI(),
  140. replace(
  141. _build_openclaw_config(tmp_path / "first"),
  142. default_model="openclaw:first",
  143. provider_model="openai/gpt-4o",
  144. ),
  145. )
  146. with pytest.raises(ValueError, match="Conflicting current-app OpenClaw defaults"):
  147. attach_openclaw_to_fastapi(
  148. FastAPI(),
  149. replace(
  150. _build_openclaw_config(tmp_path / "second"),
  151. default_model="openclaw:second",
  152. provider_model="openai/gpt-5.4-mini",
  153. ),
  154. )
  155. def test_attach_openclaw_rolls_back_partial_registration_failures(
  156. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  157. ) -> None:
  158. _clear_openclaw_env(monkeypatch)
  159. monkeypatch.setenv("OPENCLAW_PROXY_PORT", "9000")
  160. conflict_url = "https://conflict.example/openclaw/v1"
  161. openclaw_mod.openclaw_model.register_current_app_openclaw_defaults(
  162. default_model="openclaw:existing",
  163. provider_model="openai/gpt-4o",
  164. base_url=conflict_url,
  165. )
  166. with pytest.raises(ValueError, match="Conflicting"):
  167. attach_openclaw_to_fastapi(
  168. FastAPI(servers=[{"url": "https://conflict.example"}]),
  169. replace(
  170. _build_openclaw_config(tmp_path),
  171. default_model="openclaw:new",
  172. provider_model="openai/gpt-5.4-mini",
  173. ),
  174. )
  175. default_url_key = openclaw_mod.openclaw_model._normalize_openclaw_proxy_url("http://127.0.0.1:9000/openclaw/v1")
  176. assert default_url_key not in openclaw_mod.openclaw_model._CURRENT_APP_OPENCLAW_DEFAULTS
  177. assert default_url_key not in openclaw_mod.openclaw_model._CURRENT_APP_OPENCLAW_DEFAULT_COUNTS
  178. def test_attach_openclaw_releases_defaults_when_app_is_garbage_collected(
  179. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  180. ) -> None:
  181. _clear_openclaw_env(monkeypatch)
  182. monkeypatch.setenv("OPENCLAW_PROXY_PORT", "8000")
  183. attach_openclaw_to_fastapi(
  184. FastAPI(),
  185. replace(
  186. _build_openclaw_config(tmp_path / "first"),
  187. default_model="openclaw:first",
  188. provider_model="openai/gpt-4o",
  189. ),
  190. )
  191. gc.collect()
  192. attach_openclaw_to_fastapi(
  193. FastAPI(),
  194. replace(
  195. _build_openclaw_config(tmp_path / "second"),
  196. default_model="openclaw:second",
  197. provider_model="openai/gpt-5.4-mini",
  198. ),
  199. )
  200. def test_attach_openclaw_keeps_distinct_proxy_defaults_separate(
  201. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  202. ) -> None:
  203. _clear_openclaw_env(monkeypatch)
  204. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:8000/openclaw/v1")
  205. first_app = FastAPI()
  206. second_app = FastAPI()
  207. attach_openclaw_to_fastapi(
  208. first_app,
  209. replace(
  210. _build_openclaw_config(tmp_path / "first"),
  211. default_model="openclaw:first",
  212. provider_model="openai/gpt-4o",
  213. ),
  214. )
  215. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "http://127.0.0.1:9000/openclaw/v1")
  216. attach_openclaw_to_fastapi(
  217. second_app,
  218. replace(
  219. _build_openclaw_config(tmp_path / "second"),
  220. port=9000,
  221. default_model="openclaw:second",
  222. provider_model="openai/gpt-5.4-mini",
  223. ),
  224. )
  225. first_model = build_openclaw_responses_model(base_url="http://127.0.0.1:8000/openclaw/v1", api_key="first-token")
  226. second_model = build_openclaw_responses_model(
  227. base_url="http://127.0.0.1:9000/openclaw/v1",
  228. api_key="second-token",
  229. )
  230. assert first_model.model == "openclaw:first"
  231. assert second_model.model == "openclaw:second"
  232. def test_attach_openclaw_uses_public_proxy_defaults_for_exact_current_app_url(
  233. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  234. ) -> None:
  235. _clear_openclaw_env(monkeypatch)
  236. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "https://worker.example/openclaw/v1")
  237. monkeypatch.setenv("APP_TOKEN", "app-token")
  238. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  239. attach_openclaw_to_fastapi(
  240. FastAPI(),
  241. replace(
  242. _build_openclaw_config(tmp_path),
  243. default_model="openclaw:custom",
  244. provider_model="openai/gpt-5.4-mini",
  245. ),
  246. )
  247. model = build_openclaw_responses_model(base_url="https://worker.example/openclaw/v1")
  248. assert model.model == "openclaw:custom"
  249. assert get_usage_tracking_model_name(model) == "openai/gpt-5.4-mini"
  250. assert model._client.api_key == "app-token"
  251. def test_attach_openclaw_does_not_reuse_public_proxy_defaults_for_other_urls(
  252. monkeypatch: pytest.MonkeyPatch, tmp_path: Path
  253. ) -> None:
  254. _clear_openclaw_env(monkeypatch)
  255. monkeypatch.setenv("OPENCLAW_PROXY_BASE_URL", "https://external.example/openclaw/v1")
  256. monkeypatch.setenv("OPENCLAW_PROVIDER_MODEL", "openai/gpt-5.4-mini")
  257. monkeypatch.setenv("APP_TOKEN", "app-token")
  258. monkeypatch.setenv("OPENCLAW_PROXY_API_KEY", "proxy-token")
  259. attach_openclaw_to_fastapi(
  260. FastAPI(),
  261. replace(
  262. _build_openclaw_config(tmp_path),
  263. default_model="openclaw:custom",
  264. provider_model="openai/gpt-4o",
  265. ),
  266. )
  267. model = build_openclaw_responses_model(base_url="https://other.example/openclaw/v1")
  268. assert model.model == "openclaw:main"
  269. assert get_usage_tracking_model_name(model) == "openclaw:main"
  270. assert model._client.api_key == "proxy-token"