Add activity-core LLM endpoint support
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled

This commit is contained in:
2026-06-07 19:24:45 +02:00
parent 1d9fc107ed
commit 14ba47c129
25 changed files with 2082 additions and 18 deletions

View 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

View File

@@ -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
View 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"}

View File

@@ -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()