diff --git a/engine/codeflow_engine/actions/quality_engine/ai/ai_analyzer.py b/engine/codeflow_engine/actions/quality_engine/ai/ai_analyzer.py index b4c2368..5a970d8 100644 --- a/engine/codeflow_engine/actions/quality_engine/ai/ai_analyzer.py +++ b/engine/codeflow_engine/actions/quality_engine/ai/ai_analyzer.py @@ -135,8 +135,6 @@ async def analyze_code( ], "temperature": 0.3, # Lower temperature for more deterministic responses "max_tokens": 2000, - # ✅ Validated and enforced JSON output per provider - "response_format": {"type": "json"}, } if provider_name: request["provider"] = provider_name @@ -149,7 +147,7 @@ async def analyze_code( file_path=file_path, provider=provider_name or "default", ) - response = self.llm_manager.complete(request) + response = await asyncio.to_thread(self.llm_manager.complete, request) # LLMResponse is a dataclass and so always truthy; a failed call carries # `error` with empty content. Checking truthiness alone would send "" on diff --git a/engine/codeflow_engine/actions/quality_engine/ai/ai_handler.py b/engine/codeflow_engine/actions/quality_engine/ai/ai_handler.py index 5aa6323..aac64ac 100644 --- a/engine/codeflow_engine/actions/quality_engine/ai/ai_handler.py +++ b/engine/codeflow_engine/actions/quality_engine/ai/ai_handler.py @@ -5,12 +5,16 @@ """ import time -from typing import Any +from typing import TYPE_CHECKING, Any import structlog from codeflow_engine.actions.quality_engine.models import ToolResult + +if TYPE_CHECKING: + from codeflow_engine.actions.llm.manager import ActionLLMProviderManager + logger = structlog.get_logger(__name__) @@ -206,7 +210,7 @@ async def run_ai_analysis( return None -async def initialize_llm_manager() -> Any | None: +async def initialize_llm_manager() -> "ActionLLMProviderManager | None": """Initialize the LLM manager for AI analysis. Delegates to the quality engine's single initializer rather than building a diff --git a/engine/codeflow_engine/actions/quality_engine/ai/ai_modes.py b/engine/codeflow_engine/actions/quality_engine/ai/ai_modes.py index 8ebc039..21eefc2 100644 --- a/engine/codeflow_engine/actions/quality_engine/ai/ai_modes.py +++ b/engine/codeflow_engine/actions/quality_engine/ai/ai_modes.py @@ -5,8 +5,9 @@ integrating with the CodeFlow LLM provider system. """ -import json +import asyncio from enum import StrEnum +import json from pathlib import Path from typing import Any @@ -18,6 +19,7 @@ from codeflow_engine.actions.quality_engine.models import ToolResult from codeflow_engine.agents.models import CodeIssue + logger = structlog.get_logger(__name__) # System prompt templates @@ -167,17 +169,19 @@ async def run_ai_analysis( {"role": "user", "content": analysis_prompt}, ], "temperature": 0.1, - "response_format": {"type": "json_object"}, } if provider_name: request["provider"] = provider_name if model: request["model"] = model - response = llm_manager.complete(request) + response = await asyncio.to_thread(llm_manager.complete, request) if not response or not response.content: - logger.warning("No response received from LLM") + logger.warning( + "No response received from LLM", + error=getattr(response, "error", None), + ) return None # Report what was actually used, not what was asked for — when either was diff --git a/engine/tests/test_sluice_request_metadata.py b/engine/tests/test_sluice_request_metadata.py index 725ac57..e7f4c36 100644 --- a/engine/tests/test_sluice_request_metadata.py +++ b/engine/tests/test_sluice_request_metadata.py @@ -1,6 +1,6 @@ """Guards the Sluice request-metadata contract (ADR 10, raised to MUST by ADR 17). -These tests assert the *contract* — what the gateway requires — not what the code +These tests assert the *contract* — what the gateway requires — not what the code currently happens to send. That distinction is the reason this file exists. The same contract was silently violated for months in house-of-veritas: the call @@ -12,7 +12,7 @@ So: every assertion below is written against `phoenixvc/sluice` docs/architecture/17-mandatory-request-metadata.md. If an assertion here ever disagrees with the implementation, the implementation is what -moves — unless the ADR itself changed, in which case update the citation too. +moves — unless the ADR itself changed, in which case update the citation too. codeflow-engine's virtual key carries no `use:` in the gateway's keys.yaml, which classifies it as a *service*: enforced, not exempt. @@ -40,7 +40,7 @@ from codeflow_engine.core.llm.sluice_provider import SluiceProvider # ADR 10 naming rule, unchanged by ADR 17: `app` and `agent` become Prometheus label -# values, so they must be lowercase kebab-case — stable, one token per dimension. +# values, so they must be lowercase kebab-case — stable, one token per dimension. KEBAB_CASE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") SLUICE_BASE_URL = "https://litellm.sluice.example" @@ -68,7 +68,7 @@ def recorder(monkeypatch: pytest.MonkeyPatch) -> _CompletionsRecorder: """Install a fake `openai` module so tests assert on the request, not the network. Stubbing at the module boundary rather than patching the provider's `client` - attribute keeps `_initialize_client` in the code path — that is where the + attribute keeps `_initialize_client` in the code path — that is where the base_url and api_key actually get bound. """ completions = _CompletionsRecorder() @@ -144,7 +144,7 @@ def test_does_not_send_superseded_field_names( class TestNamingRules: - """ADR 10 naming rules — violations are counted, not rejected, so nothing else catches them.""" + """ADR 10 naming rules — violations are counted, not rejected, so nothing else catches them.""" def test_prometheus_labelled_fields_are_kebab_case( self, recorder: _CompletionsRecorder, sluice_env: None @@ -199,7 +199,7 @@ def test_known_literal_strings_still_resolve(self) -> None: class TestUntaggedTrafficIsRefused: - """An untagged request is accepted by the gateway today — local failure is the only signal.""" + """An untagged request is accepted by the gateway today — local failure is the only signal.""" def test_sluice_route_without_an_agent_refuses_to_send( self, recorder: _CompletionsRecorder, sluice_env: None @@ -300,7 +300,7 @@ def test_guard_ignores_path_and_trailing_slash_differences( ) -> None: from codeflow_engine.actions.llm.providers import OpenAIProvider - # Same host, cosmetically different URL — must not slip past the guard. + # Same host, cosmetically different URL — must not slip past the guard. with pytest.raises(SluiceMetadataError): OpenAIProvider( {"api_key": "sk-test", "base_url": f"{SLUICE_BASE_URL}/v1/"} @@ -848,6 +848,7 @@ async def test_analysis_requests_are_tagged( "app": "codeflow-engine", "agent": "quality-analyzer", } + assert "response_format" not in recorder.calls[0] @pytest.mark.asyncio async def test_does_not_ask_the_gateway_for_a_vendor_model( @@ -970,3 +971,4 @@ async def test_the_code_analyzer_shares_the_same_tag( await AICodeAnalyzer(manager).analyze_code("sample.py", "x = 1\n") assert sent_metadata(recorder)["agent"] == "quality-analyzer" + assert "response_format" not in recorder.calls[0]