refactor: remove custom llm provider layer

This commit is contained in:
0xallam
2026-04-26 14:04:32 -07:00
parent 9f45121dce
commit c163ef882b
17 changed files with 185 additions and 351 deletions
+1 -10
View File
@@ -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,
+3 -5
View File
@@ -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',
+1 -1
View File
@@ -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()],
)
+98
View File
@@ -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
+7 -3
View File
@@ -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",
),
+34 -26
View File
@@ -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())
-6
View File
@@ -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)
-21
View File
@@ -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")
-135
View File
@@ -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
-89
View File
@@ -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/<model>`` → :class:`AnthropicCachingLitellmModel` so
prompt caching kicks in (we inject ``cache_control`` on the system
message before the litellm call).
- ``openai/<model>`` (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/<model>`` aliases through
:class:`AnthropicCachingLitellmModel`.
The SDK's ``MultiProvider`` strips the matched prefix before calling
``get_model``, so we receive bare ``"<model>"`` (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/<model>`` 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,
)
-30
View File
@@ -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)),
),
)
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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(
+6
View File
@@ -0,0 +1,6 @@
"""Report/finding helpers."""
from strix.report.dedupe import check_duplicate
__all__ = ["check_duplicate"]
+12 -18
View File
@@ -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=[],
+16
View File
@@ -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",
+2 -2
View File
@@ -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 = {