Compare commits
1 Commits
fix/479-ha
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90162bafc |
@@ -32,6 +32,27 @@ _PROVIDER_PREFIXES: frozenset[str] = frozenset({
|
|||||||
"glm", "z-ai", "z.ai", "zhipu", "github", "github-copilot",
|
"glm", "z-ai", "z.ai", "zhipu", "github", "github-copilot",
|
||||||
"github-models", "kimi", "moonshot", "claude", "deep-seek",
|
"github-models", "kimi", "moonshot", "claude", "deep-seek",
|
||||||
"opencode", "zen", "go", "vercel", "kilo", "dashscope", "aliyun", "qwen",
|
"opencode", "zen", "go", "vercel", "kilo", "dashscope", "aliyun", "qwen",
|
||||||
|
# Additional cloud vendor prefixes (fixes #628)
|
||||||
|
"cohere", "mistralai", "mistral", "meta-llama", "databricks", "together",
|
||||||
|
"togetherai", "together-ai", "nousresearch", "moonshotai", "fireworks",
|
||||||
|
"perplexity", "ai21", "groq", "cerebras", "nebius",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Vendor prefixes that appear in cloud model IDs (e.g. "openai/gpt-4").
|
||||||
|
# Used by _classify_runtime to detect cloud runtimes from the model name
|
||||||
|
# when no base URL is available.
|
||||||
|
_CLOUD_MODEL_PREFIXES: frozenset[str] = frozenset({
|
||||||
|
# Providers present before #628
|
||||||
|
"nous", "nousresearch", "openrouter", "anthropic", "openai",
|
||||||
|
"zai", "kimi", "moonshotai", "gemini", "google", "minimax",
|
||||||
|
# Providers added by #628 fix
|
||||||
|
"deepseek", "cohere", "mistralai", "mistral", "meta-llama",
|
||||||
|
"databricks", "together", "togetherai",
|
||||||
|
# Other common cloud vendors
|
||||||
|
"microsoft", "amazon", "huggingface", "fireworks",
|
||||||
|
"perplexity", "ai21", "groq", "cerebras", "nebius",
|
||||||
|
"qwen", "alibaba", "aliyuncs", "dashscope",
|
||||||
|
"github", "copilot",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -253,6 +274,67 @@ def is_local_endpoint(base_url: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Provider names that are definitively local (never cloud).
|
||||||
|
_LOCAL_PROVIDER_NAMES: frozenset[str] = frozenset({
|
||||||
|
"ollama", "custom", "local",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Provider names that are definitively cloud (not local).
|
||||||
|
_CLOUD_PROVIDER_NAMES: frozenset[str] = frozenset({
|
||||||
|
"nous", "openrouter", "anthropic", "openai", "openai-codex",
|
||||||
|
"zai", "kimi-coding", "gemini", "minimax", "minimax-cn",
|
||||||
|
"deepseek", "cohere", "mistral", "meta-llama", "databricks", "together",
|
||||||
|
"huggingface", "copilot", "copilot-acp", "ai-gateway", "kilocode",
|
||||||
|
"alibaba", "opencode-zen", "opencode-go",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_runtime(
|
||||||
|
model: str = "",
|
||||||
|
base_url: str = "",
|
||||||
|
provider: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Classify a model/endpoint runtime as 'cloud' or 'local'.
|
||||||
|
|
||||||
|
Checks in priority order:
|
||||||
|
1. ``base_url`` — localhost / RFC-1918 → ``"local"``; known external URL → ``"cloud"``
|
||||||
|
2. ``provider`` name — matches a known local or cloud provider set
|
||||||
|
3. Model vendor prefix — e.g. ``"openai/gpt-4"`` → ``"cloud"``
|
||||||
|
4. Default — ``"cloud"`` when the runtime cannot be determined to be local
|
||||||
|
|
||||||
|
The cloud-prefix list covers both the providers present before issue #628
|
||||||
|
(nous, openrouter, anthropic, openai, zai, kimi, gemini, minimax) and the
|
||||||
|
previously missing ones (deepseek, cohere, mistral, meta-llama, databricks,
|
||||||
|
together).
|
||||||
|
|
||||||
|
Returns ``"cloud"`` or ``"local"``.
|
||||||
|
"""
|
||||||
|
# 1. URL-based check — most reliable signal
|
||||||
|
if base_url:
|
||||||
|
if is_local_endpoint(base_url):
|
||||||
|
return "local"
|
||||||
|
return "cloud"
|
||||||
|
|
||||||
|
# 2. Provider name check
|
||||||
|
provider_norm = (provider or "").strip().lower()
|
||||||
|
if provider_norm in _LOCAL_PROVIDER_NAMES:
|
||||||
|
return "local"
|
||||||
|
if provider_norm in _CLOUD_PROVIDER_NAMES:
|
||||||
|
return "cloud"
|
||||||
|
|
||||||
|
# 3. Model vendor prefix check (e.g. "openai/gpt-4" → vendor "openai")
|
||||||
|
model_norm = (model or "").strip().lower()
|
||||||
|
if "/" in model_norm:
|
||||||
|
vendor = model_norm.split("/")[0].strip()
|
||||||
|
if vendor in _CLOUD_MODEL_PREFIXES:
|
||||||
|
return "cloud"
|
||||||
|
# An unknown vendor with a slash is still likely a cloud model
|
||||||
|
return "cloud"
|
||||||
|
|
||||||
|
# 4. Default — without a URL we cannot confirm local, so assume cloud
|
||||||
|
return "cloud"
|
||||||
|
|
||||||
|
|
||||||
def detect_local_server_type(base_url: str) -> Optional[str]:
|
def detect_local_server_type(base_url: str) -> Optional[str]:
|
||||||
"""Detect which local server is running at base_url by probing known endpoints.
|
"""Detect which local server is running at base_url by probing known endpoints.
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import uuid
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_HERMES_HOME = Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes")))
|
_HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
|
||||||
DATA_DIR = _HERMES_HOME / "skills" / "productivity" / "memento-flashcards" / "data"
|
DATA_DIR = _HERMES_HOME / "skills" / "productivity" / "memento-flashcards" / "data"
|
||||||
CARDS_FILE = DATA_DIR / "cards.json"
|
CARDS_FILE = DATA_DIR / "cards.json"
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class OwnedTwilioNumber:
|
|||||||
|
|
||||||
|
|
||||||
def _hermes_home() -> Path:
|
def _hermes_home() -> Path:
|
||||||
return Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes")))
|
return Path(os.environ.get("HERMES_HOME", "~/.hermes")).expanduser()
|
||||||
|
|
||||||
|
|
||||||
def _env_path() -> Path:
|
def _env_path() -> Path:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ terminal access.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from agent.model_metadata import is_local_endpoint
|
from agent.model_metadata import is_local_endpoint, _classify_runtime
|
||||||
|
|
||||||
|
|
||||||
class TestIsLocalEndpoint:
|
class TestIsLocalEndpoint:
|
||||||
@@ -71,3 +71,98 @@ class TestCronDisabledToolsetsLogic:
|
|||||||
def test_empty_url_disables_terminal(self):
|
def test_empty_url_disables_terminal(self):
|
||||||
disabled = self._build_disabled("")
|
disabled = self._build_disabled("")
|
||||||
assert "terminal" in disabled
|
assert "terminal" in disabled
|
||||||
|
|
||||||
|
|
||||||
|
class TestClassifyRuntime:
|
||||||
|
"""Verify _classify_runtime correctly classifies runtimes as cloud or local.
|
||||||
|
|
||||||
|
Covers the bug fixed in #628: missing cloud model prefixes for deepseek,
|
||||||
|
cohere, mistral, meta-llama, databricks, and together.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ── URL-based classification ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_localhost_url_is_local(self):
|
||||||
|
assert _classify_runtime(base_url="http://localhost:11434/v1") == "local"
|
||||||
|
|
||||||
|
def test_127_loopback_is_local(self):
|
||||||
|
assert _classify_runtime(base_url="http://127.0.0.1:8080/v1") == "local"
|
||||||
|
|
||||||
|
def test_rfc1918_is_local(self):
|
||||||
|
assert _classify_runtime(base_url="http://192.168.1.10:11434/v1") == "local"
|
||||||
|
|
||||||
|
def test_openrouter_url_is_cloud(self):
|
||||||
|
assert _classify_runtime(base_url="https://openrouter.ai/api/v1") == "cloud"
|
||||||
|
|
||||||
|
def test_anthropic_url_is_cloud(self):
|
||||||
|
assert _classify_runtime(base_url="https://api.anthropic.com") == "cloud"
|
||||||
|
|
||||||
|
def test_deepseek_url_is_cloud(self):
|
||||||
|
assert _classify_runtime(base_url="https://api.deepseek.com/v1") == "cloud"
|
||||||
|
|
||||||
|
# ── Provider-name classification ──────────────────────────────────────
|
||||||
|
|
||||||
|
def test_ollama_provider_is_local(self):
|
||||||
|
assert _classify_runtime(provider="ollama") == "local"
|
||||||
|
|
||||||
|
def test_custom_provider_is_local(self):
|
||||||
|
assert _classify_runtime(provider="custom") == "local"
|
||||||
|
|
||||||
|
def test_openrouter_provider_is_cloud(self):
|
||||||
|
assert _classify_runtime(provider="openrouter") == "cloud"
|
||||||
|
|
||||||
|
def test_nous_provider_is_cloud(self):
|
||||||
|
assert _classify_runtime(provider="nous") == "cloud"
|
||||||
|
|
||||||
|
def test_anthropic_provider_is_cloud(self):
|
||||||
|
assert _classify_runtime(provider="anthropic") == "cloud"
|
||||||
|
|
||||||
|
# ── Previously-missing cloud prefixes (issue #628) ────────────────────
|
||||||
|
|
||||||
|
def test_deepseek_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="deepseek/deepseek-v2") == "cloud"
|
||||||
|
|
||||||
|
def test_cohere_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="cohere/command-r-plus") == "cloud"
|
||||||
|
|
||||||
|
def test_mistralai_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="mistralai/mistral-large-2407") == "cloud"
|
||||||
|
|
||||||
|
def test_meta_llama_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="meta-llama/llama-3.1-70b-instruct") == "cloud"
|
||||||
|
|
||||||
|
def test_databricks_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="databricks/dbrx-instruct") == "cloud"
|
||||||
|
|
||||||
|
def test_together_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="together/together-api-model") == "cloud"
|
||||||
|
|
||||||
|
# ── Providers that were already detected before #628 ─────────────────
|
||||||
|
|
||||||
|
def test_openai_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="openai/gpt-4.1") == "cloud"
|
||||||
|
|
||||||
|
def test_anthropic_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="anthropic/claude-opus-4.6") == "cloud"
|
||||||
|
|
||||||
|
def test_google_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="google/gemini-3-pro") == "cloud"
|
||||||
|
|
||||||
|
def test_minimax_model_prefix_is_cloud(self):
|
||||||
|
assert _classify_runtime(model="minimax/minimax-m2.7") == "cloud"
|
||||||
|
|
||||||
|
# ── Fallback / edge cases ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_no_args_defaults_to_cloud(self):
|
||||||
|
assert _classify_runtime() == "cloud"
|
||||||
|
|
||||||
|
def test_empty_strings_default_to_cloud(self):
|
||||||
|
assert _classify_runtime(model="", base_url="", provider="") == "cloud"
|
||||||
|
|
||||||
|
def test_url_takes_priority_over_provider(self):
|
||||||
|
# Explicit local URL wins even if provider looks like cloud
|
||||||
|
assert _classify_runtime(model="openai/gpt-4", base_url="http://localhost:11434/v1", provider="openai") == "local"
|
||||||
|
|
||||||
|
def test_bare_model_name_without_slash_defaults_to_cloud(self):
|
||||||
|
# No slash → can't infer vendor → cloud (safe default)
|
||||||
|
assert _classify_runtime(model="gpt-4o") == "cloud"
|
||||||
|
|||||||
Reference in New Issue
Block a user