mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(gateway): add api_server /p/<profile>/ multiplex routing
Docs and MultiplexConfigError already promise secondary profiles are served through the shared listener's /p/<profile>/ prefix, but only the webhook adapter registered those routes — api_server returned 404. Mirror every HTTP route, validate the prefix, and scope agent runs / session DB / model listing to the target profile.
This commit is contained in:
parent
e0240d7bf7
commit
7aa21e3362
1 changed files with 280 additions and 133 deletions
|
|
@ -27,6 +27,14 @@ AnythingLLM, NextChat, ChatBox, etc.) can connect to hermes-agent
|
||||||
through this adapter by pointing at http://localhost:8642/v1 and
|
through this adapter by pointing at http://localhost:8642/v1 and
|
||||||
authenticating with API_SERVER_KEY.
|
authenticating with API_SERVER_KEY.
|
||||||
|
|
||||||
|
When ``gateway.multiplex_profiles`` is on, the default profile owns this
|
||||||
|
listener and secondary profiles are reached via a URL prefix — same contract
|
||||||
|
as the webhook adapter:
|
||||||
|
|
||||||
|
GET /p/<profile>/v1/models
|
||||||
|
POST /p/<profile>/v1/chat/completions
|
||||||
|
...
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
- aiohttp (already available in the gateway)
|
- aiohttp (already available in the gateway)
|
||||||
"""
|
"""
|
||||||
|
|
@ -35,7 +43,7 @@ import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager, nullcontext
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -48,6 +56,17 @@ import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
# Sentinel returned by _resolve_request_profile when a /p/<profile>/ prefix
|
||||||
|
# names a profile this gateway does not serve (→ 404). Distinct from None
|
||||||
|
# (no prefix / multiplexing off → handle as the default profile).
|
||||||
|
_PROFILE_REJECTED = object()
|
||||||
|
|
||||||
|
# Profile selected by the /p/<profile>/ URL prefix for the current request.
|
||||||
|
# Set by the profile-prefix middleware; read by handlers / _run_agent.
|
||||||
|
_api_request_profile: ContextVar[Optional[str]] = ContextVar(
|
||||||
|
"api_server_request_profile", default=None
|
||||||
|
)
|
||||||
|
|
||||||
def _approval_event_choices(*, smart_denied: bool, allow_permanent: bool) -> list[str]:
|
def _approval_event_choices(*, smart_denied: bool, allow_permanent: bool) -> list[str]:
|
||||||
if smart_denied:
|
if smart_denied:
|
||||||
return ["once", "deny"]
|
return ["once", "deny"]
|
||||||
|
|
@ -1228,6 +1247,117 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
status=401,
|
status=401,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Multi-profile multiplexing (/p/<profile>/…)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _resolve_request_profile(self, request: "web.Request"):
|
||||||
|
"""Resolve + validate the /p/<profile>/ URL prefix on an API request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- ``None`` when no profile prefix is present, or multiplexing is off
|
||||||
|
(the prefix is ignored; request handled as the default profile).
|
||||||
|
- the profile name (str) when present, multiplexing is on, and the
|
||||||
|
profile is one this gateway serves.
|
||||||
|
- ``_PROFILE_REJECTED`` when a prefix is present but the profile is
|
||||||
|
unknown/unconfigured (handler/middleware returns 404).
|
||||||
|
"""
|
||||||
|
profile = (request.match_info.get("profile") or "").strip()
|
||||||
|
if not profile:
|
||||||
|
return None
|
||||||
|
runner = getattr(self, "gateway_runner", None)
|
||||||
|
cfg = getattr(runner, "config", None)
|
||||||
|
if not getattr(cfg, "multiplex_profiles", False):
|
||||||
|
# Prefix supplied but multiplexing is off — ignore it, behave as
|
||||||
|
# the single-profile gateway (don't 404 a would-be valid route).
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from hermes_cli.profiles import profiles_to_serve
|
||||||
|
|
||||||
|
served = {name for name, _ in profiles_to_serve(multiplex=True)}
|
||||||
|
except Exception:
|
||||||
|
return _PROFILE_REJECTED
|
||||||
|
if profile not in served:
|
||||||
|
return _PROFILE_REJECTED
|
||||||
|
return profile
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _profile_scope(profile: Optional[str]):
|
||||||
|
"""Enter the multiplex profile runtime scope, or a no-op when unset."""
|
||||||
|
if not profile:
|
||||||
|
return nullcontext()
|
||||||
|
from gateway.run import _profile_runtime_scope
|
||||||
|
from hermes_cli.profiles import get_profile_dir
|
||||||
|
|
||||||
|
return _profile_runtime_scope(get_profile_dir(profile))
|
||||||
|
|
||||||
|
def _make_profile_prefix_middleware(self):
|
||||||
|
"""Reject unknown /p/<profile>/ prefixes and scope the request home."""
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def profile_prefix_middleware(request: "web.Request", handler):
|
||||||
|
profile = self._resolve_request_profile(request)
|
||||||
|
if profile is _PROFILE_REJECTED:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "Unknown or unconfigured profile"},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
token = _api_request_profile.set(profile)
|
||||||
|
try:
|
||||||
|
with self._profile_scope(profile):
|
||||||
|
return await handler(request)
|
||||||
|
finally:
|
||||||
|
_api_request_profile.reset(token)
|
||||||
|
|
||||||
|
return profile_prefix_middleware
|
||||||
|
|
||||||
|
def _http_route_table(self) -> List[tuple]:
|
||||||
|
"""Return (method, path, handler) rows registered by ``connect()``.
|
||||||
|
|
||||||
|
Kept as a method so multiplex tests can assert the /p/<profile>/
|
||||||
|
mirrors without starting a real aiohttp listener.
|
||||||
|
"""
|
||||||
|
routes: List[tuple] = [
|
||||||
|
("GET", "/health", self._handle_health),
|
||||||
|
("GET", "/health/detailed", self._handle_health_detailed),
|
||||||
|
("GET", "/v1/health", self._handle_health),
|
||||||
|
("GET", "/v1/models", self._handle_models),
|
||||||
|
("GET", "/v1/capabilities", self._handle_capabilities),
|
||||||
|
("GET", "/v1/skills", self._handle_skills),
|
||||||
|
("GET", "/v1/toolsets", self._handle_toolsets),
|
||||||
|
("GET", "/api/sessions", self._handle_list_sessions),
|
||||||
|
("POST", "/api/sessions", self._handle_create_session),
|
||||||
|
("GET", "/api/sessions/{session_id}", self._handle_get_session),
|
||||||
|
("PATCH", "/api/sessions/{session_id}", self._handle_patch_session),
|
||||||
|
("DELETE", "/api/sessions/{session_id}", self._handle_delete_session),
|
||||||
|
("GET", "/api/sessions/{session_id}/messages", self._handle_session_messages),
|
||||||
|
("POST", "/api/sessions/{session_id}/fork", self._handle_fork_session),
|
||||||
|
("POST", "/api/sessions/{session_id}/chat", self._handle_session_chat),
|
||||||
|
("POST", "/api/sessions/{session_id}/chat/stream", self._handle_session_chat_stream),
|
||||||
|
("POST", "/v1/chat/completions", self._handle_chat_completions),
|
||||||
|
("POST", "/v1/responses", self._handle_responses),
|
||||||
|
("GET", "/v1/responses/{response_id}", self._handle_get_response),
|
||||||
|
("DELETE", "/v1/responses/{response_id}", self._handle_delete_response),
|
||||||
|
("GET", "/api/jobs", self._handle_list_jobs),
|
||||||
|
("POST", "/api/jobs", self._handle_create_job),
|
||||||
|
("GET", "/api/jobs/{job_id}", self._handle_get_job),
|
||||||
|
("PATCH", "/api/jobs/{job_id}", self._handle_update_job),
|
||||||
|
("DELETE", "/api/jobs/{job_id}", self._handle_delete_job),
|
||||||
|
("POST", "/api/jobs/{job_id}/pause", self._handle_pause_job),
|
||||||
|
("POST", "/api/jobs/{job_id}/resume", self._handle_resume_job),
|
||||||
|
("POST", "/api/jobs/{job_id}/run", self._handle_run_job),
|
||||||
|
("POST", "/v1/runs", self._handle_runs),
|
||||||
|
("GET", "/v1/runs/{run_id}", self._handle_get_run),
|
||||||
|
("GET", "/v1/runs/{run_id}/events", self._handle_run_events),
|
||||||
|
("POST", "/v1/runs/{run_id}/approval", self._handle_run_approval),
|
||||||
|
("POST", "/v1/runs/{run_id}/stop", self._handle_stop_run),
|
||||||
|
]
|
||||||
|
if _CRON_AVAILABLE:
|
||||||
|
# Chronos managed-cron fire webhook (NAS → agent). Authenticated
|
||||||
|
# by a NAS-minted JWT (NOT API_SERVER_KEY).
|
||||||
|
routes.append(("POST", "/api/cron/fire", self._handle_cron_fire))
|
||||||
|
return routes
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Session header helpers
|
# Session header helpers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -1298,18 +1428,39 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _ensure_session_db(self):
|
def _ensure_session_db(self):
|
||||||
"""Lazily initialise and return the shared SessionDB instance.
|
"""Lazily initialise and return the SessionDB for the active profile home.
|
||||||
|
|
||||||
Sessions are persisted to ``state.db`` so that ``hermes sessions list``
|
Sessions are persisted to ``state.db`` so that ``hermes sessions list``
|
||||||
shows API-server conversations alongside CLI and gateway ones.
|
shows API-server conversations alongside CLI and gateway ones.
|
||||||
|
|
||||||
|
Under multiplex ``/p/<profile>/`` requests the profile runtime scope
|
||||||
|
redirects ``get_hermes_home()``, so each profile gets its own DB —
|
||||||
|
never the default profile's file.
|
||||||
"""
|
"""
|
||||||
if self._session_db is None:
|
# Explicit override (tests / manual wiring) wins. Production never sets
|
||||||
try:
|
# this externally, so the per-home cache below is the live path — and
|
||||||
from hermes_state import SessionDB
|
# we deliberately do NOT write back into ``self._session_db`` there, or
|
||||||
self._session_db = SessionDB()
|
# the first profile served would pin every later request to its DB.
|
||||||
except Exception as e:
|
if self._session_db is not None:
|
||||||
logger.debug("SessionDB unavailable for API server: %s", e)
|
return self._session_db
|
||||||
return self._session_db
|
try:
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
from hermes_state import SessionDB
|
||||||
|
|
||||||
|
home = get_hermes_home()
|
||||||
|
cache = getattr(self, "_session_dbs", None)
|
||||||
|
if cache is None:
|
||||||
|
cache = {}
|
||||||
|
self._session_dbs = cache
|
||||||
|
key = str(home)
|
||||||
|
db = cache.get(key)
|
||||||
|
if db is None:
|
||||||
|
db = SessionDB(db_path=home / "state.db")
|
||||||
|
cache[key] = db
|
||||||
|
return db
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("SessionDB unavailable for API server: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Agent creation helper
|
# Agent creation helper
|
||||||
|
|
@ -1586,20 +1737,32 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
})
|
})
|
||||||
|
|
||||||
async def _handle_models(self, request: "web.Request") -> "web.Response":
|
async def _handle_models(self, request: "web.Request") -> "web.Response":
|
||||||
"""GET /v1/models — list hermes-agent and any configured model_routes aliases."""
|
"""GET /v1/models — list hermes-agent and any configured model_routes aliases.
|
||||||
|
|
||||||
|
Under ``/p/<profile>/v1/models`` (multiplex on) the advertised primary
|
||||||
|
model id follows that profile's name/config, not the default adapter's
|
||||||
|
cached ``_model_name``.
|
||||||
|
"""
|
||||||
auth_err = self._check_auth(request)
|
auth_err = self._check_auth(request)
|
||||||
if auth_err:
|
if auth_err:
|
||||||
return auth_err
|
return auth_err
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
|
# Middleware already entered the profile runtime scope when a /p/
|
||||||
|
# prefix was present, so get_active_profile_name() resolves correctly.
|
||||||
|
model_name = (
|
||||||
|
self._resolve_model_name("")
|
||||||
|
if _api_request_profile.get()
|
||||||
|
else self._model_name
|
||||||
|
)
|
||||||
models = [
|
models = [
|
||||||
{
|
{
|
||||||
"id": self._model_name,
|
"id": model_name,
|
||||||
"object": "model",
|
"object": "model",
|
||||||
"created": now,
|
"created": now,
|
||||||
"owned_by": "hermes",
|
"owned_by": "hermes",
|
||||||
"permission": [],
|
"permission": [],
|
||||||
"root": self._model_name,
|
"root": model_name,
|
||||||
"parent": None,
|
"parent": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1607,7 +1770,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
# Only the alias and resolved model name are exposed — never provider
|
# Only the alias and resolved model name are exposed — never provider
|
||||||
# credentials.
|
# credentials.
|
||||||
for alias, route_cfg in self._model_routes.items():
|
for alias, route_cfg in self._model_routes.items():
|
||||||
if alias == self._model_name:
|
if alias == model_name:
|
||||||
continue # already listed above
|
continue # already listed above
|
||||||
models.append({
|
models.append({
|
||||||
"id": alias,
|
"id": alias,
|
||||||
|
|
@ -1616,7 +1779,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
"owned_by": "hermes",
|
"owned_by": "hermes",
|
||||||
"permission": [],
|
"permission": [],
|
||||||
"root": route_cfg.get("model", alias),
|
"root": route_cfg.get("model", alias),
|
||||||
"parent": self._model_name,
|
"parent": model_name,
|
||||||
})
|
})
|
||||||
|
|
||||||
return web.json_response({"object": "list", "data": models})
|
return web.json_response({"object": "list", "data": models})
|
||||||
|
|
@ -4223,48 +4386,53 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
another thread to stop in-progress LLM calls.
|
another thread to stop in-progress LLM calls.
|
||||||
"""
|
"""
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
# Capture before hopping to the executor — ContextVars do not follow
|
||||||
|
# run_in_executor threads, so the profile scope must be re-entered
|
||||||
|
# inside _run() from this explicit value.
|
||||||
|
request_profile = _api_request_profile.get()
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
from gateway.session_context import clear_session_vars
|
from gateway.session_context import clear_session_vars
|
||||||
|
|
||||||
tokens = self._bind_api_server_session(
|
with self._profile_scope(request_profile):
|
||||||
chat_id=session_id or "",
|
tokens = self._bind_api_server_session(
|
||||||
session_key=gateway_session_key or session_id or "",
|
chat_id=session_id or "",
|
||||||
session_id=session_id or "",
|
session_key=gateway_session_key or session_id or "",
|
||||||
)
|
session_id=session_id or "",
|
||||||
try:
|
|
||||||
agent = self._create_agent(
|
|
||||||
ephemeral_system_prompt=ephemeral_system_prompt,
|
|
||||||
session_id=session_id,
|
|
||||||
stream_delta_callback=stream_delta_callback,
|
|
||||||
tool_progress_callback=tool_progress_callback,
|
|
||||||
tool_start_callback=tool_start_callback,
|
|
||||||
tool_complete_callback=tool_complete_callback,
|
|
||||||
gateway_session_key=gateway_session_key,
|
|
||||||
route=route,
|
|
||||||
)
|
)
|
||||||
if agent_ref is not None:
|
try:
|
||||||
agent_ref[0] = agent
|
agent = self._create_agent(
|
||||||
effective_task_id = session_id or str(uuid.uuid4())
|
ephemeral_system_prompt=ephemeral_system_prompt,
|
||||||
result = agent.run_conversation(
|
session_id=session_id,
|
||||||
user_message=user_message,
|
stream_delta_callback=stream_delta_callback,
|
||||||
conversation_history=conversation_history,
|
tool_progress_callback=tool_progress_callback,
|
||||||
task_id=effective_task_id,
|
tool_start_callback=tool_start_callback,
|
||||||
)
|
tool_complete_callback=tool_complete_callback,
|
||||||
usage = {
|
gateway_session_key=gateway_session_key,
|
||||||
"input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
|
route=route,
|
||||||
"output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
|
)
|
||||||
"total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
|
if agent_ref is not None:
|
||||||
}
|
agent_ref[0] = agent
|
||||||
# Include the effective session ID in the result so callers
|
effective_task_id = session_id or str(uuid.uuid4())
|
||||||
# (e.g. X-Hermes-Session-Id header) can track compression-
|
result = agent.run_conversation(
|
||||||
# triggered session rotations. (#16938)
|
user_message=user_message,
|
||||||
_eff_sid = getattr(agent, "session_id", session_id)
|
conversation_history=conversation_history,
|
||||||
if isinstance(_eff_sid, str) and _eff_sid:
|
task_id=effective_task_id,
|
||||||
result["session_id"] = _eff_sid
|
)
|
||||||
return result, usage
|
usage = {
|
||||||
finally:
|
"input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
|
||||||
clear_session_vars(tokens)
|
"output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
|
||||||
|
"total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
|
||||||
|
}
|
||||||
|
# Include the effective session ID in the result so callers
|
||||||
|
# (e.g. X-Hermes-Session-Id header) can track compression-
|
||||||
|
# triggered session rotations. (#16938)
|
||||||
|
_eff_sid = getattr(agent, "session_id", session_id)
|
||||||
|
if isinstance(_eff_sid, str) and _eff_sid:
|
||||||
|
result["session_id"] = _eff_sid
|
||||||
|
return result, usage
|
||||||
|
finally:
|
||||||
|
clear_session_vars(tokens)
|
||||||
|
|
||||||
self._activate_admitted_request()
|
self._activate_admitted_request()
|
||||||
self._inflight_agent_runs += 1
|
self._inflight_agent_runs += 1
|
||||||
|
|
@ -4464,6 +4632,9 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
|
|
||||||
# Per-client model routing for /v1/runs (see model_routes).
|
# Per-client model routing for /v1/runs (see model_routes).
|
||||||
route = self._resolve_route(body.get("model"))
|
route = self._resolve_route(body.get("model"))
|
||||||
|
# Background task outlives the HTTP response (and thus the middleware
|
||||||
|
# profile scope). Capture now and re-enter inside the task/executor.
|
||||||
|
request_profile = _api_request_profile.get()
|
||||||
|
|
||||||
async def _run_and_close():
|
async def _run_and_close():
|
||||||
try:
|
try:
|
||||||
|
|
@ -4480,14 +4651,15 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
last_event="run.cancelled",
|
last_event="run.cancelled",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
agent = self._create_agent(
|
with self._profile_scope(request_profile):
|
||||||
ephemeral_system_prompt=ephemeral_system_prompt,
|
agent = self._create_agent(
|
||||||
session_id=session_id,
|
ephemeral_system_prompt=ephemeral_system_prompt,
|
||||||
stream_delta_callback=_text_cb,
|
session_id=session_id,
|
||||||
tool_progress_callback=event_cb,
|
stream_delta_callback=_text_cb,
|
||||||
gateway_session_key=gateway_session_key,
|
tool_progress_callback=event_cb,
|
||||||
route=route,
|
gateway_session_key=gateway_session_key,
|
||||||
)
|
route=route,
|
||||||
|
)
|
||||||
self._active_run_agents[run_id] = agent
|
self._active_run_agents[run_id] = agent
|
||||||
|
|
||||||
def _approval_notify(approval_data: Dict[str, Any]) -> None:
|
def _approval_notify(approval_data: Dict[str, Any]) -> None:
|
||||||
|
|
@ -4531,40 +4703,41 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
effective_task_id = session_id or run_id
|
effective_task_id = session_id or run_id
|
||||||
approval_token = None
|
approval_token = None
|
||||||
session_tokens = []
|
session_tokens = []
|
||||||
try:
|
with self._profile_scope(request_profile):
|
||||||
# Bind approval/session identity for this API run via
|
|
||||||
# contextvars so concurrent runs do not share process
|
|
||||||
# environment state.
|
|
||||||
approval_token = set_current_session_key(approval_session_key)
|
|
||||||
session_tokens = self._bind_api_server_session(
|
|
||||||
session_key=approval_session_key,
|
|
||||||
)
|
|
||||||
register_gateway_notify(approval_session_key, _approval_notify)
|
|
||||||
r = agent.run_conversation(
|
|
||||||
user_message=user_message,
|
|
||||||
conversation_history=conversation_history,
|
|
||||||
task_id=effective_task_id,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
try:
|
try:
|
||||||
unregister_gateway_notify(approval_session_key)
|
# Bind approval/session identity for this API run via
|
||||||
|
# contextvars so concurrent runs do not share process
|
||||||
|
# environment state.
|
||||||
|
approval_token = set_current_session_key(approval_session_key)
|
||||||
|
session_tokens = self._bind_api_server_session(
|
||||||
|
session_key=approval_session_key,
|
||||||
|
)
|
||||||
|
register_gateway_notify(approval_session_key, _approval_notify)
|
||||||
|
r = agent.run_conversation(
|
||||||
|
user_message=user_message,
|
||||||
|
conversation_history=conversation_history,
|
||||||
|
task_id=effective_task_id,
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
if approval_token is not None:
|
try:
|
||||||
try:
|
unregister_gateway_notify(approval_session_key)
|
||||||
reset_current_session_key(approval_token)
|
finally:
|
||||||
except Exception:
|
if approval_token is not None:
|
||||||
pass
|
try:
|
||||||
if session_tokens:
|
reset_current_session_key(approval_token)
|
||||||
try:
|
except Exception:
|
||||||
clear_session_vars(session_tokens)
|
pass
|
||||||
except Exception:
|
if session_tokens:
|
||||||
pass
|
try:
|
||||||
u = {
|
clear_session_vars(session_tokens)
|
||||||
"input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
|
except Exception:
|
||||||
"output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
|
pass
|
||||||
"total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
|
u = {
|
||||||
}
|
"input_tokens": getattr(agent, "session_prompt_tokens", 0) or 0,
|
||||||
return r, u
|
"output_tokens": getattr(agent, "session_completion_tokens", 0) or 0,
|
||||||
|
"total_tokens": getattr(agent, "session_total_tokens", 0) or 0,
|
||||||
|
}
|
||||||
|
return r, u
|
||||||
|
|
||||||
result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)
|
result, usage = await asyncio.get_running_loop().run_in_executor(None, _run_sync)
|
||||||
if run_id in self._stopping_run_ids:
|
if run_id in self._stopping_run_ids:
|
||||||
|
|
@ -4970,50 +5143,24 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mws = [mw for mw in (cors_middleware, body_limit_middleware, security_headers_middleware) if mw is not None]
|
mws = [
|
||||||
|
mw
|
||||||
|
for mw in (
|
||||||
|
self._make_profile_prefix_middleware(),
|
||||||
|
cors_middleware,
|
||||||
|
body_limit_middleware,
|
||||||
|
security_headers_middleware,
|
||||||
|
)
|
||||||
|
if mw is not None
|
||||||
|
]
|
||||||
self._app = web.Application(middlewares=mws, client_max_size=MAX_REQUEST_BYTES)
|
self._app = web.Application(middlewares=mws, client_max_size=MAX_REQUEST_BYTES)
|
||||||
assert self._app is not None
|
assert self._app is not None
|
||||||
self._app.router.add_get("/health", self._handle_health)
|
# Native routes + multiplex /p/<profile>/… mirrors. Same handlers;
|
||||||
self._app.router.add_get("/health/detailed", self._handle_health_detailed)
|
# the profile-prefix middleware validates the prefix and scopes
|
||||||
self._app.router.add_get("/v1/health", self._handle_health)
|
# config/credentials to that profile when multiplexing is on.
|
||||||
self._app.router.add_get("/v1/models", self._handle_models)
|
for method, path, handler in self._http_route_table():
|
||||||
self._app.router.add_get("/v1/capabilities", self._handle_capabilities)
|
self._app.router.add_route(method, path, handler)
|
||||||
self._app.router.add_get("/v1/skills", self._handle_skills)
|
self._app.router.add_route(method, f"/p/{{profile}}{path}", handler)
|
||||||
self._app.router.add_get("/v1/toolsets", self._handle_toolsets)
|
|
||||||
# Session/client control surface (thin wrappers over SessionDB + _run_agent)
|
|
||||||
self._app.router.add_get("/api/sessions", self._handle_list_sessions)
|
|
||||||
self._app.router.add_post("/api/sessions", self._handle_create_session)
|
|
||||||
self._app.router.add_get("/api/sessions/{session_id}", self._handle_get_session)
|
|
||||||
self._app.router.add_patch("/api/sessions/{session_id}", self._handle_patch_session)
|
|
||||||
self._app.router.add_delete("/api/sessions/{session_id}", self._handle_delete_session)
|
|
||||||
self._app.router.add_get("/api/sessions/{session_id}/messages", self._handle_session_messages)
|
|
||||||
self._app.router.add_post("/api/sessions/{session_id}/fork", self._handle_fork_session)
|
|
||||||
self._app.router.add_post("/api/sessions/{session_id}/chat", self._handle_session_chat)
|
|
||||||
self._app.router.add_post("/api/sessions/{session_id}/chat/stream", self._handle_session_chat_stream)
|
|
||||||
self._app.router.add_post("/v1/chat/completions", self._handle_chat_completions)
|
|
||||||
self._app.router.add_post("/v1/responses", self._handle_responses)
|
|
||||||
self._app.router.add_get("/v1/responses/{response_id}", self._handle_get_response)
|
|
||||||
self._app.router.add_delete("/v1/responses/{response_id}", self._handle_delete_response)
|
|
||||||
# Cron jobs management API
|
|
||||||
self._app.router.add_get("/api/jobs", self._handle_list_jobs)
|
|
||||||
self._app.router.add_post("/api/jobs", self._handle_create_job)
|
|
||||||
self._app.router.add_get("/api/jobs/{job_id}", self._handle_get_job)
|
|
||||||
self._app.router.add_patch("/api/jobs/{job_id}", self._handle_update_job)
|
|
||||||
self._app.router.add_delete("/api/jobs/{job_id}", self._handle_delete_job)
|
|
||||||
self._app.router.add_post("/api/jobs/{job_id}/pause", self._handle_pause_job)
|
|
||||||
self._app.router.add_post("/api/jobs/{job_id}/resume", self._handle_resume_job)
|
|
||||||
self._app.router.add_post("/api/jobs/{job_id}/run", self._handle_run_job)
|
|
||||||
|
|
||||||
# Chronos managed-cron fire webhook (NAS → agent). Authenticated by a
|
|
||||||
# NAS-minted JWT (NOT API_SERVER_KEY), so it has its own auth path.
|
|
||||||
if _CRON_AVAILABLE:
|
|
||||||
self._app.router.add_post("/api/cron/fire", self._handle_cron_fire)
|
|
||||||
# Structured event streaming
|
|
||||||
self._app.router.add_post("/v1/runs", self._handle_runs)
|
|
||||||
self._app.router.add_get("/v1/runs/{run_id}", self._handle_get_run)
|
|
||||||
self._app.router.add_get("/v1/runs/{run_id}/events", self._handle_run_events)
|
|
||||||
self._app.router.add_post("/v1/runs/{run_id}/approval", self._handle_run_approval)
|
|
||||||
self._app.router.add_post("/v1/runs/{run_id}/stop", self._handle_stop_run)
|
|
||||||
# Store the adapter after native routes are registered. Local Hermes-Relay
|
# Store the adapter after native routes are registered. Local Hermes-Relay
|
||||||
# bootstrap shims use this key as a feature-detection hook; registering
|
# bootstrap shims use this key as a feature-detection hook; registering
|
||||||
# native routes first lets those shims no-op instead of shadowing the
|
# native routes first lets those shims no-op instead of shadowing the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue