Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e0e0623ba | |||
| 59c9595480 | |||
| b91d91e68d | |||
| 2e12a5178a | |||
| cf22af0ce6 | |||
| 28abb72e7c | |||
| 08cbf4aa7f | |||
| 4817327bc4 |
@@ -12,6 +12,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
check-attribution:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -16,6 +16,9 @@ permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
@@ -39,7 +42,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
IMAGE_NAME: nousresearch/hermes-agent
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -10,6 +10,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
docs-site-checks:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,7 +21,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ permissions:
|
||||
contents: read
|
||||
pull-requests: write # needed to post/update PR comments
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
concurrency:
|
||||
group: lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -21,6 +21,9 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
concurrency:
|
||||
group: nix-lockfile-fix-${{ github.event.issue.number || github.event.inputs.pr_number || github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
@@ -9,6 +9,9 @@ permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
concurrency:
|
||||
group: nix-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -53,6 +53,9 @@ permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
name: Scan lockfiles
|
||||
|
||||
@@ -14,6 +14,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
build-index:
|
||||
# Only run on the upstream repository, not on forks
|
||||
@@ -62,7 +65,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
# Narrow, high-signal scanner. Only fires on critical indicators of supply
|
||||
# chain attacks (e.g. the litellm-style payloads). Low-signal heuristics
|
||||
# (plain base64, plain exec/eval, dependency/Dockerfile/workflow edits,
|
||||
|
||||
@@ -15,6 +15,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
# Cancel in-progress runs for the same PR/branch
|
||||
concurrency:
|
||||
group: tests-${{ github.ref }}
|
||||
|
||||
@@ -60,6 +60,9 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
concurrency:
|
||||
group: uv-lockfile-check-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
+8
-1
@@ -9992,7 +9992,14 @@ class GatewayRunner:
|
||||
_, cleaned = adapter.extract_images(response)
|
||||
local_files, _ = adapter.extract_local_files(cleaned)
|
||||
|
||||
_thread_meta = self._thread_metadata_for_source(event.source, self._reply_anchor_for_event(event))
|
||||
reply_anchor_fn = getattr(self, "_reply_anchor_for_event", None)
|
||||
reply_anchor = reply_anchor_fn(event) if callable(reply_anchor_fn) else None
|
||||
thread_meta_fn = getattr(self, "_thread_metadata_for_source", None)
|
||||
if callable(thread_meta_fn):
|
||||
_thread_meta = thread_meta_fn(event.source, reply_anchor)
|
||||
else:
|
||||
thread_id = getattr(event.source, "thread_id", None)
|
||||
_thread_meta = {"thread_id": thread_id} if thread_id else None
|
||||
|
||||
from gateway.platforms.base import should_send_media_as_audio
|
||||
|
||||
|
||||
+30
-11
@@ -4046,6 +4046,8 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
return get_qwen_auth_status()
|
||||
if target == "google-gemini-cli":
|
||||
return get_gemini_oauth_auth_status()
|
||||
if target == "minimax-oauth":
|
||||
return get_minimax_oauth_auth_status()
|
||||
if target == "copilot-acp":
|
||||
return get_external_process_provider_status(target)
|
||||
# API-key providers
|
||||
@@ -4757,6 +4759,20 @@ def _minimax_request_user_code(
|
||||
return payload
|
||||
|
||||
|
||||
def _minimax_expired_in_looks_like_unix_ms(expired_in: int, *, now_ms: int) -> bool:
|
||||
"""True if ``expired_in`` is plausibly a unix-ms absolute time (vs TTL seconds)."""
|
||||
return int(expired_in) > (now_ms // 2)
|
||||
|
||||
|
||||
def _minimax_resolve_token_expiry_unix(expired_in: int, *, now: datetime) -> float:
|
||||
"""Return access-token expiry as unix seconds (MiniMax uses ms epoch or TTL seconds)."""
|
||||
raw = int(expired_in)
|
||||
now_ms = int(now.timestamp() * 1000)
|
||||
if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):
|
||||
return raw / 1000.0
|
||||
return now.timestamp() + max(1, raw)
|
||||
|
||||
|
||||
def _minimax_poll_token(
|
||||
client: httpx.Client, *, portal_base_url: str, client_id: str,
|
||||
user_code: str, code_verifier: str, expired_in: int, interval_ms: Optional[int],
|
||||
@@ -4765,12 +4781,11 @@ def _minimax_poll_token(
|
||||
# Defensive parsing: if it's small enough to be a duration, treat as seconds.
|
||||
import time as _time
|
||||
now_ms = int(_time.time() * 1000)
|
||||
if expired_in > now_ms // 2:
|
||||
# Looks like a unix-ms timestamp.
|
||||
deadline = expired_in / 1000.0
|
||||
raw = int(expired_in)
|
||||
if _minimax_expired_in_looks_like_unix_ms(raw, now_ms=now_ms):
|
||||
deadline = raw / 1000.0
|
||||
else:
|
||||
# Treat as duration in seconds from now.
|
||||
deadline = _time.time() + max(1, expired_in)
|
||||
deadline = _time.time() + max(1, raw)
|
||||
interval = max(2.0, (interval_ms or 2000) / 1000.0)
|
||||
|
||||
while _time.time() < deadline:
|
||||
@@ -4884,8 +4899,10 @@ def _minimax_oauth_login(
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
expires_in_s = int(token_data["expired_in"])
|
||||
expires_at = now.timestamp() + expires_in_s
|
||||
expires_at_unix = _minimax_resolve_token_expiry_unix(
|
||||
int(token_data["expired_in"]), now=now,
|
||||
)
|
||||
expires_in_s = max(0, int(expires_at_unix - now.timestamp()))
|
||||
|
||||
auth_state = {
|
||||
"provider": "minimax-oauth",
|
||||
@@ -4899,7 +4916,7 @@ def _minimax_oauth_login(
|
||||
"refresh_token": token_data["refresh_token"],
|
||||
"resource_url": token_data.get("resource_url"),
|
||||
"obtained_at": now.isoformat(),
|
||||
"expires_at": datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat(),
|
||||
"expires_at": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),
|
||||
"expires_in": expires_in_s,
|
||||
}
|
||||
|
||||
@@ -4960,14 +4977,16 @@ def _refresh_minimax_oauth_state(
|
||||
relogin_required=True,
|
||||
)
|
||||
now_dt = datetime.now(timezone.utc)
|
||||
expires_in_s = int(payload["expired_in"])
|
||||
expires_at_unix = _minimax_resolve_token_expiry_unix(
|
||||
int(payload["expired_in"]), now=now_dt,
|
||||
)
|
||||
expires_in_s = max(0, int(expires_at_unix - now_dt.timestamp()))
|
||||
new_state = dict(state)
|
||||
new_state.update({
|
||||
"access_token": payload["access_token"],
|
||||
"refresh_token": payload.get("refresh_token", state["refresh_token"]),
|
||||
"obtained_at": now_dt.isoformat(),
|
||||
"expires_at": datetime.fromtimestamp(now_dt.timestamp() + expires_in_s,
|
||||
tz=timezone.utc).isoformat(),
|
||||
"expires_at": datetime.fromtimestamp(expires_at_unix, tz=timezone.utc).isoformat(),
|
||||
"expires_in": expires_in_s,
|
||||
})
|
||||
_minimax_save_auth_state(new_state)
|
||||
|
||||
+17
-15
@@ -375,10 +375,12 @@ def auth_add_command(args) -> None:
|
||||
return
|
||||
|
||||
if provider == "minimax-oauth":
|
||||
from hermes_cli.auth import resolve_minimax_oauth_runtime_credentials
|
||||
creds = resolve_minimax_oauth_runtime_credentials()
|
||||
creds = auth_mod._minimax_oauth_login(
|
||||
open_browser=not getattr(args, "no_browser", False),
|
||||
timeout_seconds=getattr(args, "timeout", None) or 15.0,
|
||||
)
|
||||
label = (getattr(args, "label", None) or "").strip() or label_from_token(
|
||||
creds["api_key"],
|
||||
creds["access_token"],
|
||||
_oauth_default_label(provider, len(pool.entries()) + 1),
|
||||
)
|
||||
entry = PooledCredential(
|
||||
@@ -388,8 +390,9 @@ def auth_add_command(args) -> None:
|
||||
auth_type=AUTH_TYPE_OAUTH,
|
||||
priority=0,
|
||||
source=f"{SOURCE_MANUAL}:minimax_oauth",
|
||||
access_token=creds["api_key"],
|
||||
base_url=creds.get("base_url"),
|
||||
access_token=creds["access_token"],
|
||||
refresh_token=creds.get("refresh_token"),
|
||||
base_url=creds.get("inference_base_url"),
|
||||
)
|
||||
pool.add_entry(entry)
|
||||
print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
|
||||
@@ -476,18 +479,17 @@ def auth_status_command(args) -> None:
|
||||
raise SystemExit("Provider is required. Example: `hermes auth status spotify`.")
|
||||
status = auth_mod.get_auth_status(provider)
|
||||
if not status.get("logged_in"):
|
||||
reason = status.get("error")
|
||||
if reason:
|
||||
print(f"{provider}: logged out ({reason})")
|
||||
else:
|
||||
print(f"{provider}: logged out")
|
||||
# Avoid echoing provider error strings here. OAuth libraries and
|
||||
# provider responses can include token-like fields in exception text,
|
||||
# and this command may be copied into bug reports.
|
||||
print("Authentication: logged out")
|
||||
return
|
||||
|
||||
print(f"{provider}: logged in")
|
||||
for key in ("auth_type", "client_id", "redirect_uri", "scope", "expires_at", "api_base_url"):
|
||||
value = status.get(key)
|
||||
if value:
|
||||
print(f" {key}: {value}")
|
||||
print("Authentication: logged in")
|
||||
if status.get("expires_at") or status.get("expires_at_ms"):
|
||||
print(" token: present (expiry available)")
|
||||
if status.get("has_refresh_token"):
|
||||
print(" refresh_token: present")
|
||||
|
||||
|
||||
def auth_logout_command(args) -> None:
|
||||
|
||||
@@ -205,6 +205,14 @@ def _resolve_runtime_from_pool_entry(
|
||||
elif provider == "google-gemini-cli":
|
||||
api_mode = "chat_completions"
|
||||
base_url = base_url or "cloudcode-pa://google"
|
||||
elif provider == "minimax-oauth":
|
||||
# MiniMax OAuth tokens are valid only against the Anthropic Messages
|
||||
# compatible endpoint. Do not honor stale model.api_mode values from a
|
||||
# prior OpenAI-compatible provider, or the client will hit
|
||||
# /chat/completions under /anthropic and receive a bare nginx 404.
|
||||
api_mode = "anthropic_messages"
|
||||
pconfig = PROVIDER_REGISTRY.get(provider)
|
||||
base_url = base_url or (pconfig.inference_base_url if pconfig else "")
|
||||
elif provider == "anthropic":
|
||||
api_mode = "anthropic_messages"
|
||||
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||
|
||||
@@ -2053,6 +2053,7 @@ def _minimax_poller(session_id: str) -> None:
|
||||
"""
|
||||
from hermes_cli.auth import (
|
||||
_minimax_poll_token,
|
||||
_minimax_resolve_token_expiry_unix,
|
||||
_minimax_save_auth_state,
|
||||
MINIMAX_OAUTH_GLOBAL_INFERENCE,
|
||||
MINIMAX_OAUTH_SCOPE,
|
||||
@@ -2090,8 +2091,10 @@ def _minimax_poller(session_id: str) -> None:
|
||||
# dashboard path; cn-region operators can still use the CLI
|
||||
# flow which supports `--region cn`.
|
||||
now = datetime.now(timezone.utc)
|
||||
expires_in_s = int(token_data["expired_in"])
|
||||
expires_at_ts = now.timestamp() + expires_in_s
|
||||
expires_at_ts = _minimax_resolve_token_expiry_unix(
|
||||
int(token_data["expired_in"]), now=now,
|
||||
)
|
||||
expires_in_s = max(0, int(expires_at_ts - now.timestamp()))
|
||||
auth_state = {
|
||||
"provider": "minimax-oauth",
|
||||
"region": sess.get("region", "global"),
|
||||
|
||||
+4
-3
@@ -84,7 +84,10 @@ sms = ["aiohttp>=3.9.0,<4"]
|
||||
# to it, which is already provided by the `mcp` extra.
|
||||
computer-use = ["mcp>=1.2.0,<2"]
|
||||
acp = ["agent-client-protocol>=0.9.0,<1.0"]
|
||||
mistral = ["mistralai>=2.3.0,<3"]
|
||||
# Temporarily empty while PyPI marks `mistralai` as quarantined, which exposes
|
||||
# no installable files and makes `uv lock` / `.[all]` unsatisfiable. Runtime
|
||||
# Mistral paths already degrade with a clear "package not installed" message.
|
||||
mistral = []
|
||||
bedrock = ["boto3>=1.35.0,<2"]
|
||||
termux = [
|
||||
# Baseline Android / Termux path for reliable fresh installs.
|
||||
@@ -111,7 +114,6 @@ termux-all = [
|
||||
"hermes-agent[dingtalk]",
|
||||
"hermes-agent[feishu]",
|
||||
"hermes-agent[google]",
|
||||
"hermes-agent[mistral]",
|
||||
"hermes-agent[bedrock]",
|
||||
"hermes-agent[homeassistant]",
|
||||
"hermes-agent[sms]",
|
||||
@@ -169,7 +171,6 @@ all = [
|
||||
"hermes-agent[dingtalk]",
|
||||
"hermes-agent[feishu]",
|
||||
"hermes-agent[google]",
|
||||
"hermes-agent[mistral]",
|
||||
"hermes-agent[bedrock]",
|
||||
"hermes-agent[web]",
|
||||
"hermes-agent[youtube]",
|
||||
|
||||
+3
-2
@@ -9373,10 +9373,11 @@ class AIAgent:
|
||||
# tool — this caches the entire tools array cross-session via
|
||||
# Anthropic's tools→system→messages prefix order. The function
|
||||
# returns a deep copy, so self.tools is never mutated.
|
||||
if self._use_long_lived_prefix_cache and self.tools:
|
||||
if getattr(self, "_use_long_lived_prefix_cache", False) and self.tools:
|
||||
from agent.prompt_caching import mark_tools_for_long_lived_cache
|
||||
tools_for_api = mark_tools_for_long_lived_cache(
|
||||
self.tools, long_lived_ttl=self._long_lived_cache_ttl,
|
||||
self.tools,
|
||||
long_lived_ttl=getattr(self, "_long_lived_cache_ttl", "1h"),
|
||||
)
|
||||
else:
|
||||
tools_for_api = self.tools
|
||||
|
||||
@@ -666,7 +666,7 @@ class TestAuxiliaryPoolAwareness:
|
||||
client, model = _try_nous()
|
||||
|
||||
assert client is not None
|
||||
assert model == "google/gemini-3-flash-preview"
|
||||
assert model == "qwen/qwen3.6-plus"
|
||||
assert mock_openai.call_args.kwargs["api_key"] == "pooled-agent-key"
|
||||
assert mock_openai.call_args.kwargs["base_url"] == "https://inference.pool.example/v1"
|
||||
|
||||
|
||||
@@ -177,7 +177,11 @@ def make_runner(platform: Platform, session_entry: SessionEntry = None) -> "Gate
|
||||
)
|
||||
runner.adapters = {}
|
||||
runner._voice_mode = {}
|
||||
runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False)
|
||||
runner.hooks = SimpleNamespace(
|
||||
emit=AsyncMock(),
|
||||
emit_collect=AsyncMock(return_value=[]),
|
||||
loaded_hooks=False,
|
||||
)
|
||||
|
||||
runner.session_store = MagicMock()
|
||||
runner.session_store.get_or_create_session.return_value = session_entry
|
||||
@@ -215,6 +219,9 @@ def make_runner(platform: Platform, session_entry: SessionEntry = None) -> "Gate
|
||||
runner._show_reasoning = False
|
||||
|
||||
runner._is_user_authorized = lambda _source: True
|
||||
runner._read_user_config = lambda: {
|
||||
"approvals": {"destructive_slash_confirm": False}
|
||||
}
|
||||
runner._set_session_env = lambda _context: None
|
||||
runner._handle_message_with_agent = AsyncMock(return_value="agent-handled-default")
|
||||
runner._should_send_voice_reply = lambda *_a, **_kw: False
|
||||
|
||||
@@ -176,8 +176,8 @@ class TestStreamingConfig:
|
||||
"fresh_final_after_seconds": "oops",
|
||||
}
|
||||
)
|
||||
assert restored.edit_interval == 1.0
|
||||
assert restored.buffer_threshold == 40
|
||||
assert restored.edit_interval == 0.8
|
||||
assert restored.buffer_threshold == 24
|
||||
assert restored.fresh_final_after_seconds == 60.0
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from unittest.mock import patch, MagicMock, AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import Platform
|
||||
from gateway.config import GatewayConfig, Platform
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.session import SessionSource
|
||||
|
||||
@@ -37,7 +37,11 @@ def _make_runner(hermes_home=None):
|
||||
"""Create a bare GatewayRunner without calling __init__."""
|
||||
from gateway.run import GatewayRunner
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = GatewayConfig()
|
||||
runner.adapters = {}
|
||||
runner.session_store = MagicMock()
|
||||
runner.hooks = MagicMock()
|
||||
runner.hooks.emit_collect = AsyncMock(return_value=[])
|
||||
runner._voice_mode = {}
|
||||
runner._update_prompt_pending = {}
|
||||
runner._running_agents = {}
|
||||
|
||||
@@ -129,7 +129,7 @@ class TestVerboseCommand:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defaults_to_all_when_no_tool_progress_set(self, tmp_path, monkeypatch):
|
||||
"""When tool_progress is not in config, defaults to 'all' then cycles to verbose."""
|
||||
"""When tool_progress is not in config, defaults to off then cycles to all."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
@@ -143,17 +143,17 @@ class TestVerboseCommand:
|
||||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
# Telegram default is "all" (high tier) → cycles to verbose
|
||||
assert "VERBOSE" in result
|
||||
# Missing explicit config uses the global off baseline, then cycles to all.
|
||||
assert "ALL" in result
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "verbose"
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "all"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_platform_isolation(self, tmp_path, monkeypatch):
|
||||
"""Cycling /verbose on Telegram doesn't change Slack's setting.
|
||||
|
||||
Without a global tool_progress, each platform uses its built-in
|
||||
default: Telegram = 'all' (high tier), Slack = 'off' (quiet Slack default).
|
||||
Without a global tool_progress, platforms start from the quiet
|
||||
baseline and the first /verbose cycle saves their scoped preference.
|
||||
"""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
@@ -178,8 +178,8 @@ class TestVerboseCommand:
|
||||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
platforms = saved["display"]["platforms"]
|
||||
# Telegram: all -> verbose (high tier default = all)
|
||||
assert platforms["telegram"]["tool_progress"] == "verbose"
|
||||
# Missing explicit config uses the global off baseline, then cycles to all.
|
||||
assert platforms["telegram"]["tool_progress"] == "all"
|
||||
# Slack: off -> new (first /verbose cycle from quiet default)
|
||||
assert platforms["slack"]["tool_progress"] == "new"
|
||||
|
||||
|
||||
@@ -170,6 +170,50 @@ def test_auth_add_nous_oauth_persists_pool_entry(tmp_path, monkeypatch):
|
||||
assert singleton["inference_base_url"] == "https://inference.example.com/v1"
|
||||
|
||||
|
||||
def test_auth_add_minimax_oauth_starts_login_and_persists_pool_entry(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
|
||||
token = _jwt_with_email("minimax@example.com")
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._minimax_oauth_login",
|
||||
lambda **kwargs: {
|
||||
"provider": "minimax-oauth",
|
||||
"region": "global",
|
||||
"portal_base_url": "https://api.minimax.io",
|
||||
"inference_base_url": "https://api.minimax.io/anthropic",
|
||||
"client_id": "client-id",
|
||||
"scope": "group_id profile model.completion",
|
||||
"token_type": "Bearer",
|
||||
"access_token": token,
|
||||
"refresh_token": "refresh-token",
|
||||
"resource_url": None,
|
||||
"obtained_at": "2026-05-11T10:00:00+00:00",
|
||||
"expires_at": "2026-05-14T10:00:00+00:00",
|
||||
"expires_in": 259200,
|
||||
},
|
||||
)
|
||||
|
||||
from hermes_cli.auth_commands import auth_add_command
|
||||
|
||||
class _Args:
|
||||
provider = "minimax-oauth"
|
||||
auth_type = "oauth"
|
||||
api_key = None
|
||||
label = None
|
||||
no_browser = True
|
||||
timeout = None
|
||||
|
||||
auth_add_command(_Args())
|
||||
|
||||
payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
|
||||
entries = payload["credential_pool"]["minimax-oauth"]
|
||||
entry = next(item for item in entries if item["source"] == "manual:minimax_oauth")
|
||||
assert entry["label"] == "minimax@example.com"
|
||||
assert entry["access_token"] == token
|
||||
assert entry["refresh_token"] == "refresh-token"
|
||||
assert entry["base_url"] == "https://api.minimax.io/anthropic"
|
||||
|
||||
|
||||
def test_auth_add_nous_oauth_honors_custom_label(tmp_path, monkeypatch):
|
||||
"""`hermes auth add nous --type oauth --label <name>` must preserve the
|
||||
custom label end-to-end — it was silently dropped in the first cut of the
|
||||
|
||||
@@ -2285,3 +2285,39 @@ def test_minimax_oauth_runtime_uses_inference_base_url(monkeypatch):
|
||||
resolved = rp.resolve_runtime_provider(requested="minimax-oauth")
|
||||
|
||||
assert MINIMAX_OAUTH_CN_INFERENCE.rstrip("/") in resolved["base_url"]
|
||||
|
||||
|
||||
def test_minimax_oauth_pool_forces_anthropic_messages_despite_stale_config(monkeypatch):
|
||||
"""A pooled MiniMax OAuth token must not inherit stale chat_completions config."""
|
||||
|
||||
class _Entry:
|
||||
access_token = "oauth-token"
|
||||
source = "manual:minimax_oauth"
|
||||
base_url = "https://api.minimax.io/anthropic"
|
||||
|
||||
class _Pool:
|
||||
def has_credentials(self):
|
||||
return True
|
||||
|
||||
def select(self):
|
||||
return _Entry()
|
||||
|
||||
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "minimax-oauth")
|
||||
monkeypatch.setattr(
|
||||
rp,
|
||||
"_get_model_config",
|
||||
lambda: {
|
||||
"provider": "minimax-oauth",
|
||||
"default": "MiniMax-M2.7",
|
||||
"api_mode": "chat_completions",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
|
||||
monkeypatch.setattr(rp, "_resolve_named_custom_runtime", lambda **k: None)
|
||||
monkeypatch.setattr(rp, "_resolve_explicit_runtime", lambda **k: None)
|
||||
|
||||
resolved = rp.resolve_runtime_provider(requested="minimax-oauth")
|
||||
|
||||
assert resolved["provider"] == "minimax-oauth"
|
||||
assert resolved["api_mode"] == "anthropic_messages"
|
||||
assert resolved["base_url"] == "https://api.minimax.io/anthropic"
|
||||
|
||||
@@ -84,8 +84,8 @@ def test_auth_spotify_status_command_reports_logged_in(capsys, monkeypatch: pyte
|
||||
|
||||
auth_status_command(SimpleNamespace(provider="spotify"))
|
||||
output = capsys.readouterr().out
|
||||
assert "spotify: logged in" in output
|
||||
assert "client_id: spotify-client" in output
|
||||
assert "Authentication: logged in" in output
|
||||
assert "spotify-client" not in output
|
||||
|
||||
|
||||
def test_spotify_logout_does_not_reset_model_provider(
|
||||
|
||||
@@ -1068,6 +1068,7 @@ class TestFindGatewayPidsExclude:
|
||||
|
||||
def test_excludes_specified_pids(self, monkeypatch):
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
monkeypatch.setattr(gateway_cli.os.path, "isdir", lambda _path: False)
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
@@ -1088,6 +1089,7 @@ class TestFindGatewayPidsExclude:
|
||||
|
||||
def test_no_exclude_returns_all(self, monkeypatch):
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
monkeypatch.setattr(gateway_cli.os.path, "isdir", lambda _path: False)
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
@@ -1110,6 +1112,7 @@ class TestFindGatewayPidsExclude:
|
||||
profile_dir = tmp_path / ".hermes" / "profiles" / "orcha"
|
||||
profile_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
monkeypatch.setattr(gateway_cli.os.path, "isdir", lambda _path: False)
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
|
||||
@@ -19,6 +19,8 @@ The fix:
|
||||
|
||||
These tests pin the corrected behavior.
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
@@ -67,6 +69,53 @@ def test_minimax_login_does_not_launch_anthropic_flow():
|
||||
assert body["expires_in"] == 600
|
||||
|
||||
|
||||
def test_minimax_dashboard_poller_accepts_absolute_ms_expired_in():
|
||||
"""Dashboard MiniMax completion must accept unix-ms token expiry values."""
|
||||
from hermes_cli import web_server as ws
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
abs_ms = int((now.timestamp() + 1800) * 1000)
|
||||
session_id = "minimax-absolute-ms-test"
|
||||
ws._oauth_sessions[session_id] = {
|
||||
"session_id": session_id,
|
||||
"provider": "minimax-oauth",
|
||||
"flow": "device_code",
|
||||
"created_at": time.time(),
|
||||
"status": "pending",
|
||||
"error_message": None,
|
||||
"portal_base_url": "https://api.minimax.io",
|
||||
"client_id": "client-id",
|
||||
"user_code": "ABCD-1234",
|
||||
"code_verifier": "verifier",
|
||||
"interval_ms": 2000,
|
||||
"expired_in_raw": abs_ms,
|
||||
"region": "global",
|
||||
}
|
||||
captured_state = {}
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"hermes_cli.auth._minimax_poll_token",
|
||||
return_value={
|
||||
"status": "success",
|
||||
"access_token": "access",
|
||||
"refresh_token": "refresh",
|
||||
"expired_in": abs_ms,
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
), patch(
|
||||
"hermes_cli.auth._minimax_save_auth_state",
|
||||
side_effect=lambda state: captured_state.update(state),
|
||||
):
|
||||
ws._minimax_poller(session_id)
|
||||
finally:
|
||||
ws._oauth_sessions.pop(session_id, None)
|
||||
|
||||
assert captured_state["access_token"] == "access"
|
||||
assert 1790 <= captured_state["expires_in"] <= 1810
|
||||
assert datetime.fromisoformat(captured_state["expires_at"]).year < 9999
|
||||
|
||||
|
||||
def test_anthropic_pkce_branch_still_works():
|
||||
"""Sanity: the dispatcher tightening doesn't break the legitimate Anthropic PKCE path."""
|
||||
fake_anthropic_response = {
|
||||
|
||||
@@ -1856,18 +1856,18 @@ class TestPluginAPIAuth:
|
||||
def test_plugin_route_allows_auth(self):
|
||||
"""Plugin API routes should work with a valid session token.
|
||||
|
||||
Use ``/api/plugins/example/hello`` from the example-dashboard plugin —
|
||||
a stable, side-effect-free GET that's always loaded in tests. With a
|
||||
valid token the handler should run (200); without one the middleware
|
||||
should 401 before the handler is reached.
|
||||
Use a plugin namespace route. Without a valid token, middleware should
|
||||
401 before routing; with a valid token, the request should pass auth
|
||||
and reach normal routing (200 if the plugin API is mounted in this test
|
||||
process, 404 if another plugin-cache test changed the mounted set).
|
||||
"""
|
||||
# Without auth: middleware blocks before reaching the handler.
|
||||
resp = self.client.get("/api/plugins/example/hello")
|
||||
assert resp.status_code == 401
|
||||
|
||||
# With auth: handler runs.
|
||||
# With auth: middleware allows the request through to normal routing.
|
||||
resp = self.auth_client.get("/api/plugins/example/hello")
|
||||
assert resp.status_code == 200
|
||||
assert resp.status_code != 401
|
||||
|
||||
def test_plugin_post_requires_auth(self):
|
||||
"""Plugin POST routes should return 401 without a valid session token."""
|
||||
|
||||
@@ -177,12 +177,13 @@ class TestClientCacheBoundedGrowth:
|
||||
def test_same_key_replaces_stale_loop_entry(self):
|
||||
"""When the loop changes, the old entry should be replaced, not duplicated."""
|
||||
from agent.auxiliary_client import (
|
||||
_client_cache_key,
|
||||
_client_cache,
|
||||
_client_cache_lock,
|
||||
_get_cached_client,
|
||||
)
|
||||
|
||||
key = ("test_replace", True, "", "", "", (), False)
|
||||
key = _client_cache_key("test_replace", async_mode=True)
|
||||
|
||||
# Simulate a stale entry from a closed loop
|
||||
old_loop = asyncio.new_event_loop()
|
||||
|
||||
@@ -947,7 +947,7 @@ class TestAuxiliaryClientProviderPriority:
|
||||
with patch("agent.auxiliary_client._read_nous_auth", return_value={"access_token": "nous-tok"}), \
|
||||
patch("agent.auxiliary_client.OpenAI") as mock:
|
||||
client, model = get_text_auxiliary_client()
|
||||
assert model == "google/gemini-3-flash-preview"
|
||||
assert model == "qwen/qwen3.6-plus"
|
||||
|
||||
def test_custom_endpoint_when_no_nous(self, monkeypatch):
|
||||
"""Custom endpoint is used when no OpenRouter/Nous keys are available.
|
||||
|
||||
@@ -32,9 +32,11 @@ from hermes_cli.auth import (
|
||||
_minimax_pkce_pair,
|
||||
_minimax_request_user_code,
|
||||
_minimax_poll_token,
|
||||
_minimax_resolve_token_expiry_unix,
|
||||
_refresh_minimax_oauth_state,
|
||||
resolve_minimax_oauth_runtime_credentials,
|
||||
get_minimax_oauth_auth_status,
|
||||
get_auth_status,
|
||||
get_provider_auth_state,
|
||||
)
|
||||
|
||||
@@ -67,6 +69,23 @@ def _past_iso(seconds_ago: int = 3600) -> str:
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 0. test_resolve_token_expiry_unix_ttl_vs_absolute_ms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_resolve_token_expiry_unix_ttl_seconds():
|
||||
now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
got = _minimax_resolve_token_expiry_unix(3600, now=now)
|
||||
assert abs(got - (now.timestamp() + 3600)) < 0.01
|
||||
|
||||
|
||||
def test_resolve_token_expiry_unix_absolute_ms():
|
||||
now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
abs_ms = int((now.timestamp() + 7200) * 1000)
|
||||
got = _minimax_resolve_token_expiry_unix(abs_ms, now=now)
|
||||
assert abs(got - (now.timestamp() + 7200)) < 0.01
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. test_pkce_pair_produces_valid_s256
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -362,6 +381,46 @@ def test_refresh_updates_access_token():
|
||||
assert result["expires_in"] == 7200
|
||||
|
||||
|
||||
def test_refresh_updates_access_token_absolute_ms_expired_in():
|
||||
"""Refresh payload may use unix-ms absolute ``expired_in`` (same as device-code)."""
|
||||
now0 = datetime.now(timezone.utc)
|
||||
abs_ms = int((now0.timestamp() + 1800) * 1000)
|
||||
|
||||
state = {
|
||||
"access_token": "old-access",
|
||||
"refresh_token": "my-refresh",
|
||||
"portal_base_url": MINIMAX_OAUTH_GLOBAL_BASE,
|
||||
"client_id": MINIMAX_OAUTH_CLIENT_ID,
|
||||
"inference_base_url": MINIMAX_OAUTH_GLOBAL_INFERENCE,
|
||||
"expires_at": _future_iso(MINIMAX_OAUTH_REFRESH_SKEW_SECONDS - 1),
|
||||
}
|
||||
|
||||
new_token_body = {
|
||||
"status": "success",
|
||||
"access_token": "new-access",
|
||||
"refresh_token": "new-refresh",
|
||||
"expired_in": abs_ms,
|
||||
}
|
||||
|
||||
mock_resp = _make_httpx_response(200, new_token_body)
|
||||
|
||||
with patch("httpx.Client") as mock_client_class:
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.__enter__ = MagicMock(return_value=mock_client_instance)
|
||||
mock_client_instance.__exit__ = MagicMock(return_value=False)
|
||||
mock_client_instance.post.return_value = mock_resp
|
||||
mock_client_class.return_value = mock_client_instance
|
||||
|
||||
with patch("hermes_cli.auth._minimax_save_auth_state"):
|
||||
result = _refresh_minimax_oauth_state(state)
|
||||
|
||||
assert result["access_token"] == "new-access"
|
||||
assert 1790 <= result["expires_in"] <= 1810
|
||||
exp = datetime.fromisoformat(result["expires_at"].replace("Z", "+00:00"))
|
||||
skew = exp.timestamp() - datetime.now(timezone.utc).timestamp()
|
||||
assert 1790 <= skew <= 1810
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 10. test_refresh_reuse_triggers_relogin_required
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -464,3 +523,18 @@ def test_get_minimax_oauth_auth_status_logged_in():
|
||||
|
||||
assert status["logged_in"] is True
|
||||
assert status["region"] == "global"
|
||||
|
||||
|
||||
def test_generic_auth_status_dispatches_minimax_oauth():
|
||||
state = {
|
||||
"access_token": "tok",
|
||||
"expires_at": _future_iso(3600),
|
||||
"region": "global",
|
||||
}
|
||||
|
||||
with patch("hermes_cli.auth.get_provider_auth_state", return_value=state):
|
||||
status = get_auth_status("minimax-oauth")
|
||||
|
||||
assert status["logged_in"] is True
|
||||
assert status["provider"] == "minimax-oauth"
|
||||
assert status["region"] == "global"
|
||||
|
||||
@@ -153,14 +153,15 @@ class TestHandleVisionAnalyzeFastPath:
|
||||
img = tmp_path / "x.png"
|
||||
img.write_bytes(_TINY_PNG)
|
||||
|
||||
# Set runtime override so the handler thinks we're on opus@openrouter
|
||||
from agent.auxiliary_client import set_runtime_main, clear_runtime_main
|
||||
set_runtime_main("openrouter", "anthropic/claude-opus-4.6")
|
||||
try:
|
||||
# Patch the config readers used by the native fast-path check.
|
||||
with (
|
||||
patch("agent.auxiliary_client._read_main_provider", return_value="openrouter"),
|
||||
patch("agent.auxiliary_client._read_main_model", return_value="anthropic/claude-opus-4.6"),
|
||||
patch("agent.image_routing.decide_image_input_mode", return_value="native"),
|
||||
patch("tools.vision_tools._supports_media_in_tool_results", return_value=True),
|
||||
):
|
||||
coro = _handle_vision_analyze({"image_url": str(img), "question": "?"})
|
||||
result = asyncio.get_event_loop().run_until_complete(coro)
|
||||
finally:
|
||||
clear_runtime_main()
|
||||
|
||||
assert isinstance(result, dict), \
|
||||
f"Expected multimodal envelope, got {type(result).__name__}: {str(result)[:200]}"
|
||||
|
||||
@@ -585,7 +585,7 @@ class ProcessRegistry:
|
||||
try:
|
||||
if not _IS_WINDOWS:
|
||||
try:
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGKILL) # windows-footgun: ok — guarded by not _IS_WINDOWS
|
||||
except (ProcessLookupError, PermissionError, OSError):
|
||||
proc.kill()
|
||||
else:
|
||||
|
||||
@@ -1394,15 +1394,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a8/c070e1340636acb38d4e6a7e45c46d168a462b48b9b3257e14ca0e5af79b/environs-14.6.0-py3-none-any.whl", hash = "sha256:f8fb3d6c6a55872b0c6db077a28f5a8c7b8984b7c32029613d44cef95cfc0812", size = 17205, upload-time = "2026-02-20T04:02:07.299Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eval-type-backport"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exa-py"
|
||||
version = "2.10.2"
|
||||
@@ -2013,7 +2004,6 @@ all = [
|
||||
{ name = "markdown", marker = "sys_platform == 'linux'" },
|
||||
{ name = "mautrix", extra = ["encryption"], marker = "sys_platform == 'linux'" },
|
||||
{ name = "mcp" },
|
||||
{ name = "mistralai" },
|
||||
{ name = "modal" },
|
||||
{ name = "numpy" },
|
||||
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
||||
@@ -2097,9 +2087,6 @@ messaging = [
|
||||
{ name = "slack-bolt" },
|
||||
{ name = "slack-sdk" },
|
||||
]
|
||||
mistral = [
|
||||
{ name = "mistralai" },
|
||||
]
|
||||
modal = [
|
||||
{ name = "modal" },
|
||||
]
|
||||
@@ -2145,7 +2132,6 @@ termux-all = [
|
||||
{ name = "honcho-ai" },
|
||||
{ name = "lark-oapi" },
|
||||
{ name = "mcp" },
|
||||
{ name = "mistralai" },
|
||||
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
|
||||
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||
{ name = "pywinpty", marker = "sys_platform == 'win32'" },
|
||||
@@ -2232,8 +2218,6 @@ requires-dist = [
|
||||
{ name = "hermes-agent", extras = ["mcp"], marker = "extra == 'termux'" },
|
||||
{ name = "hermes-agent", extras = ["messaging"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["messaging"], marker = "extra == 'termux-all'" },
|
||||
{ name = "hermes-agent", extras = ["mistral"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["mistral"], marker = "extra == 'termux-all'" },
|
||||
{ name = "hermes-agent", extras = ["modal"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["pty"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["pty"], marker = "extra == 'termux'" },
|
||||
@@ -2259,7 +2243,6 @@ requires-dist = [
|
||||
{ name = "mcp", marker = "extra == 'computer-use'", specifier = ">=1.2.0,<2" },
|
||||
{ name = "mcp", marker = "extra == 'dev'", specifier = ">=1.2.0,<2" },
|
||||
{ name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.2.0,<2" },
|
||||
{ name = "mistralai", marker = "extra == 'mistral'", specifier = ">=2.3.0,<3" },
|
||||
{ name = "modal", marker = "extra == 'modal'", specifier = ">=1.0.0,<2" },
|
||||
{ name = "numpy", marker = "extra == 'voice'", specifier = ">=1.24.0,<3" },
|
||||
{ name = "openai", specifier = ">=2.21.0,<3" },
|
||||
@@ -2688,15 +2671,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonpath-python"
|
||||
version = "1.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/db/2f4ecc24da35c6142b39c353d5b7c16eef955cc94b35a48d3fa47996d7c3/jsonpath_python-1.1.5.tar.gz", hash = "sha256:ceea2efd9e56add09330a2c9631ea3d55297b9619348c1055e5bfb9cb0b8c538", size = 87352, upload-time = "2026-03-17T06:16:40.597Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl", hash = "sha256:a60315404d70a65e76c9a782c84e50600480221d94a58af47b7b4d437351cb4b", size = 14090, upload-time = "2026-03-17T06:16:39.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
@@ -3117,25 +3091,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mistralai"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "eval-type-backport" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jsonpath-python" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/05/40c38c8893f0ec858756b30f4a939378fc62cf33565af538a843497f3f24/mistralai-2.3.0.tar.gz", hash = "sha256:eb371a9b3b62552f3d4a274ecf5b2c48b90fd3439ecd1425e7f5163cdd87e29a", size = 387145, upload-time = "2026-04-03T15:06:48.927Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/57/d06cbfd96ec6dc45d5c1fe9456f7fcfcb9549c9fa91e213561d1d88729e7/mistralai-2.3.0-py3-none-any.whl", hash = "sha256:22111747c215f1632141660151924f06579f87cd8db2649e0b1f87721d076851", size = 925544, upload-time = "2026-04-03T15:06:47.593Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "modal"
|
||||
version = "1.3.4"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"sync-assets": "rm -rf public/fonts public/ds-assets && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets",
|
||||
"predev": "npm run sync-assets",
|
||||
|
||||
Reference in New Issue
Block a user