mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
feat(api-server): configurable concurrent-run cap to prevent DoS (#50007)
The OpenAI-compatible API server only enforced a hardcoded cap of 10 concurrent runs on /v1/runs, leaving /v1/chat/completions and /v1/responses unbounded — a request flood could exhaust CPU, memory, and upstream LLM quota (#7483). - Add gateway.api_server.max_concurrent_runs (config.yaml, default 10, 0 disables). No env var. - Shared concurrency gate across all three agent-serving endpoints, counting both the chat/responses in-flight counter and the /v1/runs stream set. Returns OpenAI-style 429 + Retry-After when at the cap. - Remove the dead hardcoded _MAX_CONCURRENT_RUNS class attribute. Closes #7483.
This commit is contained in:
parent
99233faf78
commit
e499d69e3e
3 changed files with 147 additions and 8 deletions
|
|
@ -420,6 +420,63 @@ class TestAuth:
|
|||
assert result.status == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Concurrency cap (gateway.api_server.max_concurrent_runs) — #7483
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestConcurrencyCap:
|
||||
def test_resolve_defaults_to_10_when_unset(self):
|
||||
with patch("hermes_cli.config.load_config", return_value={}):
|
||||
assert APIServerAdapter._resolve_max_concurrent_runs() == 10
|
||||
|
||||
def test_resolve_reads_config_value(self):
|
||||
cfg = {"gateway": {"api_server": {"max_concurrent_runs": 3}}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
assert APIServerAdapter._resolve_max_concurrent_runs() == 3
|
||||
|
||||
def test_resolve_clamps_negative_to_zero(self):
|
||||
cfg = {"gateway": {"api_server": {"max_concurrent_runs": -5}}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
assert APIServerAdapter._resolve_max_concurrent_runs() == 0
|
||||
|
||||
def test_resolve_malformed_falls_back_to_default(self):
|
||||
cfg = {"gateway": {"api_server": {"max_concurrent_runs": "not-an-int"}}}
|
||||
with patch("hermes_cli.config.load_config", return_value=cfg):
|
||||
assert APIServerAdapter._resolve_max_concurrent_runs() == 10
|
||||
|
||||
def test_under_cap_returns_none(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._max_concurrent_runs = 5
|
||||
adapter._inflight_agent_runs = 2
|
||||
assert adapter._concurrency_limited_response() is None
|
||||
|
||||
def test_at_cap_returns_429_with_retry_after(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._max_concurrent_runs = 3
|
||||
adapter._inflight_agent_runs = 3
|
||||
resp = adapter._concurrency_limited_response()
|
||||
assert resp is not None
|
||||
assert resp.status == 429
|
||||
assert resp.headers.get("Retry-After")
|
||||
|
||||
def test_cap_counts_both_buckets(self):
|
||||
# /v1/runs (tracked by _run_streams) + chat/responses (inflight)
|
||||
adapter = _make_adapter()
|
||||
adapter._max_concurrent_runs = 4
|
||||
adapter._inflight_agent_runs = 2
|
||||
adapter._run_streams = {"r1": object(), "r2": object()}
|
||||
resp = adapter._concurrency_limited_response()
|
||||
assert resp is not None
|
||||
assert resp.status == 429
|
||||
|
||||
def test_zero_disables_cap(self):
|
||||
adapter = _make_adapter()
|
||||
adapter._max_concurrent_runs = 0
|
||||
adapter._inflight_agent_runs = 9999
|
||||
assert adapter._concurrency_limited_response() is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for HTTP tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue