Compare commits

..
Author SHA1 Message Date
0xallamandClaude Opus 4.8 f60aaae1fd settle child agents before closing sessions at wind-down
Every error path cancels the agent subtree before returning, so children unwind
while their sessions are still open. The success path did not: when root called
finish_scan with children still mid-turn, the finally closed their sessions
underneath them, the in-flight save raised "SQLiteSession is closed", and those
children were marked crashed - failing a scan whose report was already written.

Cancelling the descendants moves into the finally, ahead of the session close,
so it runs on every path - clean finish included. The per-branch cancels are
dropped since the finally now covers them; each branch keeps only its own root
status. cancel_descendants targets child tasks (root runs inline with no task),
and the finally already awaits snapshot and cleanup under cancellation, so the
added await is safe there.

Fixes #1012.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-09 03:14:15 +03:00
36 changed files with 145 additions and 440 deletions
-15
View File
@@ -117,21 +117,6 @@ ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
ENV AGENT_BROWSER_USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
ENV AGENT_BROWSER_ARGS="--disable-blink-features=AutomationControlled,--no-first-run,--no-default-browser-check,--lang=en-US"
ENV AGENT_BROWSER_SCREENSHOT_DIR=/workspace/.agent-browser-screenshots
ENV AGENT_BROWSER_IDLE_TIMEOUT_MS=180000
USER root
RUN set -eu; \
{ \
for var in AGENT_BROWSER_EXECUTABLE_PATH AGENT_BROWSER_USER_AGENT \
AGENT_BROWSER_ARGS AGENT_BROWSER_SCREENSHOT_DIR \
AGENT_BROWSER_IDLE_TIMEOUT_MS; do \
eval "value=\${$var}"; \
printf 'export %s="${%s:-%s}"\n' "$var" "$var" "$value"; \
done; \
} > /tmp/agent-browser.sh; \
install -m 0644 /tmp/agent-browser.sh /etc/profile.d/agent-browser.sh; \
rm /tmp/agent-browser.sh; \
env -i bash -lc 'test "${AGENT_BROWSER_IDLE_TIMEOUT_MS}" = "180000"'
USER pentester
RUN /home/pentester/.npm-global/bin/agent-browser doctor --offline --quick
RUN set -eux; \
+4 -4
View File
@@ -8,7 +8,7 @@ Configure Strix using environment variables or a config file.
## LLM Configuration
<ParamField path="STRIX_LLM" type="string" required>
Model name in LiteLLM format (for example, `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
Model name in LiteLLM format (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
</ParamField>
<ParamField path="LLM_API_KEY" type="string">
@@ -20,7 +20,7 @@ Configure Strix using environment variables or a config file.
</ParamField>
<ParamField path="LLM_EXTRA_HEADERS" type="string">
Extra HTTP headers sent on every LLM request, as a JSON object (for example
Extra HTTP headers sent on every LLM request, as a JSON object (e.g.
`{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible
gateways that require attribution or routing headers in addition to the bearer
token. The bearer token itself still comes from `LLM_API_KEY`. Applies to both
@@ -65,8 +65,8 @@ affecting the agents that do the actual testing.
<ParamField path="DEDUPE_LLM_EXTRA_HEADERS" type="string">
Optional JSON object of extra HTTP headers sent on every deduplication-model
request, for example `{"X-Feature-Key":"value"}`. A dedicated dedupe model never
inherits `LLM_EXTRA_HEADERS`. Set this when its endpoint needs custom headers.
request, e.g. `{"X-Feature-Key":"value"}`. A dedicated dedupe model never
inherits `LLM_EXTRA_HEADERS`; set this when its endpoint needs custom headers.
</ParamField>
<ParamField path="STRIX_DEDUPE_REASONING_EFFORT" type="string">
+8 -8
View File
@@ -7,7 +7,7 @@ Skills are structured knowledge packages that give Strix agents deep expertise i
## The Idea
LLMs have broad but shallow security knowledge. They know _about_ SQL injection, but lack the nuanced techniques that experienced pentesters use: parser quirks, bypass methods, validation tricks, and chain attacks.
LLMs have broad but shallow security knowledge. They know _about_ SQL injection, but lack the nuanced techniques that experienced pentesters useparser quirks, bypass methods, validation tricks, and chain attacks.
Skills inject this deep, specialized knowledge directly into the agent's context, transforming it from a generalist into a specialist for the task at hand.
@@ -25,9 +25,9 @@ create_agent(
The skills are injected into the agent's system prompt, giving it access to:
- **Advanced techniques**: Non-obvious methods beyond standard testing
- **Working payloads**: Practical examples with variations
- **Validation methods**: How to confirm findings and avoid false positives
- **Advanced techniques** Non-obvious methods beyond standard testing
- **Working payloads** Practical examples with variations
- **Validation methods** How to confirm findings and avoid false positives
## Skill Categories
@@ -138,7 +138,7 @@ How to confirm findings and avoid false positives.
Community contributions are welcome. Create a `.md` file in the appropriate category with YAML frontmatter (`name` and `description` fields). Good skills include:
1. **Real-world techniques**: Methods that work in practice
2. **Practical payloads**: Working examples with variations
3. **Validation steps**: How to confirm without false positives
4. **Context awareness**: Version/environment-specific behavior
1. **Real-world techniques** Methods that work in practice
2. **Practical payloads** Working examples with variations
3. **Validation steps** How to confirm without false positives
4. **Context awareness** Version/environment-specific behavior
+4 -4
View File
@@ -24,10 +24,10 @@ Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai).
## What You Get
- **Penetration test reports**: Validated findings with PoCs
- **Shareable dashboards**: Collaborate with your team
- **CI/CD integration**: Block risky changes automatically
- **Continuous monitoring**: Catch new vulnerabilities quickly
- **Penetration test reports** Validated findings with PoCs
- **Shareable dashboards** Collaborate with your team
- **CI/CD integration** Block risky changes automatically
- **Continuous monitoring** Catch new vulnerabilities quickly
## Getting Started
+9 -9
View File
@@ -52,20 +52,20 @@ Skills are specialized knowledge packages that enhance agent capabilities. They
1. Choose the right category
2. Create a `.md` file with YAML frontmatter (`name` and `description` fields)
3. Include practical examples: working payloads, commands, test cases
3. Include practical examplesworking payloads, commands, test cases
4. Provide validation methods to confirm findings
5. Submit through PR
5. Submit via PR
## Contributing Code
### Pull Request Process
1. **Create an issue first**: Describe the problem or feature
2. **Fork and branch**: Work from `main`
3. **Make changes**: Follow existing code style
4. **Write tests**: Ensure coverage for new features
5. **Run checks**: `make check-all` should pass
6. **Submit PR**: Link to issue and provide context
1. **Create an issue first** Describe the problem or feature
2. **Fork and branch** Work from `main`
3. **Make changes** Follow existing code style
4. **Write tests** Ensure coverage for new features
5. **Run checks** `make check-all` should pass
6. **Submit PR** Link to issue and provide context
### Code Style
@@ -77,7 +77,7 @@ Skills are specialized knowledge packages that enhance agent capabilities. They
## Package Builds
Editable installs do not require Go. They run the TUI from source (`go run`).
Editable installs do not require Go; they run the TUI from source (`go run`).
Wheels are intentionally strict: they always bundle the matching Go sidecar and
are platform-specific.
+12 -12
View File
@@ -3,7 +3,7 @@ title: "Introduction"
description: "Open-source AI hackers to secure your apps"
---
Strix are autonomous AI agents that act like real hackers: they run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
Strix are autonomous AI agents that act like real hackersthey run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools.
<Frame>
<img src="/images/screenshot.png" alt="Strix Demo" />
@@ -26,17 +26,17 @@ Strix are autonomous AI agents that act like real hackers: they run your code dy
## Use Cases
- **Application Security Testing**: Detect and validate critical vulnerabilities in your applications
- **Rapid Penetration Testing**: Get penetration tests done in hours, not weeks
- **Bug Bounty Automation**: Automate research and generate PoCs for faster reporting
- **CI/CD Integration**: Block vulnerabilities before they reach production
- **Application Security Testing** Detect and validate critical vulnerabilities in your applications
- **Rapid Penetration Testing** Get penetration tests done in hours, not weeks
- **Bug Bounty Automation** Automate research and generate PoCs for faster reporting
- **CI/CD Integration** Block vulnerabilities before they reach production
## Key Capabilities
- **Full hacker toolkit**: Browser automation, HTTP proxy, terminal, Python runtime
- **Real validation**: PoCs, not false positives
- **Multi-agent orchestration**: Specialized agents collaborate on complex targets
- **Developer-first CLI**: Interactive TUI or headless mode for automation
- **Full hacker toolkit** Browser automation, HTTP proxy, terminal, Python runtime
- **Real validation** PoCs, not false positives
- **Multi-agent orchestration** Specialized agents collaborate on complex targets
- **Developer-first CLI** Interactive TUI or headless mode for automation
## Security Tools
@@ -67,9 +67,9 @@ Strix agents come equipped with a comprehensive toolkit:
Strix uses a graph of specialized agents for comprehensive security testing:
- **Distributed Workflows**: Specialized agents for different attacks and assets
- **Scalable Testing**: Parallel execution for fast comprehensive coverage
- **Dynamic Coordination**: Agents collaborate and share discoveries
- **Distributed Workflows** Specialized agents for different attacks and assets
- **Scalable Testing** Parallel execution for fast comprehensive coverage
- **Dynamic Coordination** Agents collaborate and share discoveries
## Quick Example
+12 -12
View File
@@ -7,7 +7,7 @@ Strix is built to be driven by AI coding agents. Install the official agent skil
## Install the Skills
Works with any agent that supports the open [SKILL.md standard](https://agentskills.io): Claude Code, Cursor, Codex, Gemini CLI, OpenCode, and dozens more:
Works with any agent that supports the open [SKILL.md standard](https://agentskills.io) Claude Code, Cursor, Codex, Gemini CLI, OpenCode, and dozens more:
```bash
npx skills add usestrix/strix
@@ -15,8 +15,8 @@ npx skills add usestrix/strix
| Skill | What your agent learns |
|-------|------------------------|
| `penetration-testing-with-strix` | Run headless scans against code, URLs, domains, or IPs (self-hosted CLI or managed cloud) with budget caps, and read the results |
| `managed-pentesting-with-strix` | Drive the managed [app.strix.ai](https://app.strix.ai) platform over REST: no local Docker or LLM key needed |
| `penetration-testing-with-strix` | Run headless scans against code, URLs, domains, or IPs self-hosted CLI or managed cloud with budget caps, and read the results |
| `managed-pentesting-with-strix` | Drive the managed [app.strix.ai](https://app.strix.ai) platform over REST no local Docker or LLM key needed |
| `fix-security-vulnerabilities-with-strix` | Triage findings, fix root causes, and re-run Strix to verify each fix |
| `ci-security-scanning-with-strix` | Add PR security scanning to GitHub Actions or any CI (self-hosted CLI or managed app) |
@@ -26,23 +26,23 @@ Install a single skill with `npx skills add usestrix/strix --skill penetration-t
npx skills use usestrix/strix@penetration-testing-with-strix | claude
```
## Two ways to run: self-hosted or managed
## Two ways to run self-hosted or managed
Both use the same engine and produce the same validated findings and SARIF, so agents can pick per situation or combine them:
- **Open-source CLI (self-hosted)**: runs locally in a Docker sandbox with your own LLM key. Free, fully local, air-gap capable. Best for local dev loops and full control.
- **Managed cloud**: runs on Strix's infrastructure through the [app.strix.ai REST API](https://docs.app.strix.ai). No Docker, no LLM key, no local install, adds team dashboards, scheduling, PR reviews, and downloadable PDF/DOCX reports (Enterprise plan). Best in sandboxed/CI environments and for teams. Create an API token under **Settings → API Access**. The `managed-pentesting-with-strix` skill has the full flow.
- **Open-source CLI (self-hosted)** runs locally in a Docker sandbox with your own LLM key. Free, fully local, air-gap capable. Best for local dev loops and full control.
- **Managed cloud** runs on Strix's infrastructure via the [app.strix.ai REST API](https://docs.app.strix.ai). No Docker, no LLM key, no local install; adds team dashboards, scheduling, PR reviews, and downloadable PDF/DOCX reports (Enterprise plan). Best in sandboxed/CI environments and for teams. Create an API token under **Settings → API Access**; the `managed-pentesting-with-strix` skill has the full flow.
## Agent-Friendly Interfaces
Everything an agent needs is machine-readable:
- **Headless CLI**: `strix -n` runs without the TUI and exits with `0` (clean), `1` (error), or `2` (vulnerabilities found).
- **REST API**: the managed platform exposes a documented [OpenAPI](https://docs.app.strix.ai/openapi.json) at `https://app.strix.ai/api/v1` (scans, vulnerabilities, assets, PR reviews, schedules, webhooks) with bearer tokens and scopes.
- **Structured results**: every run writes `vulnerabilities.json`, `vulnerabilities.csv`, `findings.sarif` (SARIF 2.1.0), and per-finding Markdown under `strix_runs/<run-name>/`. The cloud exposes the same as JSON plus SARIF export.
- **Budget controls**: `--max-budget` and `--max-turns` give agents hard cost/time caps.
- **`AGENTS.md`**: the [repository's agent guide](https://github.com/usestrix/strix/blob/main/AGENTS.md) with a quick reference.
- **`llms.txt`**: this documentation is indexed at [docs.strix.ai/llms.txt](https://docs.strix.ai/llms.txt) and fully exported at [docs.strix.ai/llms-full.txt](https://docs.strix.ai/llms-full.txt). Every page is also available as Markdown by appending `.md` to its URL.
- **Headless CLI** `strix -n` runs without the TUI and exits with `0` (clean), `1` (error), or `2` (vulnerabilities found).
- **REST API** the managed platform exposes a documented [OpenAPI](https://docs.app.strix.ai/openapi.json) at `https://app.strix.ai/api/v1` (scans, vulnerabilities, assets, PR reviews, schedules, webhooks) with bearer tokens and scopes.
- **Structured results** every run writes `vulnerabilities.json`, `vulnerabilities.csv`, `findings.sarif` (SARIF 2.1.0), and per-finding Markdown under `strix_runs/<run-name>/`; the cloud exposes the same as JSON plus SARIF export.
- **Budget controls** `--max-budget` and `--max-turns` give agents hard cost/time caps.
- **`AGENTS.md`** the [repository's agent guide](https://github.com/usestrix/strix/blob/main/AGENTS.md) with a quick reference.
- **`llms.txt`** this documentation is indexed at [docs.strix.ai/llms.txt](https://docs.strix.ai/llms.txt) and fully exported at [docs.strix.ai/llms-full.txt](https://docs.strix.ai/llms-full.txt); every page is also available as Markdown by appending `.md` to its URL.
## Example Prompts
+3 -3
View File
@@ -37,7 +37,7 @@ Add these secrets to your repository:
| Secret | Description |
|--------|-------------|
| `STRIX_LLM` | Model name (for example, `openai/gpt-5.4`) |
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5.4`) |
| `LLM_API_KEY` | API key for your LLM provider |
## Exit Codes
@@ -46,8 +46,8 @@ The workflow fails when vulnerabilities are found:
| Code | Result |
|------|--------|
| 0 | Pass: No vulnerabilities |
| 2 | Fail: Vulnerabilities found |
| 0 | Pass No vulnerabilities |
| 2 | Fail Vulnerabilities found |
## Scan Modes for CI
+3 -3
View File
@@ -1,6 +1,6 @@
---
title: "Azure OpenAI"
description: "Configure Strix with OpenAI models through Azure"
description: "Configure Strix with OpenAI models via Azure"
---
## Setup
@@ -19,7 +19,7 @@ export AZURE_API_VERSION="2025-11-01-preview"
| `STRIX_LLM` | `azure/<your-deployment-name>` |
| `AZURE_API_KEY` | Your Azure OpenAI API key |
| `AZURE_API_BASE` | Your Azure OpenAI endpoint URL |
| `AZURE_API_VERSION` | API version (for example, `2025-11-01-preview`) |
| `AZURE_API_VERSION` | API version (e.g., `2025-11-01-preview`) |
## Example
@@ -33,5 +33,5 @@ export AZURE_API_VERSION="2025-11-01-preview"
## Prerequisites
1. Create an Azure OpenAI resource
2. Deploy a model (for example, GPT-5.4)
2. Deploy a model (e.g., GPT-5.4)
3. Get the endpoint URL and API key from the Azure portal
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: "AWS Bedrock"
description: "Configure Strix with models through AWS Bedrock"
description: "Configure Strix with models via AWS Bedrock"
---
## Installation
@@ -17,7 +17,7 @@ pipx install "strix-agent[bedrock]"
export STRIX_LLM="bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0"
```
No API key required: uses AWS credentials from environment.
No API key requireduses AWS credentials from environment.
## Authentication
+9 -9
View File
@@ -59,7 +59,7 @@ export LLM_API_BASE="http://localhost:1234/v1" # Adjust port as needed
Some OpenAI-compatible gateways require extra HTTP headers (for attribution or
tenant routing) alongside the bearer token. Set them with `LLM_EXTRA_HEADERS` as
a JSON object. They are sent on every request:
a JSON object — they are sent on every request:
```bash
export STRIX_LLM="openai/your-model"
@@ -69,12 +69,12 @@ export LLM_EXTRA_HEADERS='{"X-Feature-Key":"value","X-Tenant":"acme"}'
```
For endpoints behind a private CA, point Strix at your certificate bundle with
the standard `SSL_CERT_FILE=/path/to/ca-bundle.pem`: never disable TLS
the standard `SSL_CERT_FILE=/path/to/ca-bundle.pem` never disable TLS
verification against a real endpoint.
## Tool calling must return structured `tool_calls`
Strix is entirely tool-driven: every working turn must be a **native** function/tool call. If your inference server returns the tool call as plain assistant text instead of a structured `tool_calls` field, Strix never sees a call it can execute, so the agent makes no real progress. It re-prompts the model for a tool call and gives up once its recovery attempts are exhausted.
Strix is entirely tool-driven: every working turn must be a **native** function/tool call. If your inference server returns the tool call as plain assistant text instead of a structured `tool_calls` field, Strix never sees a call it can execute, so the agent makes no real progress — it re-prompts the model for a tool call and gives up once its recovery attempts are exhausted.
This is almost always an **inference-server configuration** problem, not a model or Strix problem. Common symptoms are the model printing a call as text such as:
@@ -84,19 +84,19 @@ exec_command(cmd="nmap ...", timeout=180)
{"action": "exec_command", "params": {"cmd": "nmap ..."}}
```
The fix belongs on the inference server: it must be configured to parse the model's tool tokens into structured `tool_calls`. A correctly configured endpoint either returns a structured call or rejects the request outright. It never leaks the call as text.
The fix belongs on the inference server: it must be configured to parse the model's tool tokens into structured `tool_calls`. A correctly configured endpoint either returns a structured call or rejects the request outright — it never leaks the call as text.
### Fixes by server
**llama.cpp (`llama-server`)**
- Run with `--jinja` and a correct tool-use chat template (`--chat-template` / `--chat-template-file` matching the model). Recent builds enable `--jinja` by default. **Upgrade** if yours does not.
- For thinking models, align or disable reasoning (`--reasoning-format`, `-rea off`) so it does not break tool-call parsing.
- A low temperature (for example `--temp 0.2`) improves tool-call reliability.
- Run with `--jinja` and a correct tool-use chat template (`--chat-template` / `--chat-template-file` matching the model). Recent builds enable `--jinja` by default **upgrade** if yours doesn't.
- For thinking models, align or disable reasoning (`--reasoning-format`, `-rea off`) so it doesn't break tool-call parsing.
- A low temperature (e.g. `--temp 0.2`) improves tool-call reliability.
**Ollama**
- Use a recent Ollama and a model whose template wires tools. Modern Ollama refuses tools (`tools param requires --jinja flag`) if the template lacks tool support.
- For reasoning models (for example qwen3), disable the model's **thinking** mode: thinking left on frequently pushes the tool call into the text `content` instead of the structured `tool_calls` field. Turn it off on the Ollama side (a non-thinking model variant, or `think: false` in the model's parameters / `Modelfile`).
- Raise **`num_ctx`** to at least 16k32k. Strix sends a large system prompt plus many tool schemas. At Ollama's small default context the tool definitions are truncated out of the prompt and the model stops emitting valid calls. A short test prompt can look fine while a real scan fails, so set this explicitly rather than inferring it from a quick check.
- For reasoning models (e.g. qwen3), disable the model's **thinking** mode thinking left on frequently pushes the tool call into the text `content` instead of the structured `tool_calls` field. Turn it off on the Ollama side (a non-thinking model variant, or `think: false` in the model's parameters / `Modelfile`).
- Raise **`num_ctx`** to at least 16k32k. Strix sends a large system prompt plus many tool schemas; at Ollama's small default context the tool definitions are truncated out of the prompt and the model stops emitting valid calls. A short test prompt can look fine while a real scan fails, so set this explicitly rather than inferring it from a quick check.
**vLLM**
- Start with `--enable-auto-tool-choice`, a matching `--tool-call-parser` (`hermes`, `qwen3_xml`, or `llama3_json`), and a matching `--reasoning-parser` for reasoning models.
+5 -5
View File
@@ -3,7 +3,7 @@ title: "Novita AI"
description: "Configure Strix with Novita AI models"
---
[Novita AI](https://novita.ai) provides fast, cost-efficient inference for open-source models through an OpenAI-compatible API.
[Novita AI](https://novita.ai) provides fast, cost-efficient inference for open-source models via an OpenAI-compatible API.
## Setup
@@ -29,7 +29,7 @@ export LLM_API_BASE="https://api.novita.ai/openai"
## Benefits
- **Cost-efficient**: Competitive pricing with per-token billing
- **OpenAI-compatible**: Drop-in replacement using `LLM_API_BASE`
- **Large context**: Models support up to 262k token context windows
- **Function calling**: All listed models support tool/function calling
- **Cost-efficient** Competitive pricing with per-token billing
- **OpenAI-compatible** Drop-in replacement using `LLM_API_BASE`
- **Large context** Models support up to 262k token context windows
- **Function calling** All listed models support tool/function calling
+5 -5
View File
@@ -1,6 +1,6 @@
---
title: "OpenRouter"
description: "Configure Strix with models through OpenRouter"
description: "Configure Strix with models via OpenRouter"
---
[OpenRouter](https://openrouter.ai) provides access to 100+ models from multiple providers through a single API.
@@ -31,7 +31,7 @@ Access any model on OpenRouter using the format `openrouter/<provider>/<model>`:
## Benefits
- **Single API**: Access models from OpenAI, Anthropic, Google, Meta, and more
- **Fallback routing**: Automatic failover between providers
- **Cost tracking**: Monitor usage across all models
- **Higher rate limits**: OpenRouter handles provider limits for you
- **Single API** Access models from OpenAI, Anthropic, Google, Meta, and more
- **Fallback routing** Automatic failover between providers
- **Cost tracking** Monitor usage across all models
- **Higher rate limits** OpenRouter handles provider limits for you
+3 -3
View File
@@ -44,13 +44,13 @@ See the [Local Models guide](/llm-providers/local) for setup instructions and re
Access 100+ models through a single API.
</Card>
<Card title="Google Vertex AI" href="/llm-providers/vertex">
Gemini 3 models through Google Cloud.
Gemini 3 models via Google Cloud.
</Card>
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
Claude and Titan models through AWS.
Claude and Titan models via AWS.
</Card>
<Card title="Azure OpenAI" href="/llm-providers/azure">
GPT-5.4 through Azure.
GPT-5.4 via Azure.
</Card>
<Card title="Local Models" href="/llm-providers/local">
Llama 4, Mistral, and self-hosted models.
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: "Google Vertex AI"
description: "Configure Strix with Gemini models through Google Cloud"
description: "Configure Strix with Gemini models via Google Cloud"
---
## Installation
@@ -17,7 +17,7 @@ pipx install "strix-agent[vertex]"
export STRIX_LLM="vertex_ai/gemini-3-pro-preview"
```
No API key required: uses Google Cloud Application Default Credentials.
No API key requireduses Google Cloud Application Default Credentials.
## Authentication
+1 -1
View File
@@ -6,7 +6,7 @@ description: "Install Strix and run your first security scan"
## Prerequisites
- Docker (running)
- An LLM API key from any [supported provider](/llm-providers/overview) (OpenAI, Anthropic, Google)
- An LLM API key from any [supported provider](/llm-providers/overview) (OpenAI, Anthropic, Google, etc.)
## Installation
+1 -1
View File
@@ -3,7 +3,7 @@ title: "Browser"
description: "Playwright-powered Chrome for web application testing"
---
Strix uses a headless Chrome browser through Playwright to interact with web applications exactly like a real user would.
Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would.
## How It Works
+1 -1
View File
@@ -28,6 +28,6 @@ Strix agents use specialized tools to test your applications like a real penetra
| -------------- | ---------------------------------------- |
| Python Runtime | Write and execute custom exploit scripts |
| File Editor | Read and modify source code |
| Web Search | Real-time OSINT through Perplexity |
| Web Search | Real-time OSINT via Perplexity |
| Notes | Document findings during the scan |
| Reporting | Generate vulnerability reports with PoCs |
+10 -10
View File
@@ -70,9 +70,9 @@ asyncio.run(main())
| `view_sitemap_entry()` | Inspect one sitemap entry + its related requests |
| `scope_rules()` | Manage proxy scope (allowlist/denylist) |
For one-off arbitrary requests, use shell tooling like `curl`: the
For one-off arbitrary requests, use shell tooling like `curl` the
sandbox's `HTTP_PROXY` env routes the traffic through Caido
automatically, so it lands in `list_requests` and can be replayed through
automatically, so it lands in `list_requests` and can be replayed via
`repeat_request`.
### Example: Automated IDOR Testing
@@ -106,24 +106,24 @@ asyncio.run(main())
## Human-in-the-Loop
Strix exposes the Caido proxy to your host machine, so you can interact with it alongside the automated scan. When the sandbox starts, the Caido URL is displayed in the TUI sidebar: click it to copy, then open it in Caido Desktop.
Strix exposes the Caido proxy to your host machine, so you can interact with it alongside the automated scan. When the sandbox starts, the Caido URL is displayed in the TUI sidebar click it to copy, then open it in Caido Desktop.
### Accessing Caido
1. Start a scan as usual
2. Look for the **Caido** URL in the sidebar stats panel (for example `localhost:52341`)
2. Look for the **Caido** URL in the sidebar stats panel (e.g. `localhost:52341`)
3. Open the URL in Caido Desktop
4. Click **Continue as guest** to access the instance
### What You Can Do
- **Inspect traffic**: Browse all HTTP/HTTPS requests the agent is making in real time
- **Replay requests**: Take any captured request and resend it with your own modifications
- **Intercept and modify**: Pause requests mid-flight, edit them, then forward
- **Explore the sitemap**: See the full attack surface the agent has discovered
- **Manual testing**: Use Caido's tools to test findings the agent reports, or explore areas it has not reached
- **Inspect traffic** Browse all HTTP/HTTPS requests the agent is making in real time
- **Replay requests** Take any captured request and resend it with your own modifications
- **Intercept and modify** Pause requests mid-flight, edit them, then forward
- **Explore the sitemap** See the full attack surface the agent has discovered
- **Manual testing** Use Caido's tools to test findings the agent reports, or explore areas it hasn't reached
This turns Strix from a fully automated scanner into a collaborative tool: the agent handles the heavy lifting while you focus on the interesting parts.
This turns Strix from a fully automated scanner into a collaborative tool the agent handles the heavy lifting while you focus on the interesting parts.
## Scope
+7 -7
View File
@@ -14,14 +14,14 @@ strix (--target <target> | --target-list <path>) [options]
<ParamField path="--target, -t" type="string">
Target to test. Accepts URLs, repositories, local directories, domains, IP addresses, API spec files (OpenAPI/Swagger `.json`/`.yaml`, a Postman collection export), or a live Postman collection by id (`postman://<collection-uuid>`). Can be specified multiple times. Fresh runs require at least one target source: `--target` or `--target-list`.
When the target is an API spec, Strix copies it into the agent's workspace and authorizes the base URLs it declares (including those resolved from a Postman environment) as in-scope hosts - so the agent reads the contract and tests the full declared surface instead of discovering endpoints by crawling. Pair the spec with the deployed base URL (for example `--target ./openapi.yaml --target https://api.example.com`) so the agent has a reachable host to attack.
When the target is an API spec, Strix copies it into the agent's workspace and authorizes the base URLs it declares (including those resolved from a Postman environment) as in-scope hosts - so the agent reads the contract and tests the full declared surface instead of discovering endpoints by crawling. Pair the spec with the deployed base URL (e.g. `--target ./openapi.yaml --target https://api.example.com`) so the agent has a reachable host to attack.
<Note>
A local directory is mounted into the sandbox live and **writable**, so the agent edits your real files (`.git` excepted). Commit or stash first.
</Note>
<Note>
Fetching a Postman collection by id requires `POSTMAN_API_KEY`. Add `?env=<environment-uuid>` to also pull a Postman environment, which resolves `{{baseUrl}}` / token variables the collection references (for example `postman://<collection-uuid>?env=<environment-uid>`).
Fetching a Postman collection by id requires `POSTMAN_API_KEY`. Add `?env=<environment-uuid>` to also pull a Postman environment, which resolves `{{baseUrl}}` / token variables the collection references (e.g. `postman://<collection-uuid>?env=<environment-uid>`).
</Note>
</ParamField>
@@ -46,7 +46,7 @@ strix (--target <target> | --target-list <path>) [options]
</ParamField>
<ParamField path="--diff-base" type="string">
Target branch or commit to compare against (for example, `origin/main`). Defaults to the repository's default branch.
Target branch or commit to compare against (e.g., `origin/main`). Defaults to the repository's default branch.
</ParamField>
<ParamField path="--non-interactive, -n" type="boolean">
@@ -88,8 +88,8 @@ strix (--target <target> | --target-list <path>) [options]
slightly overshoot the limit by any calls already in flight when the
threshold is crossed (most relevant with several child agents running
concurrently).
- Cost is a best-effort estimate derived from token usage and model pricing.
Providers that do not expose priced usage may under-count.
- Cost is a best-effort estimate derived from token usage and model pricing;
providers that do not expose priced usage may under-count.
- For LiteLLM-routed models, Strix enables streaming success callbacks to
capture provider-reported cost. Message content remains excluded, but
third-party LiteLLM callbacks configured in the same process can receive
@@ -148,6 +148,6 @@ strix --target-list ./targets.txt
| Code | Meaning |
|------|---------|
| 0 | Scan completed successfully (interactive mode always exits `0`, in headless mode, `0` means no vulnerabilities were found) |
| 1 | A fatal error occurred before or during the scan (for example missing environment variables, Docker unavailable, invalid config file, diff-scope resolution failure, or an unhandled error) |
| 0 | Scan completed successfully (interactive mode always exits `0`; in headless mode, `0` means no vulnerabilities were found) |
| 1 | A fatal error occurred before or during the scan (e.g. missing environment variables, Docker unavailable, invalid config file, diff-scope resolution failure, or an unhandled error) |
| 2 | Vulnerabilities found (headless mode only) |
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "strix-agent"
version = "1.5.3"
version = "1.5.1"
description = "Open-source AI Hackers for your apps"
readme = "README.md"
license = "Apache-2.0"
+1 -7
View File
@@ -263,13 +263,7 @@ Remember: A single well-validated high-impact vulnerability is worth more than d
<multi_agent_system>
AGENT ISOLATION & SANDBOXING:
- All agents run in the same shared Docker container for efficiency
- Each agent has its own terminal sessions
- Browsers are NOT per-agent by default: `agent-browser` with no `--session` is one
shared browser, so a concurrent agent's navigation invalidates your page and refs.
Pass `--session <your-agent-name>` for any browser work of your own — then it is
yours alone. Each session is a full Chromium (~340 MB) on this shared box, so keep
one, not several, and `agent-browser --session <name> close` when you're done with
the target; an idle browser is reclaimed automatically after 3 minutes
- Each agent has its own: browser sessions, terminal sessions
- All agents share the same /workspace directory and proxy history
- Agents can see each other's files and proxy traffic for better collaboration
+5 -9
View File
@@ -652,31 +652,27 @@ def _install_openrouter_stream_cost_capture() -> None:
litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc]
OPENROUTER_ATTRIBUTION_HEADERS = {
_OPENROUTER_ATTRIBUTION_HEADERS = {
"HTTP-Referer": "https://strix.ai",
"X-Title": "Strix",
"X-OpenRouter-Categories": "cli-agent",
}
def is_openrouter_model(model_name: str | None) -> bool:
return bool(model_name) and "openrouter/" in (model_name or "").strip().lower()
def _configure_openrouter_attribution(model_name: str | None) -> None:
import litellm
current: object = litellm.headers
existing: dict[str, str] = current if isinstance(current, dict) else {}
if not is_openrouter_model(model_name):
if any(key in existing for key in OPENROUTER_ATTRIBUTION_HEADERS):
if not model_name or "openrouter/" not in model_name.strip().lower():
if any(key in existing for key in _OPENROUTER_ATTRIBUTION_HEADERS):
remaining = {
k: v for k, v in existing.items() if k not in OPENROUTER_ATTRIBUTION_HEADERS
k: v for k, v in existing.items() if k not in _OPENROUTER_ATTRIBUTION_HEADERS
}
litellm.headers = remaining or None # type: ignore[assignment]
return
litellm.headers = {**existing, **OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
litellm.headers = {**existing, **_OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
def _configure_extra_headers(llm: LlmSettings) -> None:
+2 -17
View File
@@ -10,12 +10,10 @@ from openai.types.shared import Reasoning
from strix.config.models import (
DEFAULT_MODEL_RETRY,
OPENROUTER_ATTRIBUTION_HEADERS,
bedrock_route_supports_prompt_caching,
is_bedrock_route,
is_claude_model,
is_known_openai_bare_model,
is_openrouter_model,
model_supports_reasoning,
request_timeout_extra_args,
)
@@ -203,15 +201,13 @@ def make_model_settings(
request_timeout: float | None = None,
prompt_cache: bool = True,
extra_headers: dict[str, str] | None = None,
has_tools: bool = True,
) -> ModelSettings:
headers = _request_headers(model_name, extra_headers)
model_settings = ModelSettings(
parallel_tool_calls=False if has_tools else None,
parallel_tool_calls=False,
retry=DEFAULT_MODEL_RETRY,
include_usage=True,
extra_args=request_timeout_extra_args(request_timeout),
extra_headers=headers,
extra_headers=dict(extra_headers) if extra_headers else None,
)
if (
reasoning_effort is not None
@@ -234,17 +230,6 @@ def make_model_settings(
return model_settings
def _request_headers(
model_name: str, extra_headers: dict[str, str] | None
) -> dict[str, str] | None:
headers: dict[str, str] = {}
if is_openrouter_model(model_name):
headers.update(OPENROUTER_ATTRIBUTION_HEADERS)
if extra_headers:
headers.update(extra_headers)
return headers or None
def _reasoning_settings(
effort: ReasoningEffort,
extra_args: dict[str, Any] | None,
-1
View File
@@ -224,7 +224,6 @@ async def warm_up_llm(show_model_warning: bool = True) -> None:
request_timeout=llm.timeout,
prompt_cache=False,
extra_headers=settings.dedupe.extra_headers,
has_tools=False,
)
if deduper_extra:
merged = {**(deduper_settings.extra_args or {}), **deduper_extra}
-1
View File
@@ -78,7 +78,6 @@ async def preflight_model_connection(
request_timeout=resolved_settings.llm.timeout,
prompt_cache=False,
extra_headers=resolved_settings.llm.extra_headers,
has_tools=False,
)
await asyncio.wait_for(
model.get_response(
-1
View File
@@ -294,7 +294,6 @@ async def _summarize(model: str, prompt: str, max_tokens: int) -> str | None:
request_timeout=llm.timeout,
prompt_cache=False,
extra_headers=llm.extra_headers,
has_tools=False,
).resolve(ModelSettings(max_tokens=max_tokens))
try:
response = (
-1
View File
@@ -62,7 +62,6 @@ def _dedupe_model_settings(
# must never receive the main endpoint's credentials. A dedicated model
# gets its own DEDUPE_LLM_EXTRA_HEADERS instead.
extra_headers=dedupe.extra_headers if dedupe.model else llm.extra_headers,
has_tools=False,
)
extra = _dedupe_extra_args(dedupe)
if extra:
-54
View File
@@ -1,54 +0,0 @@
"""LiteLLM model-name resolution for local cost estimates."""
from __future__ import annotations
from functools import lru_cache
from typing import Any, cast
@lru_cache(maxsize=512)
def resolve_litellm_model(model: str) -> str | None:
"""Return a provider-qualified model name that LiteLLM can price."""
try:
import litellm
normalized = model.strip()
for prefix in ("litellm/", "any-llm/", "openai/"):
if normalized.startswith(prefix):
normalized = normalized.removeprefix(prefix)
break
if not normalized:
return None
model_cost = cast(
"dict[str, dict[str, Any]]",
getattr(litellm, "model_cost"), # noqa: B009
)
bare_entry = model_cost.get(normalized)
if "/" not in normalized and isinstance(bare_entry, dict):
provider = bare_entry.get("litellm_provider")
if isinstance(provider, str) and provider:
return f"{provider}/{normalized}"
if "/" in normalized and isinstance(bare_entry, dict):
return normalized
names = [normalized]
if "/" in normalized:
names.append(normalized.rsplit("/", 1)[-1])
for name in names:
matches = sorted(key for key in model_cost if key.endswith(f"/{name}"))
if not matches:
continue
prices = {
(
model_cost[key].get("input_cost_per_token"),
model_cost[key].get("output_cost_per_token"),
)
for key in matches
if isinstance(model_cost.get(key), dict)
}
if len(matches) == 1 or len(prices) == 1:
return matches[0]
return None # noqa: TRY300
except Exception: # noqa: BLE001
return None
+2 -6
View File
@@ -14,7 +14,6 @@ from agents.usage import Usage
from strix.config import codex
from strix.config.loader import load_settings
from strix.core.paths import run_dir_for
from strix.report.pricing import resolve_litellm_model
from strix.report.sarif import write_sarif
from strix.report.usage import LLMUsageLedger
from strix.report.writer import (
@@ -697,13 +696,10 @@ def _estimate_response_cost(kwargs: Any, completion_response: Any) -> float | No
candidates.append(model.rsplit("/", 1)[-1])
for candidate in candidates:
resolved = resolve_litellm_model(candidate)
if not resolved:
continue
try:
value = completion_cost(
completion_response={"model": resolved, "usage": usage_payload},
model=resolved,
completion_response={"model": candidate, "usage": usage_payload},
model=candidate,
)
except Exception: # nosec B112 # noqa: BLE001, S112
continue
+29 -30
View File
@@ -7,8 +7,6 @@ from typing import Any
from agents.usage import Usage, deserialize_usage, serialize_usage
from strix.report.pricing import resolve_litellm_model
logger = logging.getLogger(__name__)
@@ -20,9 +18,7 @@ class LLMUsageLedger:
self._total_usage = Usage()
self._agent_usage: dict[str, Usage] = {}
self._agent_metadata: dict[str, dict[str, str]] = {}
self._observed_cost = 0.0
self._estimated_cost = 0.0
self._has_observed_cost = False
self._total_cost = 0.0
# When True, tokens are still tracked but cost stays $0 — the run is on a
# model subscription, so there is no metered per-token charge to report.
self.zero_cost = False
@@ -48,10 +44,10 @@ class LLMUsageLedger:
if model:
metadata["model"] = model
if not self.zero_cost:
if not self.zero_cost and not _is_litellm_routed(model):
estimated = _estimate_litellm_cost(usage, model)
if estimated:
self._estimated_cost += estimated
self._total_cost += estimated
return True
@@ -59,18 +55,15 @@ class LLMUsageLedger:
if self.zero_cost:
return
if isinstance(cost, int | float) and cost > 0:
self._observed_cost += float(cost)
self._has_observed_cost = True
self._total_cost += float(cost)
@property
def total_cost(self) -> float:
if self.zero_cost:
return 0.0
return _round_cost(self._observed_cost if self._has_observed_cost else self._estimated_cost)
return _round_cost(self._total_cost)
def to_record(self) -> dict[str, Any]:
record = serialize_usage(self._total_usage)
record["cost"] = self.total_cost
record["cost"] = _round_cost(self._total_cost)
record["agents"] = []
agent_tokens = {aid: _resolve_total_tokens(u) for aid, u in self._agent_usage.items()}
@@ -79,7 +72,7 @@ class LLMUsageLedger:
usage = self._agent_usage[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
self._total_cost * (agent_tokens[agent_id] / total_tokens) if total_tokens else 0.0
)
agent_record = serialize_usage(usage)
@@ -99,9 +92,7 @@ class LLMUsageLedger:
self._total_usage = Usage()
self._agent_usage.clear()
self._agent_metadata.clear()
self._observed_cost = 0.0
self._estimated_cost = 0.0
self._has_observed_cost = False
self._total_cost = 0.0
if not isinstance(raw_usage, dict):
return
@@ -112,9 +103,7 @@ class LLMUsageLedger:
logger.exception("Failed to hydrate aggregate llm_usage from run.json")
self._total_usage = Usage()
persisted_cost = _float_or_zero(raw_usage.get("cost"))
self._observed_cost = persisted_cost
self._estimated_cost = persisted_cost
self._total_cost = _float_or_zero(raw_usage.get("cost"))
for raw_agent in raw_usage.get("agents") or []:
if not isinstance(raw_agent, dict):
@@ -147,6 +136,15 @@ def _resolve_total_tokens(usage: Usage) -> int:
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:
return bool(
usage.requests
@@ -203,23 +201,24 @@ def _estimate_litellm_entry_cost(entry: Any, model: str) -> float | None:
candidates = [model]
if "/" in model:
candidates.append(model.rsplit("/", 1)[-1])
candidates.append(model.split("/", 1)[-1])
cost: Any = None
for candidate in candidates:
resolved = resolve_litellm_model(candidate)
if not resolved:
continue
try:
cost = completion_cost(
completion_response={"model": resolved, "usage": usage_payload},
model=resolved,
completion_response={"model": candidate, "usage": usage_payload},
model=model,
)
break
except Exception: # nosec B112 # noqa: BLE001, S112
continue
if cost > 0:
return float(cost)
logger.debug("LiteLLM cost estimate unavailable for model %s", model)
return None
if cost is None:
logger.debug("LiteLLM cost estimate unavailable for model %s", model)
return None
return cost if isinstance(cost, int | float) and cost >= 0 else None
def _litellm_model_name(model: str | None) -> str | None:
+2 -35
View File
@@ -58,26 +58,6 @@ agent-browser screenshot
The browser stays running across commands so these feel like a single
session. Use `agent-browser close` (or `close --all`) when you're done.
The default session is **shared with every other agent in the sandbox** — if
another agent navigates it, your page and your refs are gone from under you. So
claim your own by passing `--session <your-agent-name>` on **every** command:
```bash
agent-browser --session recon-3 open https://example.com
agent-browser --session recon-3 snapshot -i
agent-browser --session recon-3 close # when done with the target
```
The examples in the rest of this skill omit `--session` to keep them readable;
keep passing yours. Each session is a separate Chromium (~340 MB) on a shared
box, so hold one rather than several, and close it when you're finished.
A browser left idle for 3 minutes is reclaimed automatically to free memory for
the other agents; the next command relaunches it, but the page, tabs, refs and
cookies are gone. If you're authenticated and about to go do something else for a
while, save the state first (see
[Persist session across runs](#persist-session-across-runs)).
## Reading a page
```bash
@@ -327,16 +307,6 @@ agent-browser --session b fill @e1 "bob@test.com"
`AGENT_BROWSER_SESSION=myapp` sets the default session for the current
shell.
Use a session named after yourself for your own work — that's what keeps a
concurrent agent from navigating the page out from under you. Every session is a
separate Chromium though, so hold one at a time rather than a collection, and
close each one when its flow is finished:
```bash
agent-browser --session a close
agent-browser --session b close
```
### Mock network requests
```bash
@@ -398,11 +368,8 @@ agent-browser dialog dismiss # cancel
## Readiness & recovery
The first `agent-browser open` in a session launches the headless-Chrome
daemon; later commands reuse it. A daemon left idle for 3 minutes shuts itself
down to free memory for the other agents, so an `open` after a long gap is a
fresh browser rather than a resumed one — expect to re-navigate, and re-`state
load` if you were logged in. Distinguish the failure modes and react differently
— do **not** blindly re-run the same failing command in a loop:
daemon; later commands reuse it. Distinguish the two failure modes and react
differently — do **not** blindly re-run the same failing command in a loop:
- **Daemon / connection failure** (`Failed to connect`, `connection refused`,
socket missing, `browser not running`): the daemon isn't up or has died. Run
+1 -1
View File
@@ -143,7 +143,7 @@ def test_cost_callback_estimates_cost_with_bare_model_fallback() -> None:
}
def fake_completion_cost(**kwargs: object) -> float:
if kwargs["model"] == "openai/gpt-4o-mini":
if kwargs["model"] == "gpt-4o-mini":
return 0.025
raise ValueError(kwargs["model"])
-39
View File
@@ -299,16 +299,6 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
assert settings.tool_choice == "required"
def test_make_model_settings_disables_parallel_tool_calls_by_default() -> None:
assert make_model_settings("none", model_name="gpt-4o").parallel_tool_calls is False
def test_make_model_settings_omits_parallel_tool_calls_without_tools() -> None:
settings = make_model_settings("none", model_name="gpt-4o", has_tools=False)
assert settings.parallel_tool_calls is None
def test_make_model_settings_sets_request_timeout() -> None:
settings = make_model_settings(
"none",
@@ -361,32 +351,3 @@ def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
assert settings.extra_args is not None
assert settings.extra_args["timeout"] == 120.0
def test_openrouter_attribution_rides_on_the_request_headers() -> None:
# litellm.headers is ignored once a request carries any header of its own,
# so the attribution must be part of the per-request headers.
headers = make_model_settings(
None, model_name="openrouter/anthropic/claude-sonnet-4-5"
).extra_headers
assert headers == {
"HTTP-Referer": "https://strix.ai",
"X-Title": "Strix",
"X-OpenRouter-Categories": "cli-agent",
}
def test_openrouter_attribution_absent_for_other_providers() -> None:
assert make_model_settings(None, model_name="anthropic/claude-sonnet-4-5").extra_headers is None
def test_user_headers_override_openrouter_attribution() -> None:
headers = make_model_settings(
None,
model_name="openrouter/anthropic/claude-sonnet-4-5",
extra_headers={"X-Title": "Custom", "X-Tenant": "acme"},
).extra_headers
assert headers is not None
assert headers["X-Title"] == "Custom"
assert headers["X-Tenant"] == "acme"
assert headers["HTTP-Referer"] == "https://strix.ai"
-120
View File
@@ -1,120 +0,0 @@
from __future__ import annotations
from unittest.mock import patch
import litellm
from agents.usage import Usage
from strix.report.pricing import resolve_litellm_model
from strix.report.usage import LLMUsageLedger
def test_resolves_common_bare_model_names() -> None:
resolve_litellm_model.cache_clear()
assert resolve_litellm_model("deepseek-v4-flash") == "deepseek/deepseek-v4-flash"
assert resolve_litellm_model("openai/deepseek-v4-flash") == "deepseek/deepseek-v4-flash"
assert resolve_litellm_model("grok-4.5") == "xai/grok-4.5"
assert resolve_litellm_model("MiniMax-M3") == "minimax/MiniMax-M3"
def test_resolver_returns_none_for_unresolvable_model() -> None:
resolve_litellm_model.cache_clear()
assert resolve_litellm_model("provider/not-a-real-model") is None
def test_ledger_uses_estimate_when_routed_provider_reports_no_cost() -> None:
usage = Usage()
usage.requests = 1
usage.input_tokens = 1000
usage.output_tokens = 200
usage.total_tokens = 1200
ledger = LLMUsageLedger()
with patch("litellm.completion_cost", return_value=0.42):
ledger.record(agent_id="a", usage=usage, model="openai/deepseek-v4-flash")
assert ledger.total_cost == 0.42
def test_ledger_prefers_observed_cost_over_estimate() -> None:
usage = Usage()
usage.requests = 1
usage.input_tokens = 1000
usage.output_tokens = 200
usage.total_tokens = 1200
ledger = LLMUsageLedger()
with patch("litellm.completion_cost", return_value=0.42):
ledger.record(agent_id="a", usage=usage, model="openai/deepseek-v4-flash")
ledger.record_observed_cost(0.17)
assert ledger.total_cost == 0.17
def test_hydrated_estimate_continues_accumulating_new_estimates() -> None:
usage = Usage()
usage.requests = 1
usage.input_tokens = 1000
usage.output_tokens = 200
usage.total_tokens = 1200
ledger = LLMUsageLedger()
ledger.hydrate({"cost": 0.42})
with patch("litellm.completion_cost", return_value=0.17):
ledger.record(agent_id="a", usage=usage, model="openai/deepseek-v4-flash")
assert ledger.total_cost == 0.59
def test_zero_cost_disables_both_observed_and_estimated_costs() -> None:
usage = Usage()
usage.requests = 1
usage.input_tokens = 1000
usage.output_tokens = 200
usage.total_tokens = 1200
ledger = LLMUsageLedger()
ledger.zero_cost = True
with patch("litellm.completion_cost", return_value=0.42) as estimate:
ledger.record(agent_id="a", usage=usage, model="deepseek-v4-flash")
ledger.record_observed_cost(1.0)
estimate.assert_not_called()
assert ledger.total_cost == 0.0
def test_resolver_uses_provider_when_bare_entry_has_one() -> None:
original = litellm.model_cost
litellm.model_cost = {
"example": {
"litellm_provider": "example-provider",
"input_cost_per_token": 1.0,
"output_cost_per_token": 2.0,
}
}
try:
resolve_litellm_model.cache_clear()
assert resolve_litellm_model("example") == "example-provider/example"
finally:
litellm.model_cost = original
resolve_litellm_model.cache_clear()
def test_resolver_does_not_guess_between_differently_priced_providers() -> None:
original = litellm.model_cost
litellm.model_cost = {
"provider-a/example": {
"input_cost_per_token": 1.0,
"output_cost_per_token": 2.0,
},
"provider-b/example": {
"input_cost_per_token": 3.0,
"output_cost_per_token": 4.0,
},
}
try:
resolve_litellm_model.cache_clear()
assert resolve_litellm_model("example") is None
finally:
litellm.model_cost = original
resolve_litellm_model.cache_clear()
Generated
+1 -1
View File
@@ -2378,7 +2378,7 @@ wheels = [
[[package]]
name = "strix-agent"
version = "1.5.3"
version = "1.5.1"
source = { editable = "." }
dependencies = [
{ name = "caido-sdk-client" },