Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
@@ -294,6 +294,40 @@ class TestModelsEndpoint:
|
||||
assert data["data"][0]["id"] == "hermes-agent"
|
||||
assert data["data"][0]["owned_by"] == "hermes"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_models_returns_profile_name(self):
|
||||
"""When running under a named profile, /v1/models advertises the profile name."""
|
||||
with patch("gateway.platforms.api_server.APIServerAdapter._resolve_model_name", return_value="lucas"):
|
||||
adapter = _make_adapter()
|
||||
app = _create_app(adapter)
|
||||
async with TestClient(TestServer(app)) as cli:
|
||||
resp = await cli.get("/v1/models")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert data["data"][0]["id"] == "lucas"
|
||||
assert data["data"][0]["root"] == "lucas"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_models_returns_explicit_model_name(self):
|
||||
"""Explicit model_name in config overrides profile name."""
|
||||
extra = {"model_name": "my-custom-agent"}
|
||||
config = PlatformConfig(enabled=True, extra=extra)
|
||||
adapter = APIServerAdapter(config)
|
||||
assert adapter._model_name == "my-custom-agent"
|
||||
|
||||
def test_resolve_model_name_explicit(self):
|
||||
assert APIServerAdapter._resolve_model_name("my-bot") == "my-bot"
|
||||
|
||||
def test_resolve_model_name_default_profile(self):
|
||||
"""Default profile falls back to 'hermes-agent'."""
|
||||
with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"):
|
||||
assert APIServerAdapter._resolve_model_name("") == "hermes-agent"
|
||||
|
||||
def test_resolve_model_name_named_profile(self):
|
||||
"""Named profile uses the profile name as model name."""
|
||||
with patch("hermes_cli.profiles.get_active_profile_name", return_value="lucas"):
|
||||
assert APIServerAdapter._resolve_model_name("") == "lucas"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_models_requires_auth(self, auth_adapter):
|
||||
app = _create_app(auth_adapter)
|
||||
|
||||
@@ -1949,6 +1949,68 @@ class TestRunConversation:
|
||||
assert result["final_response"] is not None
|
||||
assert "Thinking Budget Exhausted" in result["final_response"]
|
||||
|
||||
def test_length_with_tool_calls_returns_partial_without_executing_tools(self, agent):
|
||||
self._setup_agent(agent)
|
||||
bad_tc = _mock_tool_call(
|
||||
name="write_file",
|
||||
arguments='{"path":"report.md","content":"partial',
|
||||
call_id="c1",
|
||||
)
|
||||
resp = _mock_response(content="", finish_reason="length", tool_calls=[bad_tc])
|
||||
agent.client.chat.completions.create.return_value = resp
|
||||
|
||||
with (
|
||||
patch("run_agent.handle_function_call") as mock_handle_function_call,
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
):
|
||||
result = agent.run_conversation("write the report")
|
||||
|
||||
assert result["completed"] is False
|
||||
assert result["partial"] is True
|
||||
assert "truncated due to output length limit" in result["error"]
|
||||
mock_handle_function_call.assert_not_called()
|
||||
|
||||
def test_truncated_tool_call_retries_once_before_refusing(self, agent):
|
||||
"""When tool call args are truncated, the agent retries the API call
|
||||
once. If the retry succeeds (valid JSON args), tool execution proceeds."""
|
||||
self._setup_agent(agent)
|
||||
agent.valid_tool_names.add("write_file")
|
||||
bad_tc = _mock_tool_call(
|
||||
name="write_file",
|
||||
arguments='{"path":"report.md","content":"partial',
|
||||
call_id="c1",
|
||||
)
|
||||
truncated_resp = _mock_response(
|
||||
content="", finish_reason="length", tool_calls=[bad_tc],
|
||||
)
|
||||
good_tc = _mock_tool_call(
|
||||
name="write_file",
|
||||
arguments='{"path":"report.md","content":"full content"}',
|
||||
call_id="c2",
|
||||
)
|
||||
good_resp = _mock_response(
|
||||
content="", finish_reason="stop", tool_calls=[good_tc],
|
||||
)
|
||||
with (
|
||||
patch("run_agent.handle_function_call", return_value='{"success":true}') as mock_hfc,
|
||||
patch.object(agent, "_persist_session"),
|
||||
patch.object(agent, "_save_trajectory"),
|
||||
patch.object(agent, "_cleanup_task_resources"),
|
||||
):
|
||||
# First call: truncated → retry. Second: valid → execute tool.
|
||||
# Third: final text response.
|
||||
final_resp = _mock_response(content="Done!", finish_reason="stop")
|
||||
agent.client.chat.completions.create.side_effect = [
|
||||
truncated_resp, good_resp, final_resp,
|
||||
]
|
||||
result = agent.run_conversation("write the report")
|
||||
|
||||
# Tool was executed on the retry (good_resp)
|
||||
mock_hfc.assert_called_once()
|
||||
assert result["final_response"] == "Done!"
|
||||
|
||||
|
||||
class TestRetryExhaustion:
|
||||
"""Regression: retry_count > max_retries was dead code (off-by-one).
|
||||
@@ -3082,6 +3144,20 @@ class TestStreamingApiCall:
|
||||
assert tc[0].function.name == "search"
|
||||
assert tc[1].function.name == "read"
|
||||
|
||||
def test_truncated_tool_call_args_upgrade_finish_reason_to_length(self, agent):
|
||||
chunks = [
|
||||
_make_chunk(tool_calls=[_make_tc_delta(0, "call_1", "write_file", '{"path":"x.txt","content":"hel')]),
|
||||
]
|
||||
agent.client.chat.completions.create.return_value = iter(chunks)
|
||||
|
||||
resp = agent._interruptible_streaming_api_call({"messages": []})
|
||||
|
||||
tc = resp.choices[0].message.tool_calls
|
||||
assert len(tc) == 1
|
||||
assert tc[0].function.name == "write_file"
|
||||
assert tc[0].function.arguments == '{"path":"x.txt","content":"hel'
|
||||
assert resp.choices[0].finish_reason == "length"
|
||||
|
||||
def test_ollama_reused_index_separate_tool_calls(self, agent):
|
||||
"""Ollama sends every tool call at index 0 with different ids.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user