From 2aac29697d46fd390dc4665582bd742030893de1 Mon Sep 17 00:00:00 2001 From: Pierre LE GUEN <26087574+PierreLeGuen@users.noreply.github.com> Date: Thu, 21 May 2026 12:50:25 +0000 Subject: [PATCH] feat(llm): add NEAR AI Cloud provider --- .env.example | 9 ++ README.md | 3 +- SETUP.md | 4 + config/providers.yaml | 17 +++ docs/getting-started/configuration.md | 3 + docs/getting-started/installation.md | 2 + docs/reference/architecture.md | 2 + install.js | 8 ++ profiles/schema.json | 2 +- providers/llm/__init__.py | 1 + providers/llm/nearai_provider.py | 142 ++++++++++++++++++++++++++ routes/admin.py | 17 +++ setup-config.js | 26 +++++ src/admin.html | 1 + tests/test_llm_providers.py | 90 ++++++++++++++++ tests/test_registry.py | 1 + tests/test_routes_admin.py | 22 ++++ 17 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 providers/llm/nearai_provider.py diff --git a/.env.example b/.env.example index 82bda79..7d9a028 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,15 @@ CLAWDBOT_AUTH_TOKEN=your-openclaw-gateway-token # Session key prefix — change if running multiple instances on the same gateway GATEWAY_SESSION_KEY=voice-main-1 +# ============================================================================= +# LLM provider keys (optional — configured through OpenClaw / admin model config) +# ============================================================================= + +# NEAR AI Cloud (OpenAI-compatible TEE inference) +# Get key: https://cloud.near.ai +# NEARAI_API_KEY=your-nearai-api-key +# NEARAI_BASE_URL=https://cloud-api.near.ai/v1 + # ============================================================================= # TTS — choose one or more providers # ============================================================================= diff --git a/README.md b/README.md index 4c4f6b5..a7729ab 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ See [`deploy/`](deploy/) for the full production setup including SSL, nginx reve All configuration is in `.env`. Copy `.env.example` to `.env` and fill in your values. **Required:** -- An LLM provider API key (OpenAI, Anthropic, Groq, Z.AI, or any OpenClaw-compatible provider) +- An LLM provider API key (OpenAI, Anthropic, Groq, Z.AI, NEAR AI Cloud, or any OpenClaw-compatible provider) - `CLAWDBOT_AUTH_TOKEN` — set during `npx openvoiceui setup` or in OpenClaw's setup wizard **Optional but recommended:** @@ -177,6 +177,7 @@ See [`.env.example`](.env.example) for all available options with descriptions. |----------|--------| | OpenClaw Gateway | Built-in — routes to OpenAI, Anthropic, Groq, Z.AI, and more | | Z.AI (GLM-5-turbo) | Built-in | +| NEAR AI Cloud | OpenAI-compatible TEE inference via OpenClaw | | Groq (Llama, Qwen) | Via OpenClaw | | Google Gemini | Via OpenClaw | | MiniMax | Via OpenClaw | diff --git a/SETUP.md b/SETUP.md index 1d16b05..e8a865d 100644 --- a/SETUP.md +++ b/SETUP.md @@ -93,6 +93,8 @@ GROQ_API_KEY=your-groq-key SECRET_KEY=any-random-string-here ``` +Optional: set `NEARAI_API_KEY=your-nearai-key` to enable NEAR AI Cloud TEE inference. + **Optional: enable the coding-agent skill** The coding-agent skill lets the AI write code, create files, and run commands @@ -160,6 +162,8 @@ Point openvoiceui at your running OpenClaw gateway instead of starting a new one SECRET_KEY=any-random-string-here ``` + Optional: set `NEARAI_API_KEY=your-nearai-key` to enable NEAR AI Cloud TEE inference. + 4. Start only the openvoiceui and supertonic services (skip the built-in openclaw): ```bash docker compose up --build openvoiceui supertonic diff --git a/config/providers.yaml b/config/providers.yaml index f448c00..7e39480 100644 --- a/config/providers.yaml +++ b/config/providers.yaml @@ -11,6 +11,7 @@ llm: fallback_provider: clawdbot modules: - providers.llm.zai_provider + - providers.llm.nearai_provider - providers.llm.clawdbot_provider providers: @@ -25,6 +26,22 @@ llm: - id: glm-4-plus name: "GLM-4 Plus" + nearai: + name: "NEAR AI Cloud" + api_key: ${NEARAI_API_KEY} + base_url: https://cloud-api.near.ai/v1 + default_model: zai-org/GLM-5.1-FP8 + priority: 15 + models: + - id: zai-org/GLM-5.1-FP8 + name: "GLM 5.1" + - id: Qwen/Qwen3.6-35B-A3B-FP8 + name: "Qwen 3.6 35B A3B FP8" + - id: Qwen/Qwen3.5-122B-A10B + name: "Qwen3.5 122B A10B" + - id: Qwen/Qwen3-VL-30B-A3B-Instruct + name: "Qwen3-VL 30B A3B Instruct" + clawdbot: name: "Clawdbot Gateway" gateway_url: ${CLAWDBOT_GATEWAY_URL} diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index db9edc5..5989e85 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -25,6 +25,7 @@ OpenVoiceUI needs at least one LLM provider to power conversations. The AI gatew |----------|---------|-------------------| | Groq (Llama, Qwen) | `GROQ_API_KEY` | [console.groq.com](https://console.groq.com) | | Z.AI (GLM-5-turbo) | Set via OpenClaw config | [z.ai](https://z.ai) | +| NEAR AI Cloud (TEE inference) | `NEARAI_API_KEY` | [cloud.near.ai](https://cloud.near.ai) | | OpenAI (GPT-4o) | `OPENAI_API_KEY` | [platform.openai.com](https://platform.openai.com) | | Anthropic (Claude) | `ANTHROPIC_API_KEY` | [console.anthropic.com](https://console.anthropic.com) | @@ -106,6 +107,8 @@ Complete list of all recognized environment variables: | `GATEWAY_SESSION_KEY` | No | `voice-main-1` | Session key prefix. Change if running multiple instances on the same gateway. | | `OPENCLAW_VERSION` | No | `2026.3.13` | Docker build arg: OpenClaw version to install. Only change if you have verified compatibility. | | `CANVAS_PAGES_DIR` | No | `runtime/canvas-pages/` | Where canvas HTML pages are stored. Docker: leave unset (uses volume mount). VPS: set to the path created by the deploy script. | +| `NEARAI_API_KEY` | No | -- | NEAR AI Cloud API key for OpenAI-compatible TEE inference. | +| `NEARAI_BASE_URL` | No | `https://cloud-api.near.ai/v1` | Override for the NEAR AI Cloud OpenAI-compatible endpoint. | | `GROQ_API_KEY` | Recommended | -- | Groq API key for Orpheus TTS and Whisper STT. | | `USE_GROQ` | No | -- | Set `true` to enable Groq integration. | | `USE_GROQ_TTS` | No | -- | Set `true` to enable Groq Orpheus TTS. | diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 74736ac..d53d7d8 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -85,6 +85,8 @@ GROQ_API_KEY=your-groq-api-key SECRET_KEY=any-random-string-here ``` +Optional: set `NEARAI_API_KEY=your-nearai-key` to enable NEAR AI Cloud TEE inference. + See [Configuration](/getting-started/configuration) for the full list of environment variables. ### 2. Optional: enable coding agent diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index c690f3c..d96e0b4 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -446,6 +446,8 @@ Set in `app.py` after_request handler. | Variable | Default | Purpose | |----------|---------|---------| | `CLAWDBOT_GATEWAY_URL` | `ws://127.0.0.1:18791` | Gateway WebSocket URL | +| `NEARAI_API_KEY` | — | NEAR AI Cloud TEE inference | +| `NEARAI_BASE_URL` | `https://cloud-api.near.ai/v1` | Optional NEAR AI endpoint override | | `GROQ_API_KEY` | — | Groq Orpheus TTS | | `FAL_KEY` | — | Qwen3-TTS via fal.ai | | `SUNO_API_KEY` | — | AI song generation | diff --git a/install.js b/install.js index 91a6f46..a374aa4 100644 --- a/install.js +++ b/install.js @@ -71,6 +71,13 @@ module.exports = { placeholder: "", required: false, }, + { + key: "NEARAI_API_KEY", + title: "[RECOMMENDED] NEAR AI Cloud API Key — TEE inference", + description: "cloud.near.ai — OpenAI-compatible private inference", + placeholder: "", + required: false, + }, { key: "ANTHROPIC_API_KEY", title: "[RECOMMENDED] Anthropic API Key — Claude models", @@ -314,6 +321,7 @@ module.exports = { PINOKIO_DEEPGRAM_API_KEY: "{{input.DEEPGRAM_API_KEY}}", PINOKIO_ANTHROPIC_API_KEY: "{{input.ANTHROPIC_API_KEY}}", PINOKIO_ZAI_API_KEY: "{{input.ZAI_API_KEY}}", + PINOKIO_NEARAI_API_KEY: "{{input.NEARAI_API_KEY}}", PINOKIO_OPENAI_API_KEY: "{{input.OPENAI_API_KEY}}", PINOKIO_GEMINI_API_KEY: "{{input.GEMINI_API_KEY}}", PINOKIO_OPENROUTER_API_KEY: "{{input.OPENROUTER_API_KEY}}", diff --git a/profiles/schema.json b/profiles/schema.json index c8be323..38d017c 100644 --- a/profiles/schema.json +++ b/profiles/schema.json @@ -43,7 +43,7 @@ "properties": { "provider": { "type": "string", - "enum": ["zai", "clawdbot", "gateway", "openai", "ollama", "anthropic", "hume", "elevenlabs"] + "enum": ["zai", "nearai", "clawdbot", "gateway", "openai", "ollama", "anthropic", "hume", "elevenlabs"] }, "model": { "type": "string" }, "config_id": { "type": "string" }, diff --git a/providers/llm/__init__.py b/providers/llm/__init__.py index ce5fba5..f3f5c48 100644 --- a/providers/llm/__init__.py +++ b/providers/llm/__init__.py @@ -8,5 +8,6 @@ # Import concrete providers so their registry.register() calls fire from providers.llm import zai_provider # noqa: F401 from providers.llm import clawdbot_provider # noqa: F401 +from providers.llm import nearai_provider # noqa: F401 __all__ = ["LLMProvider", "LLMResponse", "LLMError"] diff --git a/providers/llm/nearai_provider.py b/providers/llm/nearai_provider.py new file mode 100644 index 0000000..26310f1 --- /dev/null +++ b/providers/llm/nearai_provider.py @@ -0,0 +1,142 @@ +""" +NEAR AI Cloud LLM provider. + +OpenAI-compatible chat completions over NEAR AI Cloud TEE inference. +""" + +import os +import time +from typing import Any, Dict, Iterator, List, Optional + +from providers.llm.base import LLMError, LLMProvider, LLMResponse +from providers.registry import ProviderType, registry + + +class NearAIProvider(LLMProvider): + """NEAR AI Cloud provider via OpenAI-compatible REST API.""" + + DEFAULT_BASE_URL = "https://cloud-api.near.ai/v1" + DEFAULT_MODEL = "zai-org/GLM-5.1-FP8" + + def __init__(self, config: Dict[str, Any] = None) -> None: + super().__init__(config) + self.api_key = self._resolve_api_key() + self.base_url = self._resolve_base_url() + self.default_model = self._config.get("default_model", self.DEFAULT_MODEL) + + def _resolve_api_key(self) -> str: + key = self._config.get("api_key", "") + if key and not key.startswith("${"): + return key + return os.getenv("NEARAI_API_KEY", "") + + def _resolve_base_url(self) -> str: + base_url = ( + self._config.get("base_url") + or self._config.get("baseUrl") + or os.getenv("NEARAI_BASE_URL", "") + or self.DEFAULT_BASE_URL + ) + return base_url.rstrip("/") + + def _chat_url(self) -> str: + return f"{self.base_url}/chat/completions" + + def _build_messages( + self, + messages: List[Dict[str, str]], + system_prompt: Optional[str], + ) -> List[Dict[str, str]]: + full_messages: List[Dict[str, str]] = [] + if system_prompt: + full_messages.append({"role": "system", "content": system_prompt}) + + for message in messages: + role = message.get("role", "user") + if role == "developer": + role = "system" + full_messages.append({ + "role": role, + "content": message.get("content", ""), + }) + return full_messages + + def generate( + self, + messages: List[Dict[str, str]], + system_prompt: Optional[str] = None, + model: Optional[str] = None, + **kwargs, + ) -> LLMResponse: + try: + import requests # type: ignore + except ImportError: + raise LLMError("nearai", "requests library not installed") + + model = model or self.default_model + max_tokens = kwargs.get("max_tokens", kwargs.get("max_completion_tokens", 512)) + timeout = kwargs.get("timeout", 30) + + payload: Dict[str, Any] = { + "model": model, + "messages": self._build_messages(messages, system_prompt), + "max_tokens": max_tokens, + } + + if "temperature" in kwargs: + payload["temperature"] = kwargs["temperature"] + if "tools" in kwargs: + payload["tools"] = kwargs["tools"] + if "tool_choice" in kwargs: + payload["tool_choice"] = kwargs["tool_choice"] + + start = time.time() + try: + resp = requests.post( + self._chat_url(), + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=timeout, + ) + resp.raise_for_status() + except Exception as exc: + raise LLMError("nearai", f"API request failed: {exc}") from exc + + data = resp.json() + latency_ms = (time.time() - start) * 1000 + choice = data["choices"][0] + + return LLMResponse( + content=choice["message"]["content"], + model=model, + provider="nearai", + usage=data.get("usage", {}), + latency_ms=latency_ms, + finish_reason=choice.get("finish_reason", "stop"), + raw_response=data, + ) + + def generate_stream( + self, + messages: List[Dict[str, str]], + system_prompt: Optional[str] = None, + model: Optional[str] = None, + **kwargs, + ) -> Iterator[str]: + response = self.generate(messages, system_prompt, model, **kwargs) + yield response.content + + def is_available(self) -> bool: + return bool(self.api_key) + + def get_info(self) -> Dict[str, Any]: + info = super().get_info() + info["name"] = self._config.get("name", "NEAR AI Cloud") + info["base_url"] = self.base_url + return info + + +registry.register(ProviderType.LLM, "nearai", NearAIProvider) diff --git a/routes/admin.py b/routes/admin.py index 69013b4..5d34690 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -601,6 +601,16 @@ def list_clients(): _OPENCLAW_CONFIG_PATH = Path('/app/runtime/openclaw.json') +# Static admin list sampled from GET https://cloud-api.near.ai/v1/model/list on 2026-05-21. +_NEARAI_MODELS = [ + {'id': 'zai-org/GLM-5.1-FP8', 'name': 'GLM 5.1', 'contextWindow': 202752}, + {'id': 'Qwen/Qwen3.6-35B-A3B-FP8', 'name': 'Qwen 3.6 35B A3B FP8', 'contextWindow': 262144}, + {'id': 'Qwen/Qwen3.5-122B-A10B', 'name': 'Qwen3.5 122B A10B', 'contextWindow': 131072}, + {'id': 'Qwen/Qwen3-VL-30B-A3B-Instruct', 'name': 'Qwen3-VL 30B A3B Instruct', 'contextWindow': 256000}, + {'id': 'google/gemma-4-31B-it', 'name': 'Gemma 4 31B Instruct', 'contextWindow': 262144}, + {'id': 'openai/gpt-oss-120b', 'name': 'GPT OSS 120B', 'contextWindow': 131000}, +] + # Known providers and their config shape _AI_PROVIDERS = { 'mx': { @@ -664,6 +674,13 @@ def list_clients(): {'id': 'llama-4-maverick-17b-128e-instruct', 'name': 'Llama 4 Maverick', 'contextWindow': 131072}, ], }, + 'nearai': { + 'name': 'NEAR AI Cloud', + 'envKey': 'NEARAI_API_KEY', + 'baseUrl': 'https://cloud-api.near.ai/v1', + 'api': 'openai-completions', + 'models': _NEARAI_MODELS, + }, 'google': { 'name': 'Google Gemini', 'envKey': 'GEMINI_API_KEY', diff --git a/setup-config.js b/setup-config.js index 0fa1ad8..580ecfb 100644 --- a/setup-config.js +++ b/setup-config.js @@ -22,6 +22,16 @@ const PORT = getKey("PORT") || "5001"; const token = crypto.randomBytes(24).toString("hex"); const secret = crypto.randomBytes(32).toString("hex"); +// Sampled from GET https://cloud-api.near.ai/v1/model/list on 2026-05-21. +const NEARAI_MODELS = [ + { id: "zai-org/GLM-5.1-FP8", name: "GLM 5.1", contextWindow: 202752 }, + { id: "Qwen/Qwen3.6-35B-A3B-FP8", name: "Qwen 3.6 35B A3B FP8", contextWindow: 262144 }, + { id: "Qwen/Qwen3.5-122B-A10B", name: "Qwen3.5 122B A10B", contextWindow: 131072 }, + { id: "Qwen/Qwen3-VL-30B-A3B-Instruct", name: "Qwen3-VL 30B A3B Instruct", contextWindow: 256000 }, + { id: "google/gemma-4-31B-it", name: "Gemma 4 31B Instruct", contextWindow: 262144 }, + { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", contextWindow: 131000 }, +]; + // --------------------------------------------------------------------------- // 1. Write openclaw.json (nested gateway/agents format for v2026.3.2+) // --------------------------------------------------------------------------- @@ -67,6 +77,20 @@ const openclawConfig = { // Plugins configured by individual plugin installers }; +if (getKey("NEARAI_API_KEY")) { + openclawConfig.models = { + mode: "merge", + providers: { + nearai: { + baseUrl: "https://cloud-api.near.ai/v1", + api: "openai-completions", + apiKey: "${NEARAI_API_KEY}", + models: NEARAI_MODELS, + }, + }, + }; +} + fs.mkdirSync("openclaw-data/workspace", { recursive: true }); fs.writeFileSync( "openclaw-data/openclaw.json", @@ -95,6 +119,7 @@ const envLines = [ const envKeyList = [ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "OPENROUTER_API_KEY", "MISTRAL_API_KEY", "XAI_API_KEY", "ZAI_API_KEY", + "NEARAI_API_KEY", "CEREBRAS_API_KEY", "TOGETHER_API_KEY", "HF_TOKEN", "MOONSHOT_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", "QIANFAN_API_KEY", "MODELSTUDIO_API_KEY", "XIAOMI_API_KEY", "VOLCANO_ENGINE_API_KEY", @@ -135,6 +160,7 @@ const providerMap = { MISTRAL_API_KEY: "mistral", XAI_API_KEY: "xai", ZAI_API_KEY: "zai", + NEARAI_API_KEY: "nearai", CEREBRAS_API_KEY: "cerebras", TOGETHER_API_KEY: "together", HF_TOKEN: "huggingface", diff --git a/src/admin.html b/src/admin.html index 9c1e905..dbfa983 100644 --- a/src/admin.html +++ b/src/admin.html @@ -1756,6 +1756,7 @@ llm:[ {id:'gateway',name:'OpenClaw Gateway',detail:'GLM-5-turbo via OpenClaw. Full tools, memory, canvas.'}, {id:'zai',name:'Z.AI Direct',detail:'GLM-5-turbo / GLM-5.1. No agent tools.'}, + {id:'nearai',name:'NEAR AI Cloud',detail:'TEE inference via OpenClaw. OpenAI-compatible.'}, {id:'groq',name:'Groq',detail:'Llama 4 Scout / Qwen3-32b. Fast inference.'}, {id:'minimax',name:'MiniMax',detail:'M2.7-highspeed. Custom provider "mx".'}, {id:'kimi',name:'Kimi',detail:'K2.5 / Moonshot AI.'}, diff --git a/tests/test_llm_providers.py b/tests/test_llm_providers.py index fc4c472..3ce423a 100644 --- a/tests/test_llm_providers.py +++ b/tests/test_llm_providers.py @@ -153,3 +153,93 @@ def test_generate_stream_yields_string(self): with patch.object(p, "generate", return_value=mock_response): chunks = list(p.generate_stream([{"role": "user", "content": "hi"}])) assert chunks == ["test response"] + + +# --------------------------------------------------------------------------- +# NearAIProvider +# --------------------------------------------------------------------------- + +class TestNearAIProvider: + def test_instantiate_default(self): + from providers.llm.nearai_provider import NearAIProvider + p = NearAIProvider() + assert p is not None + + def test_default_model(self): + from providers.llm.nearai_provider import NearAIProvider + p = NearAIProvider() + assert p.default_model == "zai-org/GLM-5.1-FP8" + + def test_base_url_default(self, monkeypatch): + from providers.llm.nearai_provider import NearAIProvider + monkeypatch.delenv("NEARAI_BASE_URL", raising=False) + p = NearAIProvider() + assert p.base_url == "https://cloud-api.near.ai/v1" + + def test_is_available_without_key(self, monkeypatch): + from providers.llm.nearai_provider import NearAIProvider + monkeypatch.delenv("NEARAI_API_KEY", raising=False) + p = NearAIProvider({"api_key": ""}) + assert p.is_available() is False + + def test_is_available_with_key(self): + from providers.llm.nearai_provider import NearAIProvider + p = NearAIProvider({"api_key": "test-key"}) + assert p.is_available() is True + + def test_resolve_api_key_ignores_placeholder(self, monkeypatch): + from providers.llm.nearai_provider import NearAIProvider + monkeypatch.setenv("NEARAI_API_KEY", "env-key") + p = NearAIProvider({"api_key": "${NEARAI_API_KEY}"}) + assert p.api_key == "env-key" + + def test_generate_maps_max_completion_tokens_to_max_tokens(self): + from providers.llm.nearai_provider import NearAIProvider + + p = NearAIProvider({"api_key": "key"}) + mock_resp = MagicMock() + mock_resp.json.return_value = { + "choices": [ + {"message": {"content": "hello"}, "finish_reason": "stop"} + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 1}, + } + mock_resp.raise_for_status.return_value = None + + with patch("requests.post", return_value=mock_resp) as post: + response = p.generate( + [{"role": "developer", "content": "be concise"}, {"role": "user", "content": "hi"}], + max_completion_tokens=123, + store=True, + reasoning_effort="high", + ) + + payload = post.call_args.kwargs["json"] + assert payload["max_tokens"] == 123 + assert "max_completion_tokens" not in payload + assert "store" not in payload + assert "reasoning_effort" not in payload + assert payload["messages"][0]["role"] == "system" + assert response.content == "hello" + + def test_generate_raises_on_api_failure(self): + from providers.llm.nearai_provider import NearAIProvider + from providers.llm.base import LLMError + p = NearAIProvider({"api_key": "bad-key"}) + mock_resp = MagicMock() + mock_resp.raise_for_status.side_effect = Exception("401 Unauthorized") + with patch("requests.post", return_value=mock_resp): + with pytest.raises(LLMError): + p.generate([{"role": "user", "content": "hello"}]) + + def test_generate_stream_yields_string(self): + from providers.llm.nearai_provider import NearAIProvider + from providers.llm.base import LLMResponse + p = NearAIProvider({"api_key": "key"}) + mock_response = LLMResponse( + content="test response", model="zai-org/GLM-5.1-FP8", provider="nearai", + usage={}, latency_ms=10 + ) + with patch.object(p, "generate", return_value=mock_response): + chunks = list(p.generate_stream([{"role": "user", "content": "hi"}])) + assert chunks == ["test response"] diff --git a/tests/test_registry.py b/tests/test_registry.py index 0846861..7e5e941 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -194,6 +194,7 @@ def test_concrete_providers_registered(): from providers.registry import registry as r, ProviderType as PT assert r.is_registered(PT.LLM, "zai") + assert r.is_registered(PT.LLM, "nearai") assert r.is_registered(PT.LLM, "clawdbot") assert r.is_registered(PT.TTS, "supertonic") assert r.is_registered(PT.TTS, "groq") diff --git a/tests/test_routes_admin.py b/tests/test_routes_admin.py index 0a713fc..8d807d0 100644 --- a/tests/test_routes_admin.py +++ b/tests/test_routes_admin.py @@ -156,3 +156,25 @@ def test_server_stats_has_timestamp(self, admin_client): resp = admin_client.get("/api/server-stats") data = resp.get_json() assert "timestamp" in data + + +# --------------------------------------------------------------------------- +# AI provider catalog +# --------------------------------------------------------------------------- + +class TestAIProviderCatalog: + def test_nearai_provider_registered(self): + from routes.admin import _AI_PROVIDERS + + provider = _AI_PROVIDERS["nearai"] + assert provider["name"] == "NEAR AI Cloud" + assert provider["envKey"] == "NEARAI_API_KEY" + assert provider["baseUrl"] == "https://cloud-api.near.ai/v1" + assert provider["api"] == "openai-completions" + + def test_nearai_models_have_context_windows(self): + from routes.admin import _AI_PROVIDERS + + models = _AI_PROVIDERS["nearai"]["models"] + assert any(model["id"] == "zai-org/GLM-5.1-FP8" for model in models) + assert all(model["contextWindow"] > 0 for model in models)