Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@ def _xai_curated_models() -> list[str]:
],
"tencent-tokenhub": [
"hy3-preview",
"hy3",
"kimi-k2.7-code-highspeed",
"glm-5.2",
"minimax-m3",
"deepseek-v4-pro-202606",
"qwen3.5-plus",
],
"arcee": [
"trinity-large-thinking",
Expand Down Expand Up @@ -2411,6 +2417,19 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
return live
except Exception:
pass
if normalized == "tencent-tokenhub":
try:
from hermes_cli.auth import resolve_api_key_provider_credentials

creds = resolve_api_key_provider_credentials("tencent-tokenhub")
api_key = str(creds.get("api_key") or "").strip()
base_url = str(creds.get("base_url") or "").strip()
if api_key and base_url:
live = fetch_api_models(api_key, base_url)
if live:
return live
except Exception:
pass
if normalized == "custom":
base_url = _get_custom_base_url()
if base_url:
Expand Down
43 changes: 43 additions & 0 deletions tools/threat_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,50 @@ def scan_for_threats(content: str, scope: str = "context") -> List[str]:
# since normalisation can strip some of these codepoints.
char_set = set(content)
invisible_hits = char_set & INVISIBLE_CHARS
filtered_hits: set[str] = set()
for ch in invisible_hits:
if ch == '\u200d':
# ZWJ is only a threat if NOT joining emoji characters.
# Check if the surrounding characters are emoji-like.
# Use Unicode category + codepoint heuristics since
# unicodedata.name() does not expose the Emoji property.
import unicodedata as _ud

def _is_emoji_like(c: str) -> bool:
if not c:
return False
cp = ord(c)
# Supplementary Multilingual Plane (>U+FFFF) — almost all emoji
if cp >= 0x10000:
return True
# Emoji presentation variation selector
if cp == 0xFE0F:
return True
# Symbol Other (So) — covers most legacy emoji (☀★🐱⬛)
return _ud.category(c) == 'So'

# If every occurrence of U+200D in content is between two
# emoji-like characters, it's a legitimate emoji ZWJ sequence.
is_legitimate_emoji = True
idx = 0
while True:
idx = content.find('\u200d', idx)
if idx == -1:
break
before = content[idx - 1] if idx > 0 else ''
after = content[idx + 1] if idx < len(content) - 1 else ''
if not (before and after):
is_legitimate_emoji = False
break
if not (_is_emoji_like(before) and _is_emoji_like(after)):
is_legitimate_emoji = False
break
idx += 1
if is_legitimate_emoji:
continue # skip — legitimate emoji ZWJ sequence
filtered_hits.add(ch)

for ch in filtered_hits:
findings.append(f"invisible_unicode_U+{ord(ch):04X}")

# Normalise to NFKC so full-width / compatibility Unicode variants
Expand Down
Loading