diff --git a/pyproject.toml b/pyproject.toml index b06371d..63266ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -187,19 +187,13 @@ ignore = [ [tool.ruff.lint.per-file-ignores] # Lazy imports inside functions to avoid circular dependency with -# strix.telemetry / strix.llm.dedupe / cvss. +# strix.telemetry / strix.report.dedupe / cvss. "strix/tools/notes/tools.py" = ["PLC0415", "TC002"] "strix/tools/finish/tool.py" = ["PLC0415", "TC002"] "strix/tools/reporting/tool.py" = ["PLC0415", "TC002"] "strix/tools/**/*.py" = [ "ARG001", # Unused function argument (tools may have unused args for interface consistency) ] -# Anthropic cache wrapper inherits from LitellmModel; signature shadows builtin `input`. -"strix/llm/anthropic_cache_wrapper.py" = [ - "A002", # Argument shadows builtin (parent signature uses `input`) - "TC002", # Many SDK types are imports-for-annotations only - "TC003", # collections.abc.AsyncIterator imported for return type -] # Custom Docker subclass duplicates parent body; some imports are for annotations. # Backend factories import their backend's deps lazily so deployments # that pick a different backend don't need every backend's libs installed. @@ -208,9 +202,6 @@ ignore = [ "TC002", # Manifest, Container imported for annotations "TC003", # uuid imported for annotation ] -"strix/llm/multi_provider_setup.py" = [ - "TC002", # Model, ModelProvider imported for annotations -] # SDK function-tool wrappers: the SDK calls get_type_hints() at registration # time to derive the JSON schema, which evaluates annotations at runtime — # so RunContextWrapper / Tool / TResponseInputItem must be imported eagerly, diff --git a/strix.spec b/strix.spec index 445988e..7f5d2ed 100644 --- a/strix.spec +++ b/strix.spec @@ -121,15 +121,13 @@ hiddenimports = [ 'strix.agents', 'strix.agents.factory', 'strix.agents.prompt', - 'strix.llm', - 'strix.llm.anthropic_cache_wrapper', - 'strix.llm.dedupe', - 'strix.llm.multi_provider_setup', - 'strix.llm.retry', + 'strix.config.models', 'strix.orchestration', 'strix.orchestration.coordinator', 'strix.orchestration.runner', 'strix.orchestration.utils', + 'strix.report', + 'strix.report.dedupe', 'strix.runtime', 'strix.runtime.backends', 'strix.runtime.caido_bootstrap', diff --git a/strix/agents/factory.py b/strix/agents/factory.py index 856b2ab..06493c3 100644 --- a/strix/agents/factory.py +++ b/strix/agents/factory.py @@ -242,7 +242,7 @@ def build_strix_agent( # looping through think/list_todos forever. reset_tool_choice=interactive, # model=None so ``RunConfig.model`` drives provider selection - # via :func:`build_multi_provider` rather than the SDK's default. + # through the SDK's default MultiProvider. model=None, capabilities=[Filesystem(), Shell()], ) diff --git a/strix/config/models.py b/strix/config/models.py new file mode 100644 index 0000000..04c78e2 --- /dev/null +++ b/strix/config/models.py @@ -0,0 +1,98 @@ +"""SDK model configuration helpers. + +This module is intentionally not a provider abstraction. Strix accepts +friendly model names at the config boundary, normalizes them to the +OpenAI Agents SDK's native model ids, then lets the SDK's default +``MultiProvider`` do the actual routing. +""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from agents import set_default_openai_api, set_default_openai_key +from agents.retry import ( + ModelRetryBackoffSettings, + ModelRetrySettings, + retry_policies, +) + + +if TYPE_CHECKING: + from strix.config.settings import Settings + + +_SDK_PREFIXES = {"any-llm", "litellm", "openai"} + + +DEFAULT_MODEL_RETRY = ModelRetrySettings( + max_retries=5, + backoff=ModelRetryBackoffSettings( + initial_delay=2.0, + max_delay=90.0, + multiplier=2.0, + jitter=False, + ), + policy=retry_policies.any( + retry_policies.provider_suggested(), + retry_policies.network_error(), + retry_policies.http_status((429, 500, 502, 503, 504)), + ), +) + + +def configure_sdk_model_defaults(settings: Settings) -> None: + """Apply Strix config to SDK-native defaults. + + OpenAI-compatible base URLs are handled by the SDK OpenAI provider. + Non-OpenAI providers should use the SDK's native ``litellm/`` or + ``any-llm/`` routing, produced by :func:`normalize_model_name`. + """ + llm = settings.llm + _configure_litellm_compatibility() + if llm.api_key: + set_default_openai_key(llm.api_key, use_for_tracing=False) + _configure_litellm_default("api_key", llm.api_key) + if llm.api_base: + os.environ["OPENAI_BASE_URL"] = llm.api_base + _configure_litellm_default("api_base", llm.api_base) + set_default_openai_api("chat_completions") + else: + set_default_openai_api("responses") + + +def _configure_litellm_compatibility() -> None: + """Match the permissive LiteLLM behavior used by the pre-SDK harness.""" + import litellm + + litellm.drop_params = True + litellm.modify_params = True + + +def _configure_litellm_default(name: str, value: str) -> None: + """Set LiteLLM's module-level defaults without adding a provider wrapper.""" + import litellm + + setattr(litellm, name, value) + + +def normalize_model_name(model_name: str) -> str: + """Normalize friendly Strix model names to SDK-native model ids.""" + model = model_name.strip() + if not model: + return model + + if "/" in model: + prefix = model.split("/", 1)[0].lower() + if prefix in _SDK_PREFIXES: + return model + return f"litellm/{model}" + + lower = model.lower() + if lower.startswith("claude"): + return f"litellm/anthropic/{model}" + if lower.startswith("gemini"): + return f"litellm/gemini/{model}" + + return model diff --git a/strix/config/settings.py b/strix/config/settings.py index 1cfffc4..fea3ce1 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -10,8 +10,8 @@ Bool fields auto-parse ``"0"``/``"false"``/``"no"``/``"off"`` as falsy and any other non-empty string as truthy. Int fields auto-coerce from string env. The ``api_base`` field walks an alias chain so users can point at any OpenAI-compatible endpoint via whichever env name they -prefer (``LLM_API_BASE`` / ``OPENAI_API_BASE`` / ``LITELLM_BASE_URL`` / -``OLLAMA_API_BASE``). +prefer (``LLM_API_BASE`` / ``OPENAI_API_BASE`` / ``OPENAI_BASE_URL`` / +``LITELLM_BASE_URL`` / ``OLLAMA_API_BASE``). Each sub-model is a :class:`BaseSettings` so it reads env independently — the alternative (one mega-BaseSettings with flat fields) would lose @@ -41,12 +41,16 @@ class LlmSettings(BaseSettings): model_config = _BASE_CONFIG model: str | None = Field(default=None, alias="STRIX_LLM") - api_key: str | None = Field(default=None, alias="LLM_API_KEY") + api_key: str | None = Field( + default=None, + validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"), + ) api_base: str | None = Field( default=None, validation_alias=AliasChoices( "LLM_API_BASE", "OPENAI_API_BASE", + "OPENAI_BASE_URL", "LITELLM_BASE_URL", "OLLAMA_API_BASE", ), diff --git a/strix/interface/main.py b/strix/interface/main.py index 691ccca..4d8ebfc 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -9,15 +9,21 @@ import json import shutil import sys from pathlib import Path -from typing import Any -import litellm +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing +from agents.models.multi_provider import MultiProvider from docker.errors import DockerException from rich.console import Console from rich.panel import Panel from rich.text import Text -from strix.config import apply_config_override, load_settings, persist_current +from strix.config import ( + apply_config_override, + load_settings, + persist_current, +) +from strix.config.models import configure_sdk_model_defaults, normalize_model_name from strix.interface.cli import run_cli from strix.interface.tui import run_tui from strix.interface.utils import ( @@ -34,9 +40,9 @@ from strix.interface.utils import ( resolve_diff_scope_context, rewrite_localhost_targets, validate_config_file, - validate_llm_response, ) from strix.telemetry import posthog +from strix.telemetry.logging import configure_dependency_logging from strix.telemetry.scan_store import get_global_scan_store @@ -97,7 +103,7 @@ def validate_environment() -> None: error_text.append("• ", style="white") error_text.append("STRIX_LLM", style="bold cyan") error_text.append( - " - Model name to use with litellm (e.g., 'openai/gpt-5.4')\n", + " - Model name to use (e.g., 'gpt-5.4' or 'claude-sonnet-4-6')\n", style="white", ) @@ -136,7 +142,7 @@ def validate_environment() -> None: ) error_text.append("\nExample setup:\n", style="white") - error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white") + error_text.append("export STRIX_LLM='gpt-5.4'\n", style="dim white") if missing_optional_vars: for var in missing_optional_vars: @@ -210,27 +216,27 @@ async def warm_up_llm() -> None: logger.info("Warming up LLM connection") try: - llm = load_settings().llm + settings = load_settings() + configure_sdk_model_defaults(settings) + llm = settings.llm - test_messages = [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Reply with just 'OK'."}, - ] - - completion_kwargs: dict[str, Any] = { - "model": llm.model, - "messages": test_messages, - "timeout": llm.timeout, - } - if llm.api_key: - completion_kwargs["api_key"] = llm.api_key - if llm.api_base: - completion_kwargs["api_base"] = llm.api_base - - response = litellm.completion(**completion_kwargs) - - validate_llm_response(response) - logger.info("LLM warm-up succeeded for model %s", llm.model) + model = MultiProvider().get_model(normalize_model_name(llm.model or "")) + await asyncio.wait_for( + model.get_response( + system_instructions="You are a helpful assistant.", + input="Reply with just 'OK'.", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + conversation_id=None, + prompt=None, + ), + timeout=llm.timeout, + ) + logger.info("LLM warm-up succeeded for model %s", normalize_model_name(llm.model or "")) except Exception as e: logger.exception("LLM warm-up failed") @@ -671,6 +677,8 @@ def pull_docker_image() -> None: def main() -> None: + configure_dependency_logging() + if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 98a8c7d..7afec91 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -1320,12 +1320,6 @@ def process_pull_line( return last_update -# LLM utilities -def validate_llm_response(response: Any) -> None: - if not response or not response.choices or not response.choices[0].message.content: - raise RuntimeError("Invalid response from LLM") - - def validate_config_file(config_path: str) -> Path: console = Console() path = Path(config_path) diff --git a/strix/llm/__init__.py b/strix/llm/__init__.py deleted file mode 100644 index 9c116e9..0000000 --- a/strix/llm/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -"""LLM package — model provider, prompt-cache wrapper, session, dedup helper. - -Side effects on import: - -- Quiet litellm's debug logger (it spams ``logging.DEBUG`` on every - request). The SDK's MultiProvider routes through litellm under the - hood, and the debug stream pollutes the run-directory event log. -- Quiet asyncio's RuntimeWarning + drop its log propagation; some - litellm async paths emit benign cleanup warnings. -""" - -import logging -import warnings - -import litellm - - -litellm._logging._disable_debugging() # type: ignore[no-untyped-call] -logging.getLogger("asyncio").setLevel(logging.CRITICAL) -logging.getLogger("asyncio").propagate = False -warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio") diff --git a/strix/llm/anthropic_cache_wrapper.py b/strix/llm/anthropic_cache_wrapper.py deleted file mode 100644 index 5a68cf1..0000000 --- a/strix/llm/anthropic_cache_wrapper.py +++ /dev/null @@ -1,135 +0,0 @@ -"""``AnthropicCachingLitellmModel`` — inject ``cache_control`` on the system message. - -``ModelSettings.extra_body`` lands fields at the request top level, -which Anthropic ignores. Anthropic only honors ``cache_control`` when -it is on the message itself, so we patch the input list before -delegating to the parent. -""" - -from __future__ import annotations - -import logging -from collections.abc import AsyncIterator -from typing import Any - -from agents.agent_output import AgentOutputSchemaBase -from agents.extensions.models.litellm_model import LitellmModel -from agents.handoffs import Handoff -from agents.items import ModelResponse, TResponseInputItem, TResponseStreamEvent -from agents.model_settings import ModelSettings -from agents.models.interface import ModelTracing -from agents.tool import Tool - - -logger = logging.getLogger(__name__) - - -class AnthropicCachingLitellmModel(LitellmModel): - """LitellmModel that injects ``cache_control: {"type": "ephemeral"}`` on the - system message for Anthropic models. Other providers pass through unchanged. - - Detection: case-insensitive substring match on ``"anthropic/"`` or - ``"claude"`` against the model name. - """ - - def _is_anthropic(self) -> bool: - m = (self.model or "").lower() - return "anthropic/" in m or "claude" in m - - def _patch( - self, - items: list[TResponseInputItem], - ) -> list[TResponseInputItem]: - """Return a copy of ``items`` with cache_control on the system message. - - Returns the input list unchanged for non-Anthropic models. For - Anthropic, the first ``role: system`` item has its content rewritten - from a string to a list-of-blocks with ``cache_control`` attached. - """ - if not self._is_anthropic(): - return items - out: list[TResponseInputItem] = [] - patched_count = 0 - for item in items: - if isinstance(item, dict) and item.get("role") == "system": - content = item.get("content") - if isinstance(content, str): - new_item = { - **item, - "content": [ - { - "type": "text", - "text": content, - "cache_control": {"type": "ephemeral"}, - }, - ], - } - out.append(new_item) # type: ignore[arg-type] - patched_count += 1 - continue - out.append(item) - if patched_count: - logger.debug( - "Anthropic cache_control injected on %d system message(s) for %s", - patched_count, - self.model, - ) - return out - - async def get_response( - self, - system_instructions: str | None, - input: str | list[TResponseInputItem], - model_settings: ModelSettings, - tools: list[Tool], - output_schema: AgentOutputSchemaBase | None, - handoffs: list[Handoff], - tracing: ModelTracing, - previous_response_id: str | None = None, - conversation_id: str | None = None, - prompt: Any | None = None, - ) -> ModelResponse: - patched = self._patch(input if isinstance(input, list) else []) - # If input was a string, patching is a no-op; pass straight through. - effective: str | list[TResponseInputItem] = patched if isinstance(input, list) else input - return await super().get_response( - system_instructions, - effective, - model_settings, - tools, - output_schema, - handoffs, - tracing, - previous_response_id=previous_response_id, - conversation_id=conversation_id, - prompt=prompt, - ) - - async def stream_response( - self, - system_instructions: str | None, - input: str | list[TResponseInputItem], - model_settings: ModelSettings, - tools: list[Tool], - output_schema: AgentOutputSchemaBase | None, - handoffs: list[Handoff], - tracing: ModelTracing, - previous_response_id: str | None = None, - conversation_id: str | None = None, - prompt: Any | None = None, - ) -> AsyncIterator[TResponseStreamEvent]: - patched = self._patch(input if isinstance(input, list) else []) - effective: str | list[TResponseInputItem] = patched if isinstance(input, list) else input - async for event in super().stream_response( - system_instructions, - effective, - model_settings, - tools, - output_schema, - handoffs, - tracing, - previous_response_id=previous_response_id, - conversation_id=conversation_id, - prompt=prompt, - ): - yield event diff --git a/strix/llm/multi_provider_setup.py b/strix/llm/multi_provider_setup.py deleted file mode 100644 index b8c472f..0000000 --- a/strix/llm/multi_provider_setup.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Multi-provider routing setup. - -Wraps the SDK's :class:`MultiProvider` and threads Strix's -``LLM_API_KEY`` / ``LLM_API_BASE`` into the underlying provider chain. - -Routing: - -- ``anthropic/`` → :class:`AnthropicCachingLitellmModel` so - prompt caching kicks in (we inject ``cache_control`` on the system - message before the litellm call). -- ``openai/`` (and bare model names) → SDK-native - :class:`OpenAIProvider`, instantiated with our settings credentials so - ``LLM_API_KEY`` works without forcing the user to also export - ``OPENAI_API_KEY``. Keeps the Responses API as the default transport - for genuine OpenAI usage. -- Every other prefix (``litellm/...``, ``any-llm/...``, …) falls through - to whatever the SDK does natively. - -Real-OpenAI vs OpenAI-compatible differentiation is by -``Settings.llm.api_base`` presence. If the user pointed at a non-default -base URL they're almost certainly on an OpenAI-compatible endpoint that -doesn't speak the Responses API, so we flip ``openai_use_responses=False`` -to make the inner provider use chat-completions transport instead. -""" - -from __future__ import annotations - -import logging - -from agents.exceptions import UserError -from agents.models.interface import Model, ModelProvider -from agents.models.multi_provider import MultiProvider, MultiProviderMap - -from strix.config import load_settings -from strix.llm.anthropic_cache_wrapper import AnthropicCachingLitellmModel - - -logger = logging.getLogger(__name__) - - -class _AnthropicCachingProvider(ModelProvider): - """Routes ``anthropic/`` aliases through - :class:`AnthropicCachingLitellmModel`. - - The SDK's ``MultiProvider`` strips the matched prefix before calling - ``get_model``, so we receive bare ``""`` (e.g. - ``"claude-sonnet-4-6"``) and re-prefix with ``anthropic/`` so litellm - routes to the Anthropic API. - """ - - def get_model(self, model_name: str | None) -> Model: - if not model_name: - raise UserError( - "Anthropic provider requires a non-empty model name (e.g. 'claude-sonnet-4-6').", - ) - full = model_name if model_name.startswith("anthropic/") else f"anthropic/{model_name}" - logger.debug("Anthropic provider: building cached model for %s", full) - return AnthropicCachingLitellmModel(model=full) - - -def build_multi_provider() -> MultiProvider: - """Build the configured MultiProvider. - - Registers the ``anthropic/`` route through our caching wrapper and - threads ``Settings.llm`` credentials into the SDK-native - :class:`OpenAIProvider` so ``openai/`` works with our single - ``LLM_API_KEY`` env var. ``Settings.llm.api_base`` (when set) flips - the OpenAI provider to chat-completions transport — the de-facto - signal that the user is hitting an OpenAI-compatible endpoint that - doesn't implement the Responses API. - """ - pmap = MultiProviderMap() # type: ignore[no-untyped-call] - pmap.add_provider("anthropic", _AnthropicCachingProvider()) - - llm = load_settings().llm - use_responses = llm.api_base is None # default endpoint → real OpenAI - logger.debug( - "MultiProvider built with anthropic/ cached + openai/ native " - "(api_key=%s, base_url=%s, use_responses=%s)", - "set" if llm.api_key else "unset", - llm.api_base or "default", - use_responses, - ) - return MultiProvider( - provider_map=pmap, - openai_api_key=llm.api_key, - openai_base_url=llm.api_base, - openai_use_responses=use_responses, - ) diff --git a/strix/llm/retry.py b/strix/llm/retry.py deleted file mode 100644 index d4d9939..0000000 --- a/strix/llm/retry.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Shared model-retry policy used across every Strix LLM call.""" - -from __future__ import annotations - -from agents.retry import ( - ModelRetryBackoffSettings, - ModelRetrySettings, - retry_policies, -) - - -# Retry: 5 attempts with ``min(90, 2*2^n)`` backoff. 4xx auth/validation -# errors are excluded from the retryable status list — they can't be -# fixed by retrying and should fail fast. Used by every ``RunConfig`` -# Strix builds, plus the dedupe path's one-shot LLM call outside -# ``Runner.run``. -DEFAULT_RETRY = ModelRetrySettings( - max_retries=5, - backoff=ModelRetryBackoffSettings( - initial_delay=2.0, - max_delay=90.0, - multiplier=2.0, - jitter=False, - ), - policy=retry_policies.any( - retry_policies.provider_suggested(), - retry_policies.network_error(), - retry_policies.http_status((429, 500, 502, 503, 504)), - ), -) diff --git a/strix/orchestration/runner.py b/strix/orchestration/runner.py index 4c9a1fc..cae70e2 100644 --- a/strix/orchestration/runner.py +++ b/strix/orchestration/runner.py @@ -24,7 +24,7 @@ from openai import APIError from strix.agents.factory import build_strix_agent, make_child_factory from strix.config import load_settings -from strix.llm.multi_provider_setup import build_multi_provider +from strix.config.models import configure_sdk_model_defaults, normalize_model_name from strix.orchestration.coordinator import AgentCoordinator, Status from strix.orchestration.utils import ( DEFAULT_MAX_TURNS, @@ -470,7 +470,8 @@ async def run_strix_scan( ) settings = load_settings() - resolved_model = model or settings.llm.model + configure_sdk_model_defaults(settings) + resolved_model = normalize_model_name(model or settings.llm.model or "") if not resolved_model: raise RuntimeError( "No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().", @@ -540,7 +541,6 @@ async def run_strix_scan( model_settings = make_model_settings(settings.llm.reasoning_effort) run_config = RunConfig( model=resolved_model, - model_provider=build_multi_provider(), model_settings=model_settings, sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]), trace_include_sensitive_data=False, diff --git a/strix/orchestration/utils.py b/strix/orchestration/utils.py index cc24baa..8686785 100644 --- a/strix/orchestration/utils.py +++ b/strix/orchestration/utils.py @@ -8,7 +8,7 @@ from typing import Any, Literal from agents.model_settings import ModelSettings from openai.types.shared import Reasoning -from strix.llm.retry import DEFAULT_RETRY +from strix.config.models import DEFAULT_MODEL_RETRY # Default max_turns budget passed to the SDK runner. @@ -111,7 +111,7 @@ def make_model_settings( model_settings = ModelSettings( parallel_tool_calls=False, tool_choice="required", - retry=DEFAULT_RETRY, + retry=DEFAULT_MODEL_RETRY, ) if reasoning_effort is not None: model_settings = model_settings.resolve( diff --git a/strix/report/__init__.py b/strix/report/__init__.py new file mode 100644 index 0000000..e9a05e1 --- /dev/null +++ b/strix/report/__init__.py @@ -0,0 +1,6 @@ +"""Report/finding helpers.""" + +from strix.report.dedupe import check_duplicate + + +__all__ = ["check_duplicate"] diff --git a/strix/llm/dedupe.py b/strix/report/dedupe.py similarity index 89% rename from strix/llm/dedupe.py rename to strix/report/dedupe.py index 8120c13..0d5703b 100644 --- a/strix/llm/dedupe.py +++ b/strix/report/dedupe.py @@ -1,10 +1,4 @@ -"""LLM-based vulnerability-report deduplication. - -Routes through the same :class:`MultiProvider` (so ``anthropic/...`` -models pick up :class:`AnthropicCachingLitellmModel`'s cache_control -patching) and :data:`DEFAULT_RETRY` policy as the main agent loop — -no parallel litellm code path. -""" +"""SDK-native vulnerability-report deduplication.""" from __future__ import annotations @@ -14,11 +8,15 @@ from typing import TYPE_CHECKING, Any from agents.model_settings import ModelSettings from agents.models.interface import ModelTracing +from agents.models.multi_provider import MultiProvider from openai.types.responses import ResponseOutputMessage from strix.config import load_settings -from strix.llm.multi_provider_setup import build_multi_provider -from strix.llm.retry import DEFAULT_RETRY +from strix.config.models import ( + DEFAULT_MODEL_RETRY, + configure_sdk_model_defaults, + normalize_model_name, +) if TYPE_CHECKING: @@ -145,12 +143,6 @@ def _parse_dedupe_response(content: str) -> dict[str, Any]: def _extract_text(response: ModelResponse) -> str: - """Concatenate ``output_text`` fragments across every message item. - - The SDK returns OpenAI Responses-API-shaped output; for a plain - chat-completion the assistant message has a list of content parts, - each of which carries a ``.text`` attribute we can pull verbatim. - """ parts: list[str] = [] for item in response.output: if not isinstance(item, ResponseOutputMessage): @@ -174,7 +166,8 @@ async def check_duplicate( } try: - model_name = load_settings().llm.model + settings = load_settings() + model_name = settings.llm.model if not model_name: return { "is_duplicate": False, @@ -193,11 +186,12 @@ async def check_duplicate( f"Respond with ONLY the JSON object described in the system prompt." ) - model = build_multi_provider().get_model(model_name) + configure_sdk_model_defaults(settings) + model = MultiProvider().get_model(normalize_model_name(model_name)) response = await model.get_response( system_instructions=DEDUPE_SYSTEM_PROMPT, input=user_msg, - model_settings=ModelSettings(retry=DEFAULT_RETRY), + model_settings=ModelSettings(retry=DEFAULT_MODEL_RETRY), tools=[], output_schema=None, handoffs=[], diff --git a/strix/telemetry/logging.py b/strix/telemetry/logging.py index 7900c01..a13d3cc 100644 --- a/strix/telemetry/logging.py +++ b/strix/telemetry/logging.py @@ -16,6 +16,7 @@ from __future__ import annotations import contextlib import logging import os +import warnings from contextvars import ContextVar from pathlib import Path # noqa: TC003 used at runtime by ``setup_scan_logging`` from typing import TYPE_CHECKING @@ -81,6 +82,19 @@ _HANDLER_TAG = "_strix_scan_handler" _TRACKED_ROOTS: tuple[str, ...] = ("strix", "openai.agents") +def configure_dependency_logging() -> None: + """Quiet dependency logging/warnings that obscure Strix scan logs.""" + with contextlib.suppress(Exception): + import litellm + + litellm_logging = litellm._logging + litellm_logging._disable_debugging() # type: ignore[no-untyped-call] + + logging.getLogger("asyncio").setLevel(logging.CRITICAL) + logging.getLogger("asyncio").propagate = False + warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio") + + def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[[], None]: """Attach scan-scoped handlers; return a teardown callable. @@ -97,6 +111,8 @@ def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[ call attached. Idempotent — calling twice is a no-op the second time. Safe to call from a ``finally`` block. """ + configure_dependency_logging() + if debug is None: debug = (os.environ.get("STRIX_DEBUG") or "").strip().lower() in { "1", diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py index 68d9f97..e75ad4a 100644 --- a/strix/tools/reporting/tool.py +++ b/strix/tools/reporting/tool.py @@ -151,7 +151,7 @@ _REQUIRED_FIELDS = { } -async def _do_create( # noqa: PLR0912, PLR0915 +async def _do_create( # noqa: PLR0912 *, title: str, description: str, @@ -225,7 +225,7 @@ async def _do_create( # noqa: PLR0912, PLR0915 "warning": "Report could not be persisted - scan store unavailable", } - from strix.llm.dedupe import check_duplicate + from strix.report.dedupe import check_duplicate existing = scan_store.get_existing_vulnerabilities() candidate = {