generated from coulomb/repo-seed
Add activity-core LLM endpoint support
This commit is contained in:
92
tests/test_activity_core_smoke.py
Normal file
92
tests/test_activity_core_smoke.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from llm_connect.adapter import MockLLMAdapter
|
||||
from llm_connect.models import RunConfig
|
||||
from llm_connect.profiles import CUSTODIAN_TRIAGE_BALANCED, ProfiledLLMAdapter, RuntimeProfile
|
||||
from llm_connect.server import LLMServer
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "smoke_activity_core_endpoint.py"
|
||||
FIXTURE_DIR = ROOT / "fixtures" / "activity_core"
|
||||
|
||||
|
||||
def _load_smoke_module():
|
||||
spec = importlib.util.spec_from_file_location("smoke_activity_core_endpoint", SCRIPT)
|
||||
assert spec is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_daily_triage_fixture_content_matches_schema():
|
||||
smoke = _load_smoke_module()
|
||||
schema = json.loads((FIXTURE_DIR / "daily-triage-report.schema.json").read_text())
|
||||
content = json.loads((FIXTURE_DIR / "daily-triage-valid-content.json").read_text())
|
||||
|
||||
assert smoke.validate_json_schema(content, schema) == []
|
||||
|
||||
|
||||
def test_daily_triage_execute_request_embeds_schema_and_profile_config():
|
||||
request = json.loads((FIXTURE_DIR / "daily-triage-execute-request.json").read_text())
|
||||
schema = json.loads((FIXTURE_DIR / "daily-triage-report.schema.json").read_text())
|
||||
config = request["config"]
|
||||
|
||||
assert request["prompt"]
|
||||
assert config["model_name"] == "custodian-triage-balanced"
|
||||
assert config["temperature"] == 0.2
|
||||
assert config["max_tokens"] == 1800
|
||||
assert config["max_depth"] == 2
|
||||
assert config["timeout_seconds"] == 300
|
||||
assert config["model_params"]["reasoning_effort"] == "medium"
|
||||
assert config["model_params"]["json_schema"] == schema
|
||||
|
||||
|
||||
def test_schema_validator_reports_missing_required_field():
|
||||
smoke = _load_smoke_module()
|
||||
schema = json.loads((FIXTURE_DIR / "daily-triage-report.schema.json").read_text())
|
||||
invalid = {"summary": "missing recommendations"}
|
||||
|
||||
errors = smoke.validate_json_schema(invalid, schema)
|
||||
|
||||
assert "$: missing required property 'recommendations'" in errors
|
||||
|
||||
|
||||
def test_run_smoke_against_profiled_mock_server():
|
||||
smoke = _load_smoke_module()
|
||||
valid_content = (FIXTURE_DIR / "daily-triage-valid-content.json").read_text()
|
||||
|
||||
def factory(provider: str, model: str) -> MockLLMAdapter:
|
||||
assert provider == "mock"
|
||||
assert model == "triage-model"
|
||||
return MockLLMAdapter(mock_response=valid_content)
|
||||
|
||||
adapter = ProfiledLLMAdapter(
|
||||
MockLLMAdapter(mock_response=valid_content),
|
||||
{
|
||||
CUSTODIAN_TRIAGE_BALANCED: RuntimeProfile(
|
||||
name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
provider="mock",
|
||||
model="triage-model",
|
||||
config=RunConfig(model_name="triage-model"),
|
||||
)
|
||||
},
|
||||
adapter_factory=factory,
|
||||
)
|
||||
server = LLMServer(adapter=adapter, port=0)
|
||||
server.start()
|
||||
try:
|
||||
result = smoke.run_smoke(
|
||||
base_url=f"http://127.0.0.1:{server.port}",
|
||||
request_path=FIXTURE_DIR / "daily-triage-execute-request.json",
|
||||
schema_path=FIXTURE_DIR / "daily-triage-report.schema.json",
|
||||
timeout=3,
|
||||
)
|
||||
finally:
|
||||
server.stop()
|
||||
|
||||
assert result["health"] == "ok"
|
||||
assert result["recommendations"] == 1
|
||||
@@ -48,3 +48,16 @@ def test_wp_0005_primitives_are_exported_from_package_root():
|
||||
for name in expected_names:
|
||||
assert hasattr(llm_connect, name)
|
||||
assert name in llm_connect.__all__
|
||||
|
||||
|
||||
def test_wp_0006_profile_primitives_are_exported_from_package_root():
|
||||
expected_names = [
|
||||
"CUSTODIAN_TRIAGE_BALANCED",
|
||||
"RuntimeProfile",
|
||||
"ProfiledLLMAdapter",
|
||||
"default_runtime_profiles",
|
||||
]
|
||||
|
||||
for name in expected_names:
|
||||
assert hasattr(llm_connect, name)
|
||||
assert name in llm_connect.__all__
|
||||
|
||||
143
tests/test_profiles.py
Normal file
143
tests/test_profiles.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_connect.adapter import MockLLMAdapter
|
||||
from llm_connect.exceptions import LLMConfigurationError
|
||||
from llm_connect.models import RunConfig
|
||||
from llm_connect.profiles import (
|
||||
CUSTODIAN_TRIAGE_BALANCED,
|
||||
ProfiledLLMAdapter,
|
||||
RuntimeProfile,
|
||||
default_runtime_profiles,
|
||||
)
|
||||
|
||||
|
||||
def test_profile_dispatch_merges_defaults_and_request_params():
|
||||
created: list[MockLLMAdapter] = []
|
||||
|
||||
def factory(provider: str, model: str) -> MockLLMAdapter:
|
||||
created.append(MockLLMAdapter(mock_response=f"{provider}:{model}"))
|
||||
return created[-1]
|
||||
|
||||
profile = RuntimeProfile(
|
||||
name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
provider="mock",
|
||||
model="triage-model",
|
||||
config=RunConfig(
|
||||
model_name="triage-model",
|
||||
temperature=0.2,
|
||||
max_tokens=1800,
|
||||
max_depth=2,
|
||||
timeout_seconds=300,
|
||||
model_params={"reasoning_effort": "medium"},
|
||||
),
|
||||
)
|
||||
adapter = ProfiledLLMAdapter(
|
||||
MockLLMAdapter(mock_response="default"),
|
||||
{profile.name: profile},
|
||||
adapter_factory=factory,
|
||||
)
|
||||
|
||||
response = adapter.execute_prompt(
|
||||
"Return JSON.",
|
||||
RunConfig(
|
||||
model_name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
model_params={"json_schema": {"type": "object"}},
|
||||
),
|
||||
)
|
||||
|
||||
assert response.model == "triage-model"
|
||||
assert response.metadata["profile"] == CUSTODIAN_TRIAGE_BALANCED
|
||||
assert response.metadata["profile_provider"] == "mock"
|
||||
assert len(created) == 1
|
||||
resolved = created[0].last_config
|
||||
assert resolved.model_name == "triage-model"
|
||||
assert resolved.temperature == 0.2
|
||||
assert resolved.max_tokens == 1800
|
||||
assert resolved.max_depth == 2
|
||||
assert resolved.model_params == {
|
||||
"reasoning_effort": "medium",
|
||||
"json_schema": {"type": "object"},
|
||||
}
|
||||
|
||||
|
||||
def test_profile_dispatch_preserves_explicit_request_scalars():
|
||||
created: list[MockLLMAdapter] = []
|
||||
|
||||
def factory(provider: str, model: str) -> MockLLMAdapter:
|
||||
created.append(MockLLMAdapter())
|
||||
return created[-1]
|
||||
|
||||
profile = RuntimeProfile(
|
||||
name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
provider="mock",
|
||||
model="triage-model",
|
||||
config=RunConfig(model_name="triage-model", temperature=0.2, max_tokens=1800),
|
||||
)
|
||||
adapter = ProfiledLLMAdapter(
|
||||
MockLLMAdapter(),
|
||||
{profile.name: profile},
|
||||
adapter_factory=factory,
|
||||
)
|
||||
|
||||
adapter.execute_prompt(
|
||||
"Prompt.",
|
||||
RunConfig(
|
||||
model_name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
temperature=0.4,
|
||||
max_tokens=123,
|
||||
),
|
||||
)
|
||||
|
||||
assert created[0].last_config.temperature == 0.4
|
||||
assert created[0].last_config.max_tokens == 123
|
||||
|
||||
|
||||
def test_non_profile_model_passes_through_to_default_adapter():
|
||||
default = MockLLMAdapter(mock_response="direct")
|
||||
adapter = ProfiledLLMAdapter(default, {})
|
||||
|
||||
response = adapter.execute_prompt("Prompt.", RunConfig(model_name="gpt-4"))
|
||||
|
||||
assert response.content == "direct"
|
||||
assert default.call_count == 1
|
||||
assert default.last_config.model_name == "gpt-4"
|
||||
|
||||
|
||||
def test_unknown_custodian_profile_fails_without_secret_context():
|
||||
adapter = ProfiledLLMAdapter(MockLLMAdapter(), {})
|
||||
|
||||
with pytest.raises(LLMConfigurationError) as excinfo:
|
||||
adapter.execute_prompt("Prompt.", RunConfig(model_name="custodian-missing"))
|
||||
|
||||
assert "Unknown LLM runtime profile" in str(excinfo.value)
|
||||
assert excinfo.value.context == {"profile": "custodian-missing"}
|
||||
|
||||
|
||||
def test_default_profiles_can_be_overridden_from_json_env(monkeypatch):
|
||||
monkeypatch.setenv(
|
||||
"LLM_CONNECT_PROFILES_JSON",
|
||||
json.dumps(
|
||||
{
|
||||
CUSTODIAN_TRIAGE_BALANCED: {
|
||||
"provider": "gemini",
|
||||
"model": "gemini-2.5-flash",
|
||||
"config": {
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 900,
|
||||
"model_params": {"reasoning_effort": "low"},
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
profiles = default_runtime_profiles(provider="mock", model="fallback")
|
||||
profile = profiles[CUSTODIAN_TRIAGE_BALANCED]
|
||||
|
||||
assert profile.provider == "gemini"
|
||||
assert profile.model == "gemini-2.5-flash"
|
||||
assert profile.config.temperature == 0.1
|
||||
assert profile.config.max_tokens == 900
|
||||
assert profile.config.model_params == {"reasoning_effort": "low"}
|
||||
@@ -17,7 +17,9 @@ from llm_connect._diagnostics import (
|
||||
record_provider_response,
|
||||
)
|
||||
from llm_connect.adapter import MockLLMAdapter, ErrorLLMAdapter
|
||||
from llm_connect.exceptions import LLMAPIError, LLMConfigurationError, LLMTimeoutError
|
||||
from llm_connect.models import LLMResponse, RunConfig
|
||||
from llm_connect.profiles import CUSTODIAN_TRIAGE_BALANCED, ProfiledLLMAdapter, RuntimeProfile
|
||||
from llm_connect.server import LLMServer
|
||||
|
||||
|
||||
@@ -151,7 +153,8 @@ class TestExecute:
|
||||
{"prompt": "hello"},
|
||||
)
|
||||
assert status == 500
|
||||
assert "boom" in body["error"]
|
||||
assert body["error"] == "internal_error"
|
||||
assert "boom" in body["message"]
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
@@ -189,6 +192,142 @@ class TestExecute:
|
||||
assert status == 400
|
||||
assert "config" in body["error"]
|
||||
|
||||
def test_profile_execute_resolves_model_and_metadata(self):
|
||||
created: list[MockLLMAdapter] = []
|
||||
|
||||
def factory(provider: str, model: str) -> MockLLMAdapter:
|
||||
created.append(MockLLMAdapter(mock_response="profile response"))
|
||||
return created[-1]
|
||||
|
||||
adapter = ProfiledLLMAdapter(
|
||||
MockLLMAdapter(mock_response="default"),
|
||||
{
|
||||
CUSTODIAN_TRIAGE_BALANCED: RuntimeProfile(
|
||||
name=CUSTODIAN_TRIAGE_BALANCED,
|
||||
provider="mock",
|
||||
model="triage-model",
|
||||
config=RunConfig(
|
||||
model_name="triage-model",
|
||||
temperature=0.2,
|
||||
max_tokens=1800,
|
||||
max_depth=2,
|
||||
model_params={"reasoning_effort": "medium"},
|
||||
),
|
||||
)
|
||||
},
|
||||
adapter_factory=factory,
|
||||
)
|
||||
s = LLMServer(adapter=adapter, port=0)
|
||||
s.start()
|
||||
try:
|
||||
status, body = _post(
|
||||
f"http://127.0.0.1:{s.port}/execute",
|
||||
{
|
||||
"prompt": "Return JSON.",
|
||||
"config": {
|
||||
"model_name": CUSTODIAN_TRIAGE_BALANCED,
|
||||
"model_params": {"json_schema": {"type": "object"}},
|
||||
},
|
||||
},
|
||||
)
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
assert status == 200
|
||||
assert body["model"] == "triage-model"
|
||||
assert body["metadata"]["profile"] == CUSTODIAN_TRIAGE_BALANCED
|
||||
assert body["metadata"]["profile_provider"] == "mock"
|
||||
assert len(created) == 1
|
||||
assert created[0].last_config.model_name == "triage-model"
|
||||
assert created[0].last_config.temperature == 0.2
|
||||
assert created[0].last_config.max_tokens == 1800
|
||||
assert created[0].last_config.max_depth == 2
|
||||
assert created[0].last_config.model_params == {
|
||||
"reasoning_effort": "medium",
|
||||
"json_schema": {"type": "object"},
|
||||
}
|
||||
|
||||
def test_unknown_profile_returns_400(self):
|
||||
s = LLMServer(adapter=ProfiledLLMAdapter(MockLLMAdapter(), {}), port=0)
|
||||
s.start()
|
||||
try:
|
||||
status, body = _post(
|
||||
f"http://127.0.0.1:{s.port}/execute",
|
||||
{"prompt": "hello", "config": {"model_name": "custodian-missing"}},
|
||||
)
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
assert status == 400
|
||||
assert body["error"] == "unknown_profile"
|
||||
assert body["context"]["profile"] == "custodian-missing"
|
||||
|
||||
def test_configuration_error_is_sanitized(self):
|
||||
class SecretConfigAdapter(MockLLMAdapter):
|
||||
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
||||
raise LLMConfigurationError(
|
||||
"Bad api_key=sk-supersecret with Bearer secret-token",
|
||||
context={"api_key": "sk-supersecret", "provider": "openai"},
|
||||
)
|
||||
|
||||
s = LLMServer(adapter=SecretConfigAdapter(), port=0)
|
||||
s.start()
|
||||
try:
|
||||
status, body = _post(
|
||||
f"http://127.0.0.1:{s.port}/execute",
|
||||
{"prompt": "hello"},
|
||||
)
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
assert status == 500
|
||||
assert body["error"] == "configuration_error"
|
||||
assert "sk-supersecret" not in json.dumps(body)
|
||||
assert "secret-token" not in json.dumps(body)
|
||||
assert body["context"]["api_key"] == "<redacted>"
|
||||
assert body["context"]["provider"] == "openai"
|
||||
|
||||
def test_provider_errors_are_categorized_and_sanitized(self):
|
||||
class ProviderErrorAdapter(MockLLMAdapter):
|
||||
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
||||
raise LLMAPIError(
|
||||
"HTTP 500 from https://provider.example/v1?key=gemini-secret",
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
s = LLMServer(adapter=ProviderErrorAdapter(), port=0)
|
||||
s.start()
|
||||
try:
|
||||
status, body = _post(
|
||||
f"http://127.0.0.1:{s.port}/execute",
|
||||
{"prompt": "hello"},
|
||||
)
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
assert status == 502
|
||||
assert body["error"] == "provider_api_error"
|
||||
assert body["provider_status"] == 500
|
||||
assert "gemini-secret" not in body["message"]
|
||||
|
||||
def test_timeout_error_returns_504(self):
|
||||
class TimeoutAdapter(MockLLMAdapter):
|
||||
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
|
||||
raise LLMTimeoutError("Request timed out after 300s")
|
||||
|
||||
s = LLMServer(adapter=TimeoutAdapter(), port=0)
|
||||
s.start()
|
||||
try:
|
||||
status, body = _post(
|
||||
f"http://127.0.0.1:{s.port}/execute",
|
||||
{"prompt": "hello"},
|
||||
)
|
||||
finally:
|
||||
s.stop()
|
||||
|
||||
assert status == 504
|
||||
assert body["error"] == "provider_timeout"
|
||||
|
||||
def test_debug_query_returns_diagnostics(self):
|
||||
s = LLMServer(adapter=DiagnosticLLMAdapter(mock_response="debug body"), port=0)
|
||||
s.start()
|
||||
|
||||
Reference in New Issue
Block a user