Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 250fe2cf3e | |||
| 6c99829325 | |||
| 1c9ab993bb | |||
| 04eb03febe | |||
| ac0fef2ed7 | |||
| dcf3155a9a | |||
| 36b374bd1b | |||
| 1a329e8972 | |||
| 143b9e7040 | |||
| 3665a7899f | |||
| 232711be8c | |||
| 712c64f630 | |||
| dee2a03d07 | |||
| 1473fc7336 | |||
| dd1f816f7c | |||
| 9ab70c6d61 | |||
| 13046cc74a | |||
| 1aad460f6e | |||
| d0321510d2 | |||
| 3bd9d56814 |
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "1.0.0"
|
version = "1.0.3"
|
||||||
description = "Open-source AI Hackers for your apps"
|
description = "Open-source AI Hackers for your apps"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
@@ -219,6 +219,8 @@ ignore = [
|
|||||||
# ReportState carries scan artifact/report fields and
|
# ReportState carries scan artifact/report fields and
|
||||||
# a runtime ``Callable`` annotation on ``vulnerability_found_callback``.
|
# a runtime ``Callable`` annotation on ``vulnerability_found_callback``.
|
||||||
"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401"]
|
"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401"]
|
||||||
|
"strix/report/usage.py" = ["PLC0415"]
|
||||||
|
"strix/config/models.py" = ["PLC0415"]
|
||||||
# Interface utility branches per scope-mode / target-type combination;
|
# Interface utility branches per scope-mode / target-type combination;
|
||||||
# splitting would obscure the decision tree without simplifying it.
|
# splitting would obscure the decision tree without simplifying it.
|
||||||
"strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"]
|
"strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"]
|
||||||
|
|||||||
+2
-3
@@ -32,6 +32,8 @@ datas += collect_data_files('tiktoken_ext')
|
|||||||
|
|
||||||
datas += collect_data_files('litellm')
|
datas += collect_data_files('litellm')
|
||||||
|
|
||||||
|
datas += collect_data_files('agents', includes=['**/*.md', '**/*.jinja', '**/*.json'])
|
||||||
|
|
||||||
hiddenimports = [
|
hiddenimports = [
|
||||||
# Core dependencies
|
# Core dependencies
|
||||||
'litellm',
|
'litellm',
|
||||||
@@ -188,9 +190,6 @@ excludes = [
|
|||||||
'pyte',
|
'pyte',
|
||||||
'openhands_aci',
|
'openhands_aci',
|
||||||
'openhands-aci',
|
'openhands-aci',
|
||||||
'gql',
|
|
||||||
'fastapi',
|
|
||||||
'uvicorn',
|
|
||||||
'numpydoc',
|
'numpydoc',
|
||||||
|
|
||||||
# Google Cloud / Vertex AI
|
# Google Cloud / Vertex AI
|
||||||
|
|||||||
@@ -395,7 +395,6 @@ def build_strix_agent(
|
|||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
tool_use_behavior=_finish_tool_use_behavior,
|
tool_use_behavior=_finish_tool_use_behavior,
|
||||||
reset_tool_choice=interactive,
|
|
||||||
model=None,
|
model=None,
|
||||||
capabilities=[
|
capabilities=[
|
||||||
Filesystem(
|
Filesystem(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ AUTONOMOUS BEHAVIOR:
|
|||||||
- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response.
|
- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response.
|
||||||
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
|
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
|
||||||
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root)
|
- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root)
|
||||||
|
- A text-only turn — even one — IMMEDIATELY ends the scan/run with no report written. The lifecycle tools (``finish_scan`` for root, ``agent_finish`` for subagents) are the ONLY valid way to terminate. If you find yourself wanting to say "Done!" or "Scan complete" without a tool call, call the lifecycle tool instead — the report and termination signal both flow through it.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</communication_rules>
|
</communication_rules>
|
||||||
|
|
||||||
|
|||||||
+94
-32
@@ -5,7 +5,8 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from agents import set_default_openai_api, set_default_openai_key
|
from agents import set_default_openai_api, set_default_openai_key, set_tracing_disabled
|
||||||
|
from agents.models.multi_provider import MultiProvider
|
||||||
from agents.retry import (
|
from agents.retry import (
|
||||||
ModelRetryBackoffSettings,
|
ModelRetryBackoffSettings,
|
||||||
ModelRetrySettings,
|
ModelRetrySettings,
|
||||||
@@ -14,10 +15,31 @@ from agents.retry import (
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from agents.models.interface import ModelProvider
|
||||||
|
|
||||||
from strix.config.settings import Settings
|
from strix.config.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
_SDK_PREFIXES = {"any-llm", "litellm", "openai"}
|
class StrixProvider(MultiProvider):
|
||||||
|
"""Route any non-OpenAI prefix through LiteLLM with the prefix preserved,
|
||||||
|
so users type ``deepseek/deepseek-chat`` rather than
|
||||||
|
``litellm/deepseek/deepseek-chat``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _resolve_prefixed_model(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
original_model_name: str,
|
||||||
|
prefix: str,
|
||||||
|
stripped_model_name: str | None,
|
||||||
|
) -> tuple[ModelProvider, str | None]:
|
||||||
|
if prefix in {"openai", "litellm", "any-llm"}:
|
||||||
|
return super()._resolve_prefixed_model(
|
||||||
|
original_model_name=original_model_name,
|
||||||
|
prefix=prefix,
|
||||||
|
stripped_model_name=stripped_model_name,
|
||||||
|
)
|
||||||
|
return self._get_fallback_provider("litellm"), original_model_name
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||||
@@ -37,17 +59,14 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
|||||||
|
|
||||||
|
|
||||||
def configure_sdk_model_defaults(settings: Settings) -> None:
|
def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||||
"""Apply Strix config to SDK-native defaults.
|
"""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
|
llm = settings.llm
|
||||||
|
set_tracing_disabled(True)
|
||||||
_configure_litellm_compatibility()
|
_configure_litellm_compatibility()
|
||||||
if llm.api_key:
|
if llm.api_key:
|
||||||
set_default_openai_key(llm.api_key, use_for_tracing=False)
|
set_default_openai_key(llm.api_key, use_for_tracing=False)
|
||||||
_configure_litellm_default("api_key", llm.api_key)
|
_configure_litellm_default("api_key", llm.api_key)
|
||||||
|
_mirror_api_key_to_provider_env(llm.model, llm.api_key)
|
||||||
if llm.api_base:
|
if llm.api_base:
|
||||||
os.environ["OPENAI_BASE_URL"] = llm.api_base
|
os.environ["OPENAI_BASE_URL"] = llm.api_base
|
||||||
_configure_litellm_default("api_base", llm.api_base)
|
_configure_litellm_default("api_base", llm.api_base)
|
||||||
@@ -56,12 +75,50 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
|||||||
set_default_openai_api("responses")
|
set_default_openai_api("responses")
|
||||||
|
|
||||||
|
|
||||||
|
def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> None:
|
||||||
|
if not model_name:
|
||||||
|
return
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
name = model_name.strip()
|
||||||
|
for prefix in ("litellm/", "any-llm/"):
|
||||||
|
if name.lower().startswith(prefix):
|
||||||
|
name = name[len(prefix) :]
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
report = litellm.validate_environment(model=name.lower())
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return
|
||||||
|
for env_key in report.get("missing_keys") or []:
|
||||||
|
if env_key.endswith("_API_KEY"):
|
||||||
|
os.environ.setdefault(env_key, api_key)
|
||||||
|
|
||||||
|
|
||||||
def _configure_litellm_compatibility() -> None:
|
def _configure_litellm_compatibility() -> None:
|
||||||
"""Enable LiteLLM's permissive param-handling mode."""
|
"""Enable LiteLLM's permissive param handling and disable its callbacks."""
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
litellm.drop_params = True
|
litellm.drop_params = True
|
||||||
litellm.modify_params = True
|
litellm.modify_params = True
|
||||||
|
litellm.turn_off_message_logging = True
|
||||||
|
litellm.disable_streaming_logging = True
|
||||||
|
litellm.suppress_debug_info = True
|
||||||
|
|
||||||
|
_register_litellm_cost_callback()
|
||||||
|
|
||||||
|
|
||||||
|
def _register_litellm_cost_callback() -> None:
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
from strix.report.state import litellm_cost_callback
|
||||||
|
|
||||||
|
for bucket_name in ("success_callback", "_async_success_callback"):
|
||||||
|
bucket = getattr(litellm, bucket_name, None)
|
||||||
|
if not isinstance(bucket, list):
|
||||||
|
continue
|
||||||
|
if litellm_cost_callback in bucket:
|
||||||
|
continue
|
||||||
|
bucket.append(litellm_cost_callback)
|
||||||
|
|
||||||
|
|
||||||
def _configure_litellm_default(name: str, value: str) -> None:
|
def _configure_litellm_default(name: str, value: str) -> None:
|
||||||
@@ -71,30 +128,35 @@ def _configure_litellm_default(name: str, value: str) -> None:
|
|||||||
setattr(litellm, name, value)
|
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
|
|
||||||
|
|
||||||
|
|
||||||
def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool:
|
def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool:
|
||||||
"""Return whether the resolved SDK route can only receive JSON function tools."""
|
"""Return whether the resolved SDK route can only receive JSON function tools."""
|
||||||
model = model_name.strip().lower()
|
model = model_name.strip().lower()
|
||||||
if model.startswith(("litellm/", "any-llm/")):
|
if "/" in model and not model.startswith("openai/"):
|
||||||
return True
|
return True
|
||||||
return bool(settings.llm.api_base)
|
if settings.llm.api_base:
|
||||||
|
return True
|
||||||
|
return not model_supports_reasoning(model_name)
|
||||||
|
|
||||||
|
|
||||||
|
def model_supports_reasoning(model_name: str) -> bool:
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
name = model_name.strip().lower()
|
||||||
|
for prefix in ("litellm/", "any-llm/", "openai/"):
|
||||||
|
if name.startswith(prefix):
|
||||||
|
name = name[len(prefix) :]
|
||||||
|
break
|
||||||
|
entry = litellm.model_cost.get(name)
|
||||||
|
if entry is None and "/" in name:
|
||||||
|
entry = litellm.model_cost.get(name.rsplit("/", 1)[1])
|
||||||
|
return bool(entry and entry.get("supports_reasoning"))
|
||||||
|
|
||||||
|
|
||||||
|
def is_known_openai_bare_model(model_name: str) -> bool:
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
name = model_name.strip().lower()
|
||||||
|
if not name or "/" in name:
|
||||||
|
return False
|
||||||
|
entry = litellm.model_cost.get(name)
|
||||||
|
return bool(entry and entry.get("litellm_provider") == "openai")
|
||||||
|
|||||||
+14
-6
@@ -349,12 +349,20 @@ async def _run_cycle( # noqa: PLR0912
|
|||||||
)
|
)
|
||||||
await coordinator.attach_stream(agent_id, stream)
|
await coordinator.attach_stream(agent_id, stream)
|
||||||
try:
|
try:
|
||||||
async for event in stream.stream_events():
|
try:
|
||||||
if event_sink is not None:
|
async for event in stream.stream_events():
|
||||||
try:
|
if event_sink is not None:
|
||||||
event_sink(agent_id, event)
|
try:
|
||||||
except Exception:
|
event_sink(agent_id, event)
|
||||||
logger.exception("stream event sink failed for %s", agent_id)
|
except Exception:
|
||||||
|
logger.exception("stream event sink failed for %s", agent_id)
|
||||||
|
except RuntimeError as stream_exc:
|
||||||
|
if "after shutdown" not in str(stream_exc):
|
||||||
|
raise
|
||||||
|
logger.warning(
|
||||||
|
"Ignoring LiteLLM end-of-stream shutdown race for %s",
|
||||||
|
agent_id,
|
||||||
|
)
|
||||||
if stream.run_loop_exception is not None:
|
if stream.run_loop_exception is not None:
|
||||||
raise stream.run_loop_exception
|
raise stream.run_loop_exception
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|||||||
from agents.model_settings import ModelSettings
|
from agents.model_settings import ModelSettings
|
||||||
from openai.types.shared import Reasoning
|
from openai.types.shared import Reasoning
|
||||||
|
|
||||||
from strix.config.models import DEFAULT_MODEL_RETRY
|
from strix.config.models import DEFAULT_MODEL_RETRY, model_supports_reasoning
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -108,14 +108,19 @@ def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
def make_model_settings(
|
def make_model_settings(
|
||||||
reasoning_effort: ReasoningEffort | None,
|
reasoning_effort: ReasoningEffort | None,
|
||||||
|
*,
|
||||||
|
model_name: str,
|
||||||
) -> ModelSettings:
|
) -> ModelSettings:
|
||||||
model_settings = ModelSettings(
|
model_settings = ModelSettings(
|
||||||
parallel_tool_calls=False,
|
parallel_tool_calls=False,
|
||||||
tool_choice="required",
|
|
||||||
retry=DEFAULT_MODEL_RETRY,
|
retry=DEFAULT_MODEL_RETRY,
|
||||||
include_usage=True,
|
include_usage=True,
|
||||||
)
|
)
|
||||||
if reasoning_effort is not None:
|
if (
|
||||||
|
reasoning_effort is not None
|
||||||
|
and reasoning_effort != "none"
|
||||||
|
and model_supports_reasoning(model_name)
|
||||||
|
):
|
||||||
model_settings = model_settings.resolve(
|
model_settings = model_settings.resolve(
|
||||||
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
|
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
|
||||||
)
|
)
|
||||||
|
|||||||
+29
-4
@@ -15,8 +15,8 @@ from agents.sandbox import SandboxRunConfig
|
|||||||
from strix.agents.factory import build_strix_agent, make_child_factory
|
from strix.agents.factory import build_strix_agent, make_child_factory
|
||||||
from strix.config import load_settings
|
from strix.config import load_settings
|
||||||
from strix.config.models import (
|
from strix.config.models import (
|
||||||
|
StrixProvider,
|
||||||
configure_sdk_model_defaults,
|
configure_sdk_model_defaults,
|
||||||
normalize_model_name,
|
|
||||||
uses_chat_completions_tool_schema,
|
uses_chat_completions_tool_schema,
|
||||||
)
|
)
|
||||||
from strix.core.agents import AgentCoordinator
|
from strix.core.agents import AgentCoordinator
|
||||||
@@ -90,7 +90,7 @@ async def run_strix_scan(
|
|||||||
|
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
configure_sdk_model_defaults(settings)
|
configure_sdk_model_defaults(settings)
|
||||||
resolved_model = normalize_model_name(model or settings.llm.model or "")
|
resolved_model = (model or settings.llm.model or "").strip()
|
||||||
if not resolved_model:
|
if not resolved_model:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().",
|
"No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().",
|
||||||
@@ -153,9 +153,13 @@ async def run_strix_scan(
|
|||||||
is_whitebox = any(t.get("type") == "local_code" for t in targets)
|
is_whitebox = any(t.get("type") == "local_code" for t in targets)
|
||||||
skills = list(scan_config.get("skills") or [])
|
skills = list(scan_config.get("skills") or [])
|
||||||
root_task = build_root_task(scan_config)
|
root_task = build_root_task(scan_config)
|
||||||
model_settings = make_model_settings(settings.llm.reasoning_effort)
|
model_settings = make_model_settings(
|
||||||
|
settings.llm.reasoning_effort,
|
||||||
|
model_name=resolved_model,
|
||||||
|
)
|
||||||
run_config = RunConfig(
|
run_config = RunConfig(
|
||||||
model=resolved_model,
|
model=resolved_model,
|
||||||
|
model_provider=StrixProvider(),
|
||||||
model_settings=model_settings,
|
model_settings=model_settings,
|
||||||
sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]),
|
sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]),
|
||||||
trace_include_sensitive_data=False,
|
trace_include_sensitive_data=False,
|
||||||
@@ -261,7 +265,7 @@ async def run_strix_scan(
|
|||||||
async with coordinator._lock:
|
async with coordinator._lock:
|
||||||
root_status = coordinator.statuses.get(root_id)
|
root_status = coordinator.statuses.get(root_id)
|
||||||
|
|
||||||
return await run_agent_loop(
|
result = await run_agent_loop(
|
||||||
agent=root_agent,
|
agent=root_agent,
|
||||||
initial_input=initial_input,
|
initial_input=initial_input,
|
||||||
run_config=run_config,
|
run_config=run_config,
|
||||||
@@ -275,6 +279,27 @@ async def run_strix_scan(
|
|||||||
event_sink=event_sink,
|
event_sink=event_sink,
|
||||||
hooks=hooks,
|
hooks=hooks,
|
||||||
)
|
)
|
||||||
|
if not interactive and result is not None:
|
||||||
|
final = getattr(result, "final_output", None)
|
||||||
|
scan_completed = False
|
||||||
|
if isinstance(final, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(final)
|
||||||
|
scan_completed = bool(isinstance(parsed, dict) and parsed.get("scan_completed"))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
scan_completed = False
|
||||||
|
elif isinstance(final, dict):
|
||||||
|
scan_completed = bool(final.get("scan_completed"))
|
||||||
|
if not scan_completed:
|
||||||
|
logger.error(
|
||||||
|
"Scan %s ended without calling finish_scan. The agent "
|
||||||
|
"emitted a text-only turn instead of a lifecycle tool call, "
|
||||||
|
"so no executive report was written. Final output (first "
|
||||||
|
"300 chars): %r",
|
||||||
|
scan_id,
|
||||||
|
str(final)[:300],
|
||||||
|
)
|
||||||
|
return result # noqa: TRY300
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception("Strix scan %s failed", scan_id)
|
logger.exception("Strix scan %s failed", scan_id)
|
||||||
if root_id is not None:
|
if root_id is not None:
|
||||||
|
|||||||
+42
-6
@@ -12,7 +12,6 @@ from pathlib import Path
|
|||||||
|
|
||||||
from agents.model_settings import ModelSettings
|
from agents.model_settings import ModelSettings
|
||||||
from agents.models.interface import ModelTracing
|
from agents.models.interface import ModelTracing
|
||||||
from agents.models.multi_provider import MultiProvider
|
|
||||||
from docker.errors import DockerException
|
from docker.errors import DockerException
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
@@ -23,7 +22,11 @@ from strix.config import (
|
|||||||
load_settings,
|
load_settings,
|
||||||
persist_current,
|
persist_current,
|
||||||
)
|
)
|
||||||
from strix.config.models import configure_sdk_model_defaults, normalize_model_name
|
from strix.config.models import (
|
||||||
|
StrixProvider,
|
||||||
|
configure_sdk_model_defaults,
|
||||||
|
is_known_openai_bare_model,
|
||||||
|
)
|
||||||
from strix.core.paths import run_dir_for, runtime_state_dir
|
from strix.core.paths import run_dir_for, runtime_state_dir
|
||||||
from strix.interface.cli import run_cli
|
from strix.interface.cli import run_cli
|
||||||
from strix.interface.tui import run_tui
|
from strix.interface.tui import run_tui
|
||||||
@@ -98,7 +101,8 @@ def validate_environment() -> None:
|
|||||||
error_text.append("• ", style="white")
|
error_text.append("• ", style="white")
|
||||||
error_text.append("STRIX_LLM", style="bold cyan")
|
error_text.append("STRIX_LLM", style="bold cyan")
|
||||||
error_text.append(
|
error_text.append(
|
||||||
" - Model name to use (e.g., 'gpt-5.4' or 'claude-sonnet-4-6')\n",
|
" - Model name to use (e.g., 'openai/gpt-5.4' or "
|
||||||
|
"'anthropic/claude-opus-4-7')\n",
|
||||||
style="white",
|
style="white",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,7 +141,7 @@ def validate_environment() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
error_text.append("\nExample setup:\n", style="white")
|
error_text.append("\nExample setup:\n", style="white")
|
||||||
error_text.append("export STRIX_LLM='gpt-5.4'\n", style="dim white")
|
error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white")
|
||||||
|
|
||||||
if missing_optional_vars:
|
if missing_optional_vars:
|
||||||
for var in missing_optional_vars:
|
for var in missing_optional_vars:
|
||||||
@@ -215,7 +219,39 @@ async def warm_up_llm() -> None:
|
|||||||
configure_sdk_model_defaults(settings)
|
configure_sdk_model_defaults(settings)
|
||||||
llm = settings.llm
|
llm = settings.llm
|
||||||
|
|
||||||
model = MultiProvider().get_model(normalize_model_name(llm.model or ""))
|
raw_model = (llm.model or "").strip()
|
||||||
|
if (
|
||||||
|
raw_model
|
||||||
|
and "/" not in raw_model
|
||||||
|
and not is_known_openai_bare_model(raw_model)
|
||||||
|
and not llm.api_base
|
||||||
|
):
|
||||||
|
warn_text = Text()
|
||||||
|
warn_text.append("UNKNOWN MODEL NAME", style="bold yellow")
|
||||||
|
warn_text.append("\n\n", style="white")
|
||||||
|
warn_text.append(f"'{raw_model}'", style="bold cyan")
|
||||||
|
warn_text.append(
|
||||||
|
" is not a known OpenAI model. Bare names route to OpenAI by default.\n"
|
||||||
|
"If you meant a non-OpenAI provider, use the '",
|
||||||
|
style="white",
|
||||||
|
)
|
||||||
|
warn_text.append("<provider>/<model>", style="bold cyan")
|
||||||
|
warn_text.append(
|
||||||
|
"' form, e.g. 'anthropic/claude-opus-4-7', 'deepseek/deepseek-v4-pro'.",
|
||||||
|
style="white",
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
warn_text,
|
||||||
|
title="[bold white]STRIX",
|
||||||
|
title_align="left",
|
||||||
|
border_style="yellow",
|
||||||
|
padding=(1, 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = StrixProvider().get_model(raw_model)
|
||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
model.get_response(
|
model.get_response(
|
||||||
system_instructions="You are a helpful assistant.",
|
system_instructions="You are a helpful assistant.",
|
||||||
@@ -231,7 +267,7 @@ async def warm_up_llm() -> None:
|
|||||||
),
|
),
|
||||||
timeout=llm.timeout,
|
timeout=llm.timeout,
|
||||||
)
|
)
|
||||||
logger.info("LLM warm-up succeeded for model %s", normalize_model_name(llm.model or ""))
|
logger.info("LLM warm-up succeeded for model %s", (llm.model or "").strip())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("LLM warm-up failed")
|
logger.exception("LLM warm-up failed")
|
||||||
|
|||||||
+34
-11
@@ -663,9 +663,9 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
|
|||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
if event.button.id == "quit":
|
if event.button.id == "quit":
|
||||||
self.app.action_custom_quit()
|
await self.app.action_custom_quit()
|
||||||
else:
|
else:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
@@ -751,6 +751,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
self.report_state.cleanup()
|
self.report_state.cleanup()
|
||||||
|
|
||||||
def signal_handler(_signum: int, _frame: Any) -> None:
|
def signal_handler(_signum: int, _frame: Any) -> None:
|
||||||
|
self._teardown_sandbox_blocking(timeout=10.0)
|
||||||
self.report_state.cleanup(status="interrupted")
|
self.report_state.cleanup(status="interrupted")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
@@ -1142,9 +1143,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
return (text, Text(), False)
|
return (text, Text(), False)
|
||||||
|
|
||||||
if status == "waiting":
|
if status == "waiting":
|
||||||
keymap = Text()
|
text = Text()
|
||||||
keymap.append("Send message to resume", style="dim")
|
text.append("Send message to resume", style="dim")
|
||||||
return (Text(" "), keymap, False)
|
return (text, Text(), False)
|
||||||
|
|
||||||
if status == "running":
|
if status == "running":
|
||||||
if self._agent_has_real_activity(agent_id):
|
if self._agent_has_real_activity(agent_id):
|
||||||
@@ -1632,9 +1633,9 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
|
|
||||||
self.push_screen(HelpScreen())
|
self.push_screen(HelpScreen())
|
||||||
|
|
||||||
def action_request_quit(self) -> None:
|
async def action_request_quit(self) -> None:
|
||||||
if self.show_splash or not self.is_mounted:
|
if self.show_splash or not self.is_mounted:
|
||||||
self.action_custom_quit()
|
await self.action_custom_quit()
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(self.screen_stack) > 1:
|
if len(self.screen_stack) > 1:
|
||||||
@@ -1643,7 +1644,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
try:
|
try:
|
||||||
self.query_one("#main_container")
|
self.query_one("#main_container")
|
||||||
except (ValueError, Exception):
|
except (ValueError, Exception):
|
||||||
self.action_custom_quit()
|
await self.action_custom_quit()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.push_screen(QuitScreen())
|
self.push_screen(QuitScreen())
|
||||||
@@ -1703,16 +1704,38 @@ class StrixTUIApp(App): # type: ignore[misc]
|
|||||||
self._scan_loop,
|
self._scan_loop,
|
||||||
)
|
)
|
||||||
|
|
||||||
def action_custom_quit(self) -> None:
|
async def action_custom_quit(self) -> None:
|
||||||
|
await asyncio.to_thread(self._teardown_sandbox_blocking, timeout=10.0)
|
||||||
|
|
||||||
if self._scan_thread and self._scan_thread.is_alive():
|
if self._scan_thread and self._scan_thread.is_alive():
|
||||||
self._scan_stop_event.set()
|
self._scan_stop_event.set()
|
||||||
|
self._scan_thread.join(timeout=2.0)
|
||||||
self._scan_thread.join(timeout=1.0)
|
|
||||||
|
|
||||||
self.report_state.cleanup()
|
self.report_state.cleanup()
|
||||||
|
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
||||||
|
def _teardown_sandbox_blocking(self, *, timeout: float) -> None:
|
||||||
|
loop = self._scan_loop
|
||||||
|
if loop is None or loop.is_closed():
|
||||||
|
return
|
||||||
|
run_name = self.scan_config.get("run_name")
|
||||||
|
if not run_name:
|
||||||
|
return
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
session_manager.cleanup(run_name),
|
||||||
|
loop,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
future.result(timeout=timeout)
|
||||||
|
except TimeoutError:
|
||||||
|
logger.warning(
|
||||||
|
"Sandbox cleanup timed out after %.1fs; container may still be running",
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Sandbox cleanup failed")
|
||||||
|
|
||||||
def _is_widget_safe(self, widget: Any) -> bool:
|
def _is_widget_safe(self, widget: Any) -> bool:
|
||||||
try:
|
try:
|
||||||
_ = widget.screen
|
_ = widget.screen
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ from typing import TYPE_CHECKING, Any
|
|||||||
|
|
||||||
from agents.model_settings import ModelSettings
|
from agents.model_settings import ModelSettings
|
||||||
from agents.models.interface import ModelTracing
|
from agents.models.interface import ModelTracing
|
||||||
from agents.models.multi_provider import MultiProvider
|
|
||||||
from openai.types.responses import ResponseOutputMessage
|
from openai.types.responses import ResponseOutputMessage
|
||||||
|
|
||||||
from strix.config import load_settings
|
from strix.config import load_settings
|
||||||
from strix.config.models import (
|
from strix.config.models import (
|
||||||
DEFAULT_MODEL_RETRY,
|
DEFAULT_MODEL_RETRY,
|
||||||
|
StrixProvider,
|
||||||
configure_sdk_model_defaults,
|
configure_sdk_model_defaults,
|
||||||
normalize_model_name,
|
|
||||||
)
|
)
|
||||||
from strix.report.state import get_global_report_state
|
from strix.report.state import get_global_report_state
|
||||||
|
|
||||||
@@ -188,8 +187,8 @@ async def check_duplicate(
|
|||||||
)
|
)
|
||||||
|
|
||||||
configure_sdk_model_defaults(settings)
|
configure_sdk_model_defaults(settings)
|
||||||
resolved_model = normalize_model_name(model_name)
|
resolved_model = model_name.strip()
|
||||||
model = MultiProvider().get_model(resolved_model)
|
model = StrixProvider().get_model(resolved_model)
|
||||||
response = await model.get_response(
|
response = await model.get_response(
|
||||||
system_instructions=DEDUPE_SYSTEM_PROMPT,
|
system_instructions=DEDUPE_SYSTEM_PROMPT,
|
||||||
input=user_msg,
|
input=user_msg,
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ class ReportState:
|
|||||||
):
|
):
|
||||||
self.save_run_data()
|
self.save_run_data()
|
||||||
|
|
||||||
|
def record_observed_llm_cost(self, cost: float) -> None:
|
||||||
|
self._llm_usage.record_observed_cost(cost)
|
||||||
|
|
||||||
def get_total_llm_usage(self) -> dict[str, Any]:
|
def get_total_llm_usage(self) -> dict[str, Any]:
|
||||||
return dict(self.run_record.get("llm_usage") or self._build_llm_usage_record())
|
return dict(self.run_record.get("llm_usage") or self._build_llm_usage_record())
|
||||||
|
|
||||||
@@ -343,3 +346,45 @@ class ReportState:
|
|||||||
def _hydrate_llm_usage(self, raw_usage: Any) -> None:
|
def _hydrate_llm_usage(self, raw_usage: Any) -> None:
|
||||||
self._llm_usage.hydrate(raw_usage)
|
self._llm_usage.hydrate(raw_usage)
|
||||||
self._sync_llm_usage_record()
|
self._sync_llm_usage_record()
|
||||||
|
|
||||||
|
|
||||||
|
def litellm_cost_callback(
|
||||||
|
kwargs: Any,
|
||||||
|
completion_response: Any,
|
||||||
|
_start_time: Any = None,
|
||||||
|
_end_time: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""LiteLLM ``success_callback`` adapter; forwards observed cost to the active scan."""
|
||||||
|
cost: float | None = None
|
||||||
|
raw = kwargs.get("response_cost") if isinstance(kwargs, dict) else None
|
||||||
|
if isinstance(raw, int | float) and raw > 0:
|
||||||
|
cost = float(raw)
|
||||||
|
|
||||||
|
if cost is None:
|
||||||
|
hidden = getattr(completion_response, "_hidden_params", None) or {}
|
||||||
|
candidate = hidden.get("response_cost") if isinstance(hidden, dict) else None
|
||||||
|
if isinstance(candidate, int | float) and candidate > 0:
|
||||||
|
cost = float(candidate)
|
||||||
|
else:
|
||||||
|
headers = hidden.get("additional_headers") or {} if isinstance(hidden, dict) else {}
|
||||||
|
raw = (
|
||||||
|
headers.get("llm_provider-x-litellm-response-cost")
|
||||||
|
if isinstance(headers, dict)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
value = float(raw) if raw is not None else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
value = None
|
||||||
|
if value is not None and value > 0:
|
||||||
|
cost = value
|
||||||
|
|
||||||
|
if cost is None or cost <= 0:
|
||||||
|
return
|
||||||
|
report_state = get_global_report_state()
|
||||||
|
if report_state is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
report_state.record_observed_llm_cost(cost)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to record observed LiteLLM cost")
|
||||||
|
|||||||
+52
-28
@@ -19,7 +19,6 @@ class LLMUsageLedger:
|
|||||||
self._agent_usage: dict[str, Usage] = {}
|
self._agent_usage: dict[str, Usage] = {}
|
||||||
self._agent_metadata: dict[str, dict[str, str]] = {}
|
self._agent_metadata: dict[str, dict[str, str]] = {}
|
||||||
self._total_cost = 0.0
|
self._total_cost = 0.0
|
||||||
self._agent_cost: dict[str, float] = {}
|
|
||||||
|
|
||||||
def record(
|
def record(
|
||||||
self,
|
self,
|
||||||
@@ -33,8 +32,6 @@ class LLMUsageLedger:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
normalized_agent_id = str(agent_id or "unknown")
|
normalized_agent_id = str(agent_id or "unknown")
|
||||||
estimated_cost = _estimate_litellm_cost(usage, model)
|
|
||||||
|
|
||||||
self._total_usage.add(usage)
|
self._total_usage.add(usage)
|
||||||
self._agent_usage.setdefault(normalized_agent_id, Usage()).add(usage)
|
self._agent_usage.setdefault(normalized_agent_id, Usage()).add(usage)
|
||||||
|
|
||||||
@@ -44,31 +41,38 @@ class LLMUsageLedger:
|
|||||||
if model:
|
if model:
|
||||||
metadata["model"] = model
|
metadata["model"] = model
|
||||||
|
|
||||||
if estimated_cost is not None:
|
if not _is_litellm_routed(model):
|
||||||
self._total_cost += estimated_cost
|
estimated = _estimate_litellm_cost(usage, model)
|
||||||
self._agent_cost[normalized_agent_id] = (
|
if estimated:
|
||||||
self._agent_cost.get(normalized_agent_id, 0.0) + estimated_cost
|
self._total_cost += estimated
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def record_observed_cost(self, cost: float) -> None:
|
||||||
|
if isinstance(cost, int | float) and cost > 0:
|
||||||
|
self._total_cost += float(cost)
|
||||||
|
|
||||||
def to_record(self) -> dict[str, Any]:
|
def to_record(self) -> dict[str, Any]:
|
||||||
record = serialize_usage(self._total_usage)
|
record = serialize_usage(self._total_usage)
|
||||||
record["cost"] = _round_cost(self._total_cost)
|
record["cost"] = _round_cost(self._total_cost)
|
||||||
record["cost_source"] = "litellm_estimate"
|
|
||||||
record["agents"] = []
|
record["agents"] = []
|
||||||
|
|
||||||
|
agent_tokens = {aid: _resolve_total_tokens(u) for aid, u in self._agent_usage.items()}
|
||||||
|
total_tokens = sum(agent_tokens.values())
|
||||||
for agent_id in sorted(self._agent_usage):
|
for agent_id in sorted(self._agent_usage):
|
||||||
usage = self._agent_usage[agent_id]
|
usage = self._agent_usage[agent_id]
|
||||||
metadata = self._agent_metadata.get(agent_id, {})
|
metadata = self._agent_metadata.get(agent_id, {})
|
||||||
|
agent_cost = (
|
||||||
|
self._total_cost * (agent_tokens[agent_id] / total_tokens) if total_tokens else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
agent_record = serialize_usage(usage)
|
agent_record = serialize_usage(usage)
|
||||||
agent_record.update(
|
agent_record.update(
|
||||||
{
|
{
|
||||||
"agent_id": agent_id,
|
"agent_id": agent_id,
|
||||||
"agent_name": metadata.get("agent_name") or agent_id,
|
"agent_name": metadata.get("agent_name") or agent_id,
|
||||||
"model": metadata.get("model"),
|
"model": metadata.get("model"),
|
||||||
"cost": _round_cost(self._agent_cost.get(agent_id, 0.0)),
|
"cost": _round_cost(agent_cost),
|
||||||
"cost_source": "litellm_estimate",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
record["agents"].append(agent_record)
|
record["agents"].append(agent_record)
|
||||||
@@ -80,7 +84,6 @@ class LLMUsageLedger:
|
|||||||
self._agent_usage.clear()
|
self._agent_usage.clear()
|
||||||
self._agent_metadata.clear()
|
self._agent_metadata.clear()
|
||||||
self._total_cost = 0.0
|
self._total_cost = 0.0
|
||||||
self._agent_cost.clear()
|
|
||||||
|
|
||||||
if not isinstance(raw_usage, dict):
|
if not isinstance(raw_usage, dict):
|
||||||
return
|
return
|
||||||
@@ -92,11 +95,8 @@ class LLMUsageLedger:
|
|||||||
self._total_usage = Usage()
|
self._total_usage = Usage()
|
||||||
|
|
||||||
self._total_cost = _float_or_zero(raw_usage.get("cost"))
|
self._total_cost = _float_or_zero(raw_usage.get("cost"))
|
||||||
agents = raw_usage.get("agents") or []
|
|
||||||
if not isinstance(agents, list):
|
|
||||||
return
|
|
||||||
|
|
||||||
for raw_agent in agents:
|
for raw_agent in raw_usage.get("agents") or []:
|
||||||
if not isinstance(raw_agent, dict):
|
if not isinstance(raw_agent, dict):
|
||||||
continue
|
continue
|
||||||
agent_id = str(raw_agent.get("agent_id") or "").strip()
|
agent_id = str(raw_agent.get("agent_id") or "").strip()
|
||||||
@@ -116,7 +116,24 @@ class LLMUsageLedger:
|
|||||||
if isinstance(model, str) and model:
|
if isinstance(model, str) and model:
|
||||||
metadata["model"] = model
|
metadata["model"] = model
|
||||||
self._agent_metadata[agent_id] = metadata
|
self._agent_metadata[agent_id] = metadata
|
||||||
self._agent_cost[agent_id] = _float_or_zero(raw_agent.get("cost"))
|
|
||||||
|
|
||||||
|
def _resolve_total_tokens(usage: Usage) -> int:
|
||||||
|
total = max(0, int(usage.total_tokens or 0))
|
||||||
|
if total > 0:
|
||||||
|
return total
|
||||||
|
prompt = _int_or_zero(getattr(usage, "input_tokens", 0))
|
||||||
|
completion = _int_or_zero(getattr(usage, "output_tokens", 0))
|
||||||
|
return prompt + completion
|
||||||
|
|
||||||
|
|
||||||
|
def _is_litellm_routed(model: str | None) -> bool:
|
||||||
|
if not model:
|
||||||
|
return False
|
||||||
|
name = model.strip().lower()
|
||||||
|
if "/" not in name:
|
||||||
|
return False
|
||||||
|
return not name.startswith("openai/")
|
||||||
|
|
||||||
|
|
||||||
def _usage_has_activity(usage: Usage) -> bool:
|
def _usage_has_activity(usage: Usage) -> bool:
|
||||||
@@ -171,18 +188,25 @@ def _estimate_litellm_entry_cost(entry: Any, model: str) -> float | None:
|
|||||||
if completion_details:
|
if completion_details:
|
||||||
usage_payload["completion_tokens_details"] = completion_details
|
usage_payload["completion_tokens_details"] = completion_details
|
||||||
|
|
||||||
try:
|
from litellm import completion_cost
|
||||||
from litellm import completion_cost
|
|
||||||
|
|
||||||
cost = completion_cost(
|
candidates = [model]
|
||||||
completion_response={
|
if "/" in model:
|
||||||
"model": model.split("/", 1)[-1],
|
candidates.append(model.split("/", 1)[-1])
|
||||||
"usage": usage_payload,
|
|
||||||
},
|
cost: Any = None
|
||||||
model=model,
|
for candidate in candidates:
|
||||||
)
|
try:
|
||||||
except Exception: # noqa: BLE001 - LiteLLM raises plain Exception for unknown model prices.
|
cost = completion_cost(
|
||||||
logger.debug("LiteLLM cost estimate unavailable for model %s", model, exc_info=True)
|
completion_response={"model": candidate, "usage": usage_payload},
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except Exception: # nosec B112 # noqa: BLE001, S112
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cost is None:
|
||||||
|
logger.debug("LiteLLM cost estimate unavailable for model %s", model)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return cost if isinstance(cost, int | float) and cost >= 0 else None
|
return cost if isinstance(cost, int | float) and cost >= 0 else None
|
||||||
|
|||||||
@@ -956,7 +956,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litellm"
|
name = "litellm"
|
||||||
version = "1.83.7"
|
version = "1.88.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
@@ -972,9 +972,9 @@ dependencies = [
|
|||||||
{ name = "tiktoken" },
|
{ name = "tiktoken" },
|
||||||
{ name = "tokenizers" },
|
{ name = "tokenizers" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/77/2b/b58bf6bbcbc3d0e55d0a84fdf9128e5b1436517f46fce89b1cd8948ebb81/litellm-1.83.7.tar.gz", hash = "sha256:e2f2cb99df2e2b2eab63f1354faa45c88dd7c8d40c18eb648afb1b349c689633", size = 17791694, upload-time = "2026-04-13T17:35:01.606Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/8c/6cfce5d15554e076b8438a955436e9e6e2a2bd39c76539656c1c3861c369/litellm-1.88.0.tar.gz", hash = "sha256:4ff794493e40bd86c6f13e91dcb3e1aad697403fd46a96902196d93356ba48f4", size = 13886026, upload-time = "2026-06-06T23:25:17.93Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/80/caeb4cdcad96451ba83ad3ba2a9da08b1e1a915fa845c489f56ea044488b/litellm-1.83.7-py3-none-any.whl", hash = "sha256:5784a1d9a9a4a8acd6ca1e347003a5e2e1b3c749b4d41e7da4904577adade111", size = 16069807, upload-time = "2026-04-13T17:34:58.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/71/deb6637475253b83eb4577c058863eec4b4ddaef2d08dcd93981b1aa4209/litellm-1.88.0-py3-none-any.whl", hash = "sha256:abd3037e0bf5703f833f5565c87bdfd93578d3de46cbcb36dfa108c3ef58021c", size = 15276203, upload-time = "2026-06-06T23:25:07.257Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2035,7 +2035,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "1.0.0"
|
version = "1.0.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "caido-sdk-client" },
|
{ name = "caido-sdk-client" },
|
||||||
|
|||||||
Reference in New Issue
Block a user