diff --git a/app/features/demo/pipeline.py b/app/features/demo/pipeline.py index 3eb64533..041d5361 100644 --- a/app/features/demo/pipeline.py +++ b/app/features/demo/pipeline.py @@ -280,15 +280,21 @@ def _model_config_payload(model_type: str) -> dict[str, Any]: def _llm_key_present() -> bool: - """Return True when the configured agent model's provider API key is set. + """Return True when the configured agent model's provider can be used. Matches the provider prefix of ``agent_default_model`` so the agent step skips gracefully when its provider is unreachable. Logs key PRESENCE only, never the value (port of run_demo.py:317-335; see security-patterns.md). + + The local ``ollama`` provider needs no API key (#340), so it always returns + True — the agent step still degrades gracefully if Ollama is unreachable + (the chat round-trip fails and the step skips via its error path). """ settings = get_settings() model = settings.agent_default_model provider = model.split(":", 1)[0] if ":" in model else "" + if provider == "ollama": + return True if provider == "anthropic": return bool(settings.anthropic_api_key) if provider == "openai": diff --git a/app/features/demo/tests/test_pipeline.py b/app/features/demo/tests/test_pipeline.py index 971e1dd7..5f73a8c8 100644 --- a/app/features/demo/tests/test_pipeline.py +++ b/app/features/demo/tests/test_pipeline.py @@ -1801,6 +1801,41 @@ async def request( return _HitlClient(event_sink=intermediate), intermediate +def test_llm_key_present_ollama_needs_no_key(monkeypatch): + """#340 — the local ollama provider needs no API key, so the gate is True. + + Without this, a local-Ollama stack (agent_default_model=ollama:*) makes the + showcase agent_hitl_flow / agent steps skip with "no API key matching + agent_default_model provider" even though Ollama is reachable. + """ + monkeypatch.setattr( + pipeline, + "get_settings", + lambda: SimpleNamespace( + agent_default_model="ollama:qwen3:8b", + anthropic_api_key="", + openai_api_key="", + google_api_key="", + ), + ) + assert pipeline._llm_key_present() is True + + +def test_llm_key_present_cloud_still_requires_key(monkeypatch): + """Regression guard for #340 — a cloud provider still requires its key.""" + monkeypatch.setattr( + pipeline, + "get_settings", + lambda: SimpleNamespace( + agent_default_model="openai:gpt-4.1-mini", + anthropic_api_key="", + openai_api_key="", + google_api_key="", + ), + ) + assert pipeline._llm_key_present() is False + + async def test_agent_hitl_flow_happy_path(monkeypatch, tmp_path): """PRP-41 — full HITL round-trip: chat -> intermediate -> approve -> pass.""" monkeypatch.setattr(