mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 17:27:26 +02:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca7bc2092 | ||
|
|
cb3fa2db51 | ||
|
|
5e8a773f3e | ||
|
|
13b7374d54 | ||
|
|
5ad2c96f9b | ||
|
|
067597cfdb | ||
|
|
b79483c6b7 | ||
|
|
2bc86902a4 | ||
|
|
c8b83d91db |
@@ -315,18 +315,6 @@ strix auth status # show the active sign-in
|
||||
strix auth logout # forget the sign-in
|
||||
```
|
||||
|
||||
#### Sign in with an OpenCode subscription
|
||||
|
||||
You can also run Strix on [OpenCode Zen](https://opencode.ai/docs/zen/) credits or an [OpenCode Go](https://opencode.ai/docs/go/) subscription:
|
||||
|
||||
```bash
|
||||
strix auth login opencode # paste your API key from opencode.ai/auth
|
||||
|
||||
export STRIX_LLM="opencode/claude-sonnet-5" # opencode/<model> runs on Zen credits
|
||||
export STRIX_LLM="opencode-go/kimi-k3" # opencode-go/<model> runs on the Go subscription
|
||||
strix --target ./app-directory
|
||||
```
|
||||
|
||||
**Recommended models for best results:**
|
||||
|
||||
- [OpenAI GPT-5.4](https://openai.com/api/) - `openai/gpt-5.4`
|
||||
|
||||
@@ -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 (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
|
||||
Model name in LiteLLM format, such as `openai/gpt-5.4` or `anthropic/claude-sonnet-4-6`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="LLM_API_KEY" type="string">
|
||||
@@ -20,8 +20,8 @@ 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 (e.g.
|
||||
`{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible
|
||||
Extra HTTP headers sent on every LLM request as a JSON object, such as
|
||||
`{"X-Feature-Key":"value","X-Tenant":"acme"}`. These headers help 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
|
||||
the LiteLLM and native OpenAI routing paths.
|
||||
@@ -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, e.g. `{"X-Feature-Key":"value"}`. A dedicated dedupe model never
|
||||
inherits `LLM_EXTRA_HEADERS`; set this when its endpoint needs custom headers.
|
||||
request, such as `{"X-Feature-Key":"value"}`. A dedicated dedupe model never
|
||||
inherits `LLM_EXTRA_HEADERS`. Set this variable when its endpoint needs custom headers.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_DEDUPE_REASONING_EFFORT" type="string">
|
||||
@@ -81,7 +81,7 @@ affecting the agents that do the actual testing.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="POSTMAN_API_KEY" type="string">
|
||||
Postman API key (`PMAK-…`). Enables fetching Postman collections by id as a target (`postman://<collection-uid>`), and Postman environments (`postman://<collection-uid>?env=<environment-uid>`) to resolve collection variables. Not needed when passing a local collection export file.
|
||||
Postman API key (`PMAK-…`). Enables fetching Postman collections by ID as a target (`postman://<collection-uid>`), and Postman environments (`postman://<collection-uid>?env=<environment-uid>`) to resolve collection variables. Not needed when passing a local collection export file.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="STRIX_TELEMETRY" default="1" type="string">
|
||||
|
||||
@@ -3,11 +3,11 @@ title: "Skills"
|
||||
description: "Specialized knowledge packages that enhance agent capabilities"
|
||||
---
|
||||
|
||||
Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methodologies.
|
||||
Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methods.
|
||||
|
||||
## 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 use, such as parser 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 examples, such as working payloads, commands, and test cases
|
||||
4. Provide validation methods to confirm findings
|
||||
5. Submit via PR
|
||||
5. Submit a pull request
|
||||
|
||||
## 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 a pull request:** 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
@@ -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 agents are autonomous and act like real hackers. They run your code dynamically, find vulnerabilities, and validate each vulnerability with a proof of concept. Strix helps developers and security teams that need fast and accurate security testing. Strix does not have the overhead of a manual pentest or the false positives of a static analysis tool.
|
||||
|
||||
<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 d
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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), including 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 with the self-hosted CLI or the managed cloud, apply 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,25 @@ 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:
|
||||
Both use the same engine and produce the same validated findings and SARIF. 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 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.
|
||||
- **Open-source CLI (self-hosted):** Runs locally in a Docker sandbox with your own LLM key. It is free, fully local, and air-gap capable. It suits local development loops and full control.
|
||||
- **Managed cloud:** Runs on Strix infrastructure through the [app.strix.ai REST API](https://docs.app.strix.ai). It needs no Docker, LLM key, or local installation. The Enterprise plan adds team dashboards, scheduling, pull request reviews, and downloadable PDF or DOCX reports. It suits sandboxed or CI environments and teams.
|
||||
|
||||
Create a managed API token under **Settings → API Access**. The `managed-pentesting-with-strix` skill documents 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. It exits with `0` for a clean scan, `1` for an error, or `2` for vulnerabilities.
|
||||
- **REST API:** The managed platform exposes a documented [OpenAPI](https://docs.app.strix.ai/openapi.json) at `https://app.strix.ai/api/v1`. It supports scans, vulnerabilities, assets, pull request reviews, schedules, and webhooks. The API uses bearer tokens and scopes.
|
||||
- **Structured results:** Each self-hosted run writes `vulnerabilities.json`, `vulnerabilities.csv`, and `findings.sarif` in SARIF 2.1.0 format. It also writes per-finding Markdown under `strix_runs/<run-name>/`. The cloud exposes the same data as JSON and provides SARIF export.
|
||||
- **Budget controls:** `--max-budget` and `--max-turns` set cost and turn limits.
|
||||
- **`AGENTS.md`:** The [repository's agent guide](https://github.com/usestrix/strix/blob/main/AGENTS.md) provides a quick reference.
|
||||
- **`llms.txt`:** The index is available at [docs.strix.ai/llms.txt](https://docs.strix.ai/llms.txt). The full export is available 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
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Add these secrets to your repository:
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `STRIX_LLM` | Model name (e.g., `openai/gpt-5.4`) |
|
||||
| `STRIX_LLM` | Model name, such as `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 found |
|
||||
| 2 | Fail. Vulnerabilities found |
|
||||
|
||||
## Scan Modes for CI
|
||||
|
||||
@@ -62,5 +62,5 @@ Use `quick` mode for PRs to keep feedback fast. Schedule `deep` scans nightly.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
For pull_request workflows, Strix automatically uses changed-files diff-scope in CI/headless runs. If diff resolution fails, ensure full history is fetched (`fetch-depth: 0`) or set `--diff-base`.
|
||||
For `pull_request` workflows, Strix automatically uses changed-files diff-scope in CI/headless runs. If diff resolution fails, fetch full history with `fetch-depth: 0` or set `--diff-base`.
|
||||
</Note>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Azure OpenAI"
|
||||
description: "Configure Strix with OpenAI models via Azure"
|
||||
description: "Configure Strix with OpenAI models through 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 (e.g., `2025-11-01-preview`) |
|
||||
| `AZURE_API_VERSION` | API version, such as `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 (e.g., GPT-5.4)
|
||||
2. Deploy a model, such as GPT-5.4
|
||||
3. Get the endpoint URL and API key from the Azure portal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "AWS Bedrock"
|
||||
description: "Configure Strix with models via AWS Bedrock"
|
||||
description: "Configure Strix with models through 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.
|
||||
Strix does not require an API key. Strix uses AWS credentials from the environment.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Running Strix with local models allows for completely offline, privacy-first sec
|
||||
<Warning>
|
||||
**Compatibility Note**: Strix relies on advanced agentic capabilities (tool use, multi-step planning, self-correction). Most local models, especially those under 70B parameters, struggle with these complex tasks.
|
||||
|
||||
For critical assessments, we strongly recommend using state-of-the-art cloud models like **Claude 4.5 Sonnet** or **GPT-5**. Use local models only when privacy is the absolute priority.
|
||||
For critical assessments, use state-of-the-art cloud models such as **Claude 4.5 Sonnet** or **GPT-5**. Use local models only when privacy is the absolute priority.
|
||||
</Warning>
|
||||
|
||||
## Ollama
|
||||
@@ -40,8 +40,6 @@ For critical assessments, we strongly recommend using state-of-the-art cloud mod
|
||||
### Recommended Models
|
||||
|
||||
We recommend these models for the best balance of reasoning and tool use:
|
||||
|
||||
**Recommended models:**
|
||||
- **Qwen3 VL** (`ollama pull qwen3-vl`)
|
||||
- **DeepSeek V3.1** (`ollama pull deepseek-v3.1`)
|
||||
- **Devstral 2** (`ollama pull devstral-2`)
|
||||
@@ -59,7 +57,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. Strix sends these headers on every request:
|
||||
|
||||
```bash
|
||||
export STRIX_LLM="openai/your-model"
|
||||
@@ -69,12 +67,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`. Do not 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 or tool call. If the inference server returns the call as plain assistant text, Strix never sees a call it can execute. The agent makes no real progress and gives up after its recovery attempts end.
|
||||
|
||||
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 +82,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 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.
|
||||
- Run with `--jinja` and a correct tool-use chat template (`--chat-template` or `--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` or `-rea off`) so it does not break tool-call parsing.
|
||||
- A low temperature, such as `--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 (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 16k–32k. 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.
|
||||
- Use a recent Ollama and a model whose template wires tools. Modern Ollama returns `tools param requires --jinja flag` if the template lacks tool support.
|
||||
- For reasoning models such as qwen3, disable the model's **thinking** mode. Thinking left on can push the tool call into `content` instead of the structured `tool_calls` field. Turn it off on the Ollama side with a non-thinking model variant, `think: false` in the model's parameters, or `Modelfile`.
|
||||
- Raise **`num_ctx`** to at least 16k to 32k. Strix sends a large system prompt plus many tool schemas. At Ollama's small default context, the tool definitions can be truncated from the prompt. The model can then stop emitting valid calls. A short test prompt can look fine while a real scan fails. Set this explicitly instead of 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.
|
||||
|
||||
@@ -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 via an OpenAI-compatible API.
|
||||
[Novita AI](https://novita.ai) provides fast, cost-efficient inference for open-source models through 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "OpenRouter"
|
||||
description: "Configure Strix with models via OpenRouter"
|
||||
description: "Configure Strix with models through 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
|
||||
|
||||
@@ -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 via Google Cloud.
|
||||
Gemini 3 models through Google Cloud.
|
||||
</Card>
|
||||
<Card title="AWS Bedrock" href="/llm-providers/bedrock">
|
||||
Claude and Titan models via AWS.
|
||||
Claude and Titan models through AWS.
|
||||
</Card>
|
||||
<Card title="Azure OpenAI" href="/llm-providers/azure">
|
||||
GPT-5.4 via Azure.
|
||||
GPT-5.4 through Azure.
|
||||
</Card>
|
||||
<Card title="Local Models" href="/llm-providers/local">
|
||||
Llama 4, Mistral, and self-hosted models.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Google Vertex AI"
|
||||
description: "Configure Strix with Gemini models via Google Cloud"
|
||||
description: "Configure Strix with Gemini models through 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.
|
||||
Strix does not require an API key. Strix uses Google Cloud Application Default Credentials.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
||||
+3
-2
@@ -6,7 +6,8 @@ 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, etc.)
|
||||
- Access to a [supported LLM provider](/llm-providers/overview), such as OpenAI, Anthropic, or Google
|
||||
- Most providers require an API key. Vertex and Bedrock use cloud credentials.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -43,7 +44,7 @@ strix --target ./your-app
|
||||
```
|
||||
|
||||
<Note>
|
||||
First run pulls the Docker sandbox image automatically. Results are saved to `strix_runs/<run-name>`.
|
||||
The first run pulls the Docker sandbox image automatically. Strix saves results to `strix_runs/<run-name>`.
|
||||
</Note>
|
||||
|
||||
## Target Types
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "Browser"
|
||||
description: "Playwright-powered Chrome for web application testing"
|
||||
---
|
||||
|
||||
Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would.
|
||||
Strix uses a headless Chrome browser through Playwright to interact with web applications exactly like a real user would.
|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
@@ -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 via Perplexity |
|
||||
| Web Search | Real-time OSINT through Perplexity |
|
||||
| Notes | Document findings during the scan |
|
||||
| Reporting | Generate vulnerability reports with PoCs |
|
||||
|
||||
+11
-12
@@ -70,10 +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
|
||||
sandbox's `HTTP_PROXY` env routes the traffic through Caido
|
||||
automatically, so it lands in `list_requests` and can be replayed via
|
||||
`repeat_request`.
|
||||
For one-off arbitrary requests, use shell tools such as `curl`.
|
||||
The sandbox routes traffic through Caido with the `HTTP_PROXY` variable.
|
||||
Caido then adds each request to `list_requests` for replay through `repeat_request`.
|
||||
|
||||
### Example: Automated IDOR Testing
|
||||
|
||||
@@ -106,24 +105,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 the URL to copy it, 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 (e.g. `localhost:52341`)
|
||||
2. Find the **Caido** URL in the sidebar stats panel, such as `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 hasn't 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 has not 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.
|
||||
Strix is a collaborative tool, not only a fully automated scanner. The agent handles the heavy lifting while you focus on the interesting parts.
|
||||
|
||||
## Scope
|
||||
|
||||
|
||||
+7
-7
@@ -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 (e.g. `--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 workspace and authorizes its declared base URLs as in-scope hosts. Strix also authorizes base URLs that it resolves from a Postman environment. The agent then reads the contract and tests the full declared surface instead of finding endpoints by crawling. Pair the spec with the deployed base URL, such as `--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 (e.g. `postman://<collection-uuid>?env=<environment-uid>`).
|
||||
Fetching a Postman collection by ID requires `POSTMAN_API_KEY`. Add `?env=<environment-uuid>` to also fetch a Postman environment, which resolves the `{{baseUrl}}` and token variables that the collection references. Use a target such as `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 (e.g., `origin/main`). Defaults to the repository's default branch.
|
||||
Target branch or commit to compare against, such as `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 (e.g. missing environment variables, Docker unavailable, invalid config file, diff-scope resolution failure, or an unhandled error) |
|
||||
| 0 | Interactive mode always exits with `0`. In headless mode, `0` means that no vulnerabilities were found. |
|
||||
| 1 | A fatal error occurred before or during the scan. Causes include missing environment variables, unavailable Docker, an invalid config file, diff-scope resolution failure, or an unhandled error. |
|
||||
| 2 | Vulnerabilities found (headless mode only) |
|
||||
|
||||
@@ -280,7 +280,6 @@ ignore = [
|
||||
# a runtime ``Callable`` annotation on ``vulnerability_found_callback``.
|
||||
"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401", "PLC0415"]
|
||||
"strix/report/usage.py" = ["PLC0415"]
|
||||
"strix/report/pricing.py" = ["PLC0415"]
|
||||
# Lazy import of strix.config.models avoids a circular dependency between the
|
||||
# report pipeline and the config layer.
|
||||
"strix/report/dedupe.py" = ["PLC0415"]
|
||||
|
||||
+13
-27
@@ -72,32 +72,8 @@ def _write_store(data: dict[str, Any]) -> None:
|
||||
write_secret_text(AUTH_PATH, json.dumps(data, indent=2))
|
||||
|
||||
|
||||
def read_provider_record(provider: str) -> dict[str, Any] | None:
|
||||
"""Raw record for *provider* from the shared subscription-auth store."""
|
||||
record = _read_store().get(provider)
|
||||
return record if isinstance(record, dict) else None
|
||||
|
||||
|
||||
def save_provider_record(provider: str, record: dict[str, Any]) -> None:
|
||||
data = _read_store()
|
||||
data[provider] = record
|
||||
_write_store(data)
|
||||
|
||||
|
||||
def remove_provider_record(provider: str) -> None:
|
||||
data = _read_store()
|
||||
if provider not in data:
|
||||
return
|
||||
del data[provider]
|
||||
if data:
|
||||
_write_store(data)
|
||||
return
|
||||
with contextlib.suppress(OSError):
|
||||
AUTH_PATH.unlink()
|
||||
|
||||
|
||||
def read_record() -> dict[str, Any] | None:
|
||||
record = read_provider_record(PROVIDER)
|
||||
record = _read_store().get(PROVIDER)
|
||||
if not isinstance(record, dict) or record.get("type") != "oauth":
|
||||
return None
|
||||
if not (record.get("access") and record.get("refresh") and record.get("account_id")):
|
||||
@@ -110,11 +86,21 @@ def is_authenticated() -> bool:
|
||||
|
||||
|
||||
def save_record(record: dict[str, Any]) -> None:
|
||||
save_provider_record(PROVIDER, record)
|
||||
data = _read_store()
|
||||
data[PROVIDER] = record
|
||||
_write_store(data)
|
||||
|
||||
|
||||
def logout() -> None:
|
||||
remove_provider_record(PROVIDER)
|
||||
data = _read_store()
|
||||
if PROVIDER not in data:
|
||||
return
|
||||
del data[PROVIDER]
|
||||
if data:
|
||||
_write_store(data)
|
||||
return
|
||||
with contextlib.suppress(OSError):
|
||||
AUTH_PATH.unlink()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
||||
+9
-41
@@ -20,7 +20,6 @@ from agents.model_settings import ModelSettings
|
||||
from agents.models.fake_id import FAKE_RESPONSES_ID
|
||||
from agents.models.interface import Model
|
||||
from agents.models.multi_provider import MultiProvider
|
||||
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
||||
from agents.models.openai_responses import OpenAIResponsesModel
|
||||
from agents.retry import (
|
||||
ModelRetryBackoffSettings,
|
||||
@@ -37,7 +36,7 @@ from openai.types.responses import (
|
||||
from openai.types.responses.response_usage import ResponseUsage
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from strix.config import codex, opencode
|
||||
from strix.config import codex
|
||||
from strix.config.loader import load_settings
|
||||
from strix.config.tool_call_ids import TurnCallIdRewriter, dedupe_input
|
||||
from strix.config.tool_call_limits import TurnToolCallLimiter
|
||||
@@ -80,12 +79,7 @@ def _retry_statusless_provider_errors(context: RetryPolicyContext) -> bool:
|
||||
|
||||
|
||||
class _CodexResponsesModel(OpenAIResponsesModel):
|
||||
"""Responses model for stateless subscription gateways (always streamed).
|
||||
|
||||
Used for the ChatGPT subscription backend and for Responses-served models on
|
||||
the OpenCode gateway: neither stores responses server-side, so reasoning is
|
||||
carried inline via ``reasoning.encrypted_content``.
|
||||
"""
|
||||
"""Responses model for the ChatGPT subscription backend (always streamed, stateless)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -477,7 +471,6 @@ class StrixProvider(MultiProvider):
|
||||
def get_model(self, model_name: str | None) -> Model:
|
||||
llm = load_settings().llm
|
||||
slug = codex.subscription_model(model_name)
|
||||
oc = opencode.subscription_model(model_name)
|
||||
idle_timeout = float(llm.stream_idle_timeout)
|
||||
if slug:
|
||||
# The ChatGPT subscription backend is always streamed; it has no
|
||||
@@ -488,19 +481,6 @@ class StrixProvider(MultiProvider):
|
||||
codex.get_subscription_client(),
|
||||
reasoning_effort=llm.reasoning_effort,
|
||||
)
|
||||
elif oc and oc.uses_responses:
|
||||
model = _CodexResponsesModel(
|
||||
oc.slug,
|
||||
opencode.get_subscription_client(oc.base_url),
|
||||
reasoning_effort=llm.reasoning_effort,
|
||||
)
|
||||
elif oc:
|
||||
model = OpenAIChatCompletionsModel(
|
||||
oc.slug, opencode.get_subscription_client(oc.base_url)
|
||||
)
|
||||
if llm.disable_streaming:
|
||||
model = _NonStreamingModel(model)
|
||||
idle_timeout = 0.0
|
||||
else:
|
||||
model = super().get_model(model_name)
|
||||
if llm.disable_streaming:
|
||||
@@ -560,24 +540,15 @@ RECOMMENDED_MODEL_NAMES = (
|
||||
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
|
||||
|
||||
FRONTIER_MODEL_FAMILIES = (
|
||||
(("azure", "azure_ai", "bedrock_mantle", "chatgpt", "openai", "opencode"), ("gpt-5",)),
|
||||
(("azure", "azure_ai", "bedrock_mantle", "chatgpt", "openai"), ("gpt-5",)),
|
||||
(
|
||||
(
|
||||
"anthropic",
|
||||
"azure_ai",
|
||||
"bedrock",
|
||||
"claude",
|
||||
"databricks",
|
||||
"opencode",
|
||||
"snowflake",
|
||||
"vertex_ai",
|
||||
),
|
||||
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
|
||||
("claude-fable-5", "claude-opus-5", "claude-opus-4", "claude-sonnet-5", "claude-sonnet-4"),
|
||||
),
|
||||
(("google", "gemini", "opencode", "vertex_ai"), ("gemini-3",)),
|
||||
(("deepseek", "opencode"), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "opencode", "qwen"), ("qwen3.8", "qwen3.7", "qwen3-max")),
|
||||
(("kimi", "moonshot", "moonshotai", "opencode"), ("kimi-k3", "kimi-k2.7", "kimi-k2.6")),
|
||||
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
|
||||
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
|
||||
(("alibaba", "dashscope", "qwen"), ("qwen3.8", "qwen3.7", "qwen3-max")),
|
||||
(("moonshot", "moonshotai", "kimi"), ("kimi-k3", "kimi-k2.7", "kimi-k2.6")),
|
||||
)
|
||||
|
||||
|
||||
@@ -585,7 +556,7 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
llm = settings.llm
|
||||
set_tracing_disabled(True)
|
||||
if codex.subscription_model(llm.model) or opencode.subscription_model(llm.model):
|
||||
if codex.subscription_model(llm.model):
|
||||
return
|
||||
_configure_litellm_compatibility()
|
||||
_configure_openrouter_attribution(llm.model)
|
||||
@@ -770,9 +741,6 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo
|
||||
"""Return whether the resolved SDK route can only receive JSON function tools."""
|
||||
if codex.subscription_model(model_name):
|
||||
return False
|
||||
oc = opencode.subscription_model(model_name)
|
||||
if oc:
|
||||
return not oc.uses_responses
|
||||
model = model_name.strip().lower()
|
||||
if "/" in model and not model.startswith("openai/"):
|
||||
return True
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
"""OpenCode subscription auth: API-key sign-in and the OpenAI clients that
|
||||
route inference through the OpenCode gateway.
|
||||
|
||||
Covers both OpenCode offerings — Zen (pay-as-you-go credits) and Go (the
|
||||
monthly subscription) — which share one account and API key but live behind
|
||||
different gateway base URLs. Unlike the ChatGPT subscription there is no
|
||||
OAuth: the user copies a plain API key from https://opencode.ai/auth, and
|
||||
using the gateway from other agents is officially supported.
|
||||
|
||||
Model routing follows the endpoint each model is served on (see
|
||||
https://opencode.ai/docs/zen/): GPT models use the Responses API, everything
|
||||
else the OpenAI-compatible Chat Completions API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import requests
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from strix.config import codex
|
||||
|
||||
|
||||
PROVIDER = "opencode"
|
||||
|
||||
ZEN_BASE_URL = "https://opencode.ai/zen/v1"
|
||||
GO_BASE_URL = "https://opencode.ai/zen/go/v1"
|
||||
|
||||
# ``opencode/<model>`` runs on Zen credits; ``opencode-go/<model>`` on the Go
|
||||
# subscription (matching OpenCode's own ``opencode-go/`` model ids).
|
||||
ZEN_PREFIX = "opencode/"
|
||||
GO_PREFIX = "opencode-go/"
|
||||
|
||||
AUTH_CONSOLE_URL = "https://opencode.ai/auth"
|
||||
|
||||
_KEY_CHECK_TIMEOUT = 30
|
||||
|
||||
|
||||
class OpencodeAuthError(Exception):
|
||||
def __init__(self, code: str, message: str | None = None) -> None:
|
||||
self.code = code
|
||||
super().__init__(message or code)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SubscriptionModel:
|
||||
slug: str
|
||||
base_url: str
|
||||
uses_responses: bool
|
||||
|
||||
|
||||
def _uses_responses(slug: str, base_url: str) -> bool:
|
||||
lowered = slug.lower()
|
||||
if lowered.startswith("gpt-"):
|
||||
return True
|
||||
# Grok is served via Responses on Zen but Chat Completions on Go.
|
||||
return lowered.startswith("grok") and base_url == ZEN_BASE_URL
|
||||
|
||||
|
||||
def subscription_model(model_name: str | None) -> SubscriptionModel | None:
|
||||
"""The gateway model behind an ``opencode/`` or ``opencode-go/`` STRIX_LLM."""
|
||||
name = (model_name or "").strip()
|
||||
lowered = name.lower()
|
||||
for prefix, base_url in ((GO_PREFIX, GO_BASE_URL), (ZEN_PREFIX, ZEN_BASE_URL)):
|
||||
if lowered.startswith(prefix):
|
||||
slug = name[len(prefix) :]
|
||||
if not slug:
|
||||
return None
|
||||
return SubscriptionModel(slug, base_url, _uses_responses(slug, base_url))
|
||||
return None
|
||||
|
||||
|
||||
def read_record() -> dict[str, Any] | None:
|
||||
record = codex.read_provider_record(PROVIDER)
|
||||
if not isinstance(record, dict) or record.get("type") != "api_key":
|
||||
return None
|
||||
key = record.get("key")
|
||||
if not isinstance(key, str) or not key:
|
||||
return None
|
||||
return record
|
||||
|
||||
|
||||
def is_authenticated() -> bool:
|
||||
return read_record() is not None
|
||||
|
||||
|
||||
def save_api_key(key: str) -> None:
|
||||
codex.save_provider_record(PROVIDER, {"type": "api_key", "provider": PROVIDER, "key": key})
|
||||
|
||||
|
||||
def logout() -> None:
|
||||
codex.remove_provider_record(PROVIDER)
|
||||
|
||||
|
||||
def get_api_key() -> str:
|
||||
record = read_record()
|
||||
if record is None:
|
||||
raise OpencodeAuthError(
|
||||
"not_authenticated", "not signed in; run: strix auth login opencode"
|
||||
)
|
||||
return str(record["key"])
|
||||
|
||||
|
||||
def validate_api_key(key: str) -> None:
|
||||
"""Check the key against the gateway's models endpoint; raise if rejected."""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{ZEN_BASE_URL}/models",
|
||||
headers={"Authorization": f"Bearer {key}"},
|
||||
timeout=_KEY_CHECK_TIMEOUT,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
raise OpencodeAuthError("unavailable", str(exc)) from exc
|
||||
if response.status_code in (401, 403):
|
||||
raise OpencodeAuthError(
|
||||
"invalid_key", f"OpenCode rejected the API key (HTTP {response.status_code})"
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
raise OpencodeAuthError("http_error", f"HTTP {response.status_code}: {response.text[:300]}")
|
||||
|
||||
|
||||
def build_openai_client(base_url: str) -> AsyncOpenAI:
|
||||
return AsyncOpenAI(
|
||||
api_key=get_api_key(),
|
||||
base_url=base_url,
|
||||
http_client=httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0)),
|
||||
)
|
||||
|
||||
|
||||
_subscription_clients: dict[str, AsyncOpenAI] = {}
|
||||
|
||||
|
||||
def get_subscription_client(base_url: str) -> AsyncOpenAI:
|
||||
client = _subscription_clients.get(base_url)
|
||||
if client is None:
|
||||
client = build_openai_client(base_url)
|
||||
_subscription_clients[base_url] = client
|
||||
return client
|
||||
|
||||
|
||||
def auth_mode(model_name: str | None) -> str:
|
||||
"""Return "subscription" when STRIX_LLM runs on any subscription
|
||||
(OpenCode or ChatGPT), else "api_key"."""
|
||||
if subscription_model(model_name) or codex.subscription_model(model_name):
|
||||
return "subscription"
|
||||
return "api_key"
|
||||
|
||||
|
||||
def subscription_provider(model_name: str | None) -> str | None:
|
||||
"""The subscription behind STRIX_LLM: "opencode", "chatgpt", or None."""
|
||||
if subscription_model(model_name):
|
||||
return PROVIDER
|
||||
if codex.subscription_model(model_name):
|
||||
return "chatgpt"
|
||||
return None
|
||||
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any
|
||||
from agents.model_settings import ModelSettings
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from strix.config import opencode
|
||||
from strix.config.models import (
|
||||
DEFAULT_MODEL_RETRY,
|
||||
OPENROUTER_ATTRIBUTION_HEADERS,
|
||||
@@ -273,10 +272,6 @@ def _prompt_cache_extra_args(model_name: str) -> dict[str, Any] | None:
|
||||
"""
|
||||
if not is_claude_model(model_name):
|
||||
return None
|
||||
# OpenCode routes use the raw OpenAI SDK, which rejects this LiteLLM-only
|
||||
# argument; the gateway applies Anthropic prompt caching itself.
|
||||
if opencode.subscription_model(model_name):
|
||||
return None
|
||||
if is_bedrock_route(model_name) and not bedrock_route_supports_prompt_caching(model_name):
|
||||
return None
|
||||
|
||||
|
||||
+19
-105
@@ -1,9 +1,8 @@
|
||||
"""`strix auth` — subscription sign-in (login / status / logout).
|
||||
"""`strix auth` — ChatGPT subscription sign-in (login / status / logout).
|
||||
|
||||
Signing in only stores credentials (``~/.strix/subscription-auth.json``); model
|
||||
Signing in only stores OAuth tokens (``~/.strix/subscription-auth.json``); model
|
||||
selection stays with ``STRIX_LLM``. A ``chatgpt/<model>`` STRIX_LLM runs on the
|
||||
ChatGPT subscription; ``opencode/<model>`` (Zen credits) or
|
||||
``opencode-go/<model>`` (Go subscription) run on OpenCode.
|
||||
subscription.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +21,7 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import codex, load_settings, opencode
|
||||
from strix.config import codex, load_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -33,20 +32,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_CALLBACK_TIMEOUT_S = 300
|
||||
|
||||
# CLI-facing name for the default login provider. Internally this is the Codex
|
||||
# OAuth flow (``codex.PROVIDER``), but users know it as ChatGPT, so that's what
|
||||
# the command and messaging say. ``codex`` is accepted as an alias.
|
||||
# CLI-facing name for the login provider. Internally this is the Codex OAuth
|
||||
# flow (``codex.PROVIDER``), but users know it as ChatGPT, so that's what the
|
||||
# command and messaging say. ``codex`` is accepted as an alias.
|
||||
LOGIN_PROVIDER = "chatgpt"
|
||||
_ACCEPTED_PROVIDERS = frozenset({LOGIN_PROVIDER, codex.PROVIDER})
|
||||
_OPENCODE_PROVIDERS = frozenset({opencode.PROVIDER, "opencode-go", "zen"})
|
||||
|
||||
_USAGE = (
|
||||
"Usage:\n"
|
||||
" strix auth login chatgpt [--manual]\n"
|
||||
" strix auth login opencode\n"
|
||||
" strix auth status\n"
|
||||
" strix auth logout [chatgpt|opencode]"
|
||||
)
|
||||
_USAGE = "Usage:\n strix auth login chatgpt [--manual]\n strix auth status\n strix auth logout"
|
||||
|
||||
|
||||
def run_auth(argv: list[str]) -> int:
|
||||
@@ -63,7 +55,7 @@ def run_auth(argv: list[str]) -> int:
|
||||
handlers: dict[str, Callable[[], int]] = {
|
||||
"login": lambda: _login(console, rest),
|
||||
"status": lambda: _status(console),
|
||||
"logout": lambda: _logout(console, rest),
|
||||
"logout": lambda: _logout(console),
|
||||
}
|
||||
handler = handlers.get(subcommand)
|
||||
if handler is not None:
|
||||
@@ -92,14 +84,10 @@ def _login(console: Console, argv: list[str]) -> int:
|
||||
except SystemExit as exc: # argparse already printed the message
|
||||
return int(exc.code or 2)
|
||||
|
||||
if args.provider.lower() in _OPENCODE_PROVIDERS:
|
||||
return _login_opencode(console)
|
||||
|
||||
if args.provider.lower() not in _ACCEPTED_PROVIDERS:
|
||||
console.print(
|
||||
f"[red]Unsupported provider:[/] {args.provider}. "
|
||||
f"Supported: '{LOGIN_PROVIDER}' (ChatGPT subscription) and "
|
||||
f"'{opencode.PROVIDER}' (OpenCode Zen/Go)."
|
||||
f"Only '{LOGIN_PROVIDER}' (ChatGPT subscription) is supported."
|
||||
)
|
||||
return 2
|
||||
|
||||
@@ -127,63 +115,6 @@ def _login(console: Console, argv: list[str]) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _login_opencode(console: Console) -> int:
|
||||
console.print()
|
||||
console.print("[bold]Signing in with OpenCode[/] [dim](provider: opencode)[/]")
|
||||
console.print(
|
||||
"[dim]This uses your OpenCode Zen credits or Go subscription for inference.\n"
|
||||
f"Get your API key at {opencode.AUTH_CONSOLE_URL}[/]"
|
||||
)
|
||||
console.print()
|
||||
try:
|
||||
key = console.input("Paste your OpenCode API key: ", password=True).strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
console.print("\n[yellow]Sign-in cancelled.[/]")
|
||||
return 130
|
||||
if not key:
|
||||
console.print("[red]No API key provided.[/]")
|
||||
return 2
|
||||
try:
|
||||
opencode.validate_api_key(key)
|
||||
except opencode.OpencodeAuthError as exc:
|
||||
console.print(f"[red]SIGN-IN FAILED:[/] {exc}")
|
||||
return 1
|
||||
opencode.save_api_key(key)
|
||||
_print_opencode_success(console)
|
||||
return 0
|
||||
|
||||
|
||||
def _print_opencode_success(console: Console) -> None:
|
||||
text = Text()
|
||||
text.append("Signed in with your OpenCode account", style="bold #22c55e")
|
||||
text.append("\n\n", style="white")
|
||||
text.append("Set ", style="white")
|
||||
text.append("STRIX_LLM", style="bold white")
|
||||
text.append(" to an ", style="white")
|
||||
text.append("opencode/", style="bold cyan")
|
||||
text.append(" model (e.g. ", style="white")
|
||||
text.append("opencode/claude-sonnet-5", style="bold cyan")
|
||||
text.append(") to run on Zen credits, or ", style="white")
|
||||
text.append("opencode-go/", style="bold cyan")
|
||||
text.append(" (e.g. ", style="white")
|
||||
text.append("opencode-go/kimi-k3", style="bold cyan")
|
||||
text.append(") to run on the Go subscription.", style="white")
|
||||
text.append("\n\n", style="white")
|
||||
text.append("Run a scan as usual, e.g. ", style="white")
|
||||
text.append("strix --target https://example.com", style="bold cyan")
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(
|
||||
text,
|
||||
title="[bold white]STRIX",
|
||||
title_align="left",
|
||||
border_style="#22c55e",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
|
||||
def _run_oauth_flow(
|
||||
console: Console,
|
||||
authorize_url: str,
|
||||
@@ -313,41 +244,24 @@ def _first(query: dict[str, list[str]], key: str) -> str | None:
|
||||
|
||||
def _status(console: Console) -> int:
|
||||
record = codex.read_record()
|
||||
opencode_signed_in = opencode.is_authenticated()
|
||||
if record is None and not opencode_signed_in:
|
||||
console.print(
|
||||
"[yellow]Not signed in.[/] Run [cyan]strix auth login chatgpt[/] or "
|
||||
"[cyan]strix auth login opencode[/] to sign in."
|
||||
)
|
||||
if record is None:
|
||||
console.print("[yellow]Not signed in.[/] Run [cyan]strix auth login chatgpt[/] to sign in.")
|
||||
return 1
|
||||
settings = load_settings()
|
||||
if record is not None:
|
||||
console.print("[green]Signed in[/] with a ChatGPT subscription.")
|
||||
console.print(f" Account: [bold]{record.get('account_id')}[/]")
|
||||
if opencode_signed_in:
|
||||
console.print("[green]Signed in[/] with an OpenCode account.")
|
||||
if codex.subscription_model(settings.llm.model) or opencode.subscription_model(
|
||||
settings.llm.model
|
||||
):
|
||||
console.print("[green]Signed in[/] with a ChatGPT subscription.")
|
||||
console.print(f" Account: [bold]{record.get('account_id')}[/]")
|
||||
if codex.subscription_model(settings.llm.model):
|
||||
console.print(f" Runs use the subscription (STRIX_LLM=[bold]{settings.llm.model}[/]).")
|
||||
else:
|
||||
console.print(
|
||||
" [yellow]Note:[/] set [cyan]STRIX_LLM[/] to e.g. [cyan]chatgpt/gpt-5.4[/] or "
|
||||
"[cyan]opencode/claude-sonnet-5[/] to run on a subscription."
|
||||
" [yellow]Note:[/] set [cyan]STRIX_LLM[/] to e.g. [cyan]chatgpt/gpt-5.4[/] "
|
||||
"to run on the subscription."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _logout(console: Console, argv: list[str] | None = None) -> int:
|
||||
target = (argv[0].lower() if argv else "") or "all"
|
||||
if target in _ACCEPTED_PROVIDERS or target == "all":
|
||||
codex.logout()
|
||||
if target in _OPENCODE_PROVIDERS or target == "all":
|
||||
opencode.logout()
|
||||
if target != "all" and target not in _ACCEPTED_PROVIDERS | _OPENCODE_PROVIDERS:
|
||||
console.print(f"[red]Unknown provider:[/] {target}\n")
|
||||
console.print(_USAGE)
|
||||
return 2
|
||||
def _logout(console: Console) -> int:
|
||||
codex.logout()
|
||||
console.print("[green]Signed out.[/] Stored subscription credentials removed.")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import codex, load_settings, opencode
|
||||
from strix.config import codex, load_settings
|
||||
from strix.interface.utils import (
|
||||
check_docker_connection,
|
||||
image_exists,
|
||||
@@ -37,16 +37,6 @@ def validate_environment() -> None:
|
||||
logger.info("Environment OK (ChatGPT subscription)")
|
||||
return
|
||||
|
||||
if opencode.subscription_model(settings.llm.model):
|
||||
if not opencode.is_authenticated():
|
||||
console.print(
|
||||
f"[red]STRIX_LLM={settings.llm.model} uses your OpenCode subscription, "
|
||||
"but you're not signed in.[/] Run [cyan]strix auth login opencode[/] first."
|
||||
)
|
||||
sys.exit(1)
|
||||
logger.info("Environment OK (OpenCode subscription)")
|
||||
return
|
||||
|
||||
if not settings.llm.model:
|
||||
missing_required_vars.append("STRIX_LLM")
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.config import codex, load_settings, opencode, persist_current
|
||||
from strix.config import codex, load_settings, persist_current
|
||||
from strix.core.paths import run_dir_for
|
||||
from strix.interface.cli_args import parse_arguments
|
||||
from strix.interface.environment import (
|
||||
@@ -104,14 +104,8 @@ def _provider_import_hint(exc: BaseException, model: str) -> str | None:
|
||||
|
||||
|
||||
def _subscription_error_hint(exc: BaseException) -> str | None:
|
||||
"""Return an actionable hint for a known subscription error, or None."""
|
||||
model = load_settings().llm.model
|
||||
if opencode.subscription_model(model):
|
||||
joined = " ".join(_exception_messages(exc)).lower()
|
||||
if "error code: 401" in joined or "http 401" in joined or "unauthorized" in joined:
|
||||
return "Your OpenCode API key was rejected. Sign in again:\n strix auth login opencode"
|
||||
return None
|
||||
if not codex.subscription_model(model):
|
||||
"""Return an actionable hint for a known ChatGPT-subscription error, or None."""
|
||||
if not codex.subscription_model(load_settings().llm.model):
|
||||
return None
|
||||
joined = " ".join(_exception_messages(exc)).lower()
|
||||
if "not supported when using codex with a chatgpt account" in joined:
|
||||
|
||||
@@ -14,7 +14,7 @@ import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from strix.config import Settings, load_settings, opencode
|
||||
from strix.config import Settings, codex, load_settings
|
||||
from strix.core.paths import run_dir_for
|
||||
from strix.interface.utils import (
|
||||
assign_workspace_subdirs,
|
||||
@@ -226,7 +226,7 @@ def telemetry_start(args: argparse.Namespace) -> None:
|
||||
model = load_settings().llm.model
|
||||
kwargs = {
|
||||
"model": model,
|
||||
"auth_mode": opencode.auth_mode(model),
|
||||
"auth_mode": codex.auth_mode(model),
|
||||
"scan_mode": args.scan_mode,
|
||||
"is_whitebox": is_whitebox_scan(args.targets_info),
|
||||
"interactive": not args.non_interactive,
|
||||
@@ -247,8 +247,7 @@ def _persist_run_record(args: argparse.Namespace) -> None:
|
||||
"status": "running",
|
||||
"start_time": datetime.now(UTC).isoformat(),
|
||||
"end_time": None,
|
||||
"auth_mode": opencode.auth_mode(load_settings().llm.model),
|
||||
"subscription_provider": opencode.subscription_provider(load_settings().llm.model),
|
||||
"auth_mode": codex.auth_mode(load_settings().llm.model),
|
||||
"targets_info": args.targets_info,
|
||||
"scan_mode": args.scan_mode,
|
||||
"instruction": args.instruction,
|
||||
|
||||
@@ -24,7 +24,7 @@ from strix.interface.tui.backend.projection import (
|
||||
sanitize_terminal_text,
|
||||
terminal_projection,
|
||||
)
|
||||
from strix.interface.utils import is_subscription_run, subscription_label
|
||||
from strix.interface.utils import is_subscription_run
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -164,10 +164,6 @@ class TuiController:
|
||||
subscription = False
|
||||
with contextlib.suppress(Exception):
|
||||
subscription = is_subscription_run(self.report_state)
|
||||
label = ""
|
||||
if subscription:
|
||||
with contextlib.suppress(Exception):
|
||||
label = subscription_label()
|
||||
model_warning = ""
|
||||
if model and not is_recommended_or_frontier_model(model):
|
||||
model_warning = (
|
||||
@@ -204,7 +200,6 @@ class TuiController:
|
||||
],
|
||||
"usage": terminal_projection(usage, max_string=256, max_items=20),
|
||||
"subscription": subscription,
|
||||
"subscription_label": label,
|
||||
"viewer_status": self.viewer_status,
|
||||
"viewer_url": terminal_projection(self.viewer_url, max_string=1024),
|
||||
"error": terminal_projection(self.error, max_string=2 * 1024),
|
||||
|
||||
@@ -596,11 +596,7 @@ func (m Model) statsView() string {
|
||||
if b.Len() > 0 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
label := m.snapshot.SubscriptionLabel
|
||||
if label == "" {
|
||||
label = "ChatGPT subscription"
|
||||
}
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(green).Render(label))
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(green).Render("ChatGPT subscription"))
|
||||
}
|
||||
total := numberValue(m.snapshot.Usage["total_tokens"])
|
||||
if total > 0 {
|
||||
|
||||
@@ -68,7 +68,6 @@ type Snapshot struct {
|
||||
Vulnerabilities []map[string]any `json:"-"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
Subscription bool `json:"subscription"`
|
||||
SubscriptionLabel string `json:"subscription_label"`
|
||||
ViewerStatus string `json:"viewer_status"`
|
||||
ViewerURL *string `json:"viewer_url"`
|
||||
Error *string `json:"error"`
|
||||
|
||||
@@ -262,19 +262,9 @@ def is_subscription_run(report_state: Any) -> bool:
|
||||
record = getattr(report_state, "run_record", None)
|
||||
if isinstance(record, dict) and record.get("auth_mode"):
|
||||
return record.get("auth_mode") == "subscription"
|
||||
from strix.config import opencode
|
||||
from strix.config import codex
|
||||
|
||||
return opencode.auth_mode(load_settings().llm.model) == "subscription"
|
||||
|
||||
|
||||
def subscription_label() -> str:
|
||||
"""Display name of the subscription behind the configured model."""
|
||||
from strix.config import opencode
|
||||
|
||||
model = load_settings().llm.model
|
||||
if opencode.subscription_model(model):
|
||||
return "OpenCode subscription"
|
||||
return "ChatGPT subscription"
|
||||
return codex.auth_mode(load_settings().llm.model) == "subscription"
|
||||
|
||||
|
||||
def _int_stat(usage: dict[str, Any], key: str) -> int:
|
||||
@@ -378,7 +368,7 @@ def build_live_stats_text(report_state: Any) -> Text:
|
||||
stats_text.append(str(model), style="white")
|
||||
if is_subscription_run(report_state):
|
||||
stats_text.append(" · ", style="dim white")
|
||||
stats_text.append(subscription_label(), style="#22c55e")
|
||||
stats_text.append("ChatGPT subscription", style="#22c55e")
|
||||
stats_text.append("\n")
|
||||
|
||||
vuln_count = len(report_state.vulnerability_reports)
|
||||
@@ -424,7 +414,7 @@ def build_tui_stats_text(report_state: Any) -> Text:
|
||||
subscription = is_subscription_run(report_state)
|
||||
if subscription:
|
||||
stats_text.append("\n")
|
||||
stats_text.append(subscription_label(), style="#22c55e")
|
||||
stats_text.append("ChatGPT subscription", style="#22c55e")
|
||||
|
||||
usage = _llm_usage(report_state)
|
||||
if usage and _int_stat(usage, "total_tokens") > 0:
|
||||
|
||||
@@ -101,11 +101,6 @@ export function RunDetails({
|
||||
const totalTokens = num(usage.total_tokens);
|
||||
const cost = num(usage.cost);
|
||||
const subscription = str(raw.auth_mode) === "subscription";
|
||||
const subscriptionProvider =
|
||||
str(raw.subscription_provider) ??
|
||||
(models.some((m) => m.toLowerCase().startsWith("opencode")) ? "opencode" : "chatgpt");
|
||||
const subscriptionLabel =
|
||||
subscriptionProvider === "opencode" ? "OpenCode subscription" : "ChatGPT subscription";
|
||||
|
||||
const sub = (n: number, word: string) => (
|
||||
<span className="text-[#666]"> ({formatNumber(n)} {word})</span>
|
||||
@@ -185,7 +180,7 @@ export function RunDetails({
|
||||
<Field label="Provider">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="rounded-full border border-[#22c55e]/40 bg-[#22c55e]/10 px-2 py-0.5 text-[11px] text-[#22c55e]">
|
||||
{subscriptionLabel}
|
||||
ChatGPT subscription
|
||||
</span>
|
||||
</span>
|
||||
</Field>
|
||||
|
||||
+24
-24
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Strix Results</title>
|
||||
<script type="module" crossorigin src="./assets/index-1LIW3rcB.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DBJ-RJqo.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DKbLYAbP.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -20,8 +20,6 @@ logger = logging.getLogger(__name__)
|
||||
_STRIPPABLE_PREFIXES = (
|
||||
"openai/",
|
||||
"chatgpt/",
|
||||
"opencode-go/",
|
||||
"opencode/",
|
||||
"litellm/",
|
||||
"any-llm/",
|
||||
"ollama/",
|
||||
@@ -50,11 +48,7 @@ def _model_info(model: str) -> dict[str, int]:
|
||||
lookup_key = _lookup_key(model)
|
||||
# Provider-qualified ChatGPT lookups may start a synchronous device-login
|
||||
# poll. LiteLLM keys the metadata by the underlying model slug.
|
||||
candidates = (
|
||||
(lookup_key,)
|
||||
if model.startswith(("chatgpt/", "opencode/", "opencode-go/"))
|
||||
else (model, lookup_key)
|
||||
)
|
||||
candidates = (lookup_key,) if model.startswith("chatgpt/") else (model, lookup_key)
|
||||
for candidate in candidates:
|
||||
info = _safe_get_model_info(candidate)
|
||||
if info is not None:
|
||||
|
||||
@@ -11,7 +11,7 @@ from uuid import uuid4
|
||||
|
||||
from agents.usage import Usage
|
||||
|
||||
from strix.config import opencode
|
||||
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
|
||||
@@ -123,7 +123,7 @@ class ReportState:
|
||||
self.scan_results: dict[str, Any] | None = None
|
||||
self.scan_config: dict[str, Any] | None = None
|
||||
self._llm_usage = LLMUsageLedger()
|
||||
auth_mode = opencode.auth_mode(load_settings().llm.model)
|
||||
auth_mode = codex.auth_mode(load_settings().llm.model)
|
||||
self._llm_usage.zero_cost = auth_mode == "subscription"
|
||||
self.run_record: dict[str, Any] = {
|
||||
"run_id": self.run_id,
|
||||
@@ -132,7 +132,6 @@ class ReportState:
|
||||
"end_time": None,
|
||||
"status": "running",
|
||||
"auth_mode": auth_mode,
|
||||
"subscription_provider": opencode.subscription_provider(load_settings().llm.model),
|
||||
"targets_info": [],
|
||||
"llm_usage": self._build_llm_usage_record(),
|
||||
}
|
||||
|
||||
+1
-59
@@ -2,12 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
|
||||
from strix.config import codex, opencode
|
||||
from strix.config import codex
|
||||
from strix.interface import auth_cli
|
||||
|
||||
|
||||
@@ -105,60 +104,3 @@ def test_login_accepts_provider_aliases(provider: str, monkeypatch: pytest.Monke
|
||||
|
||||
assert auth_cli.run_auth(["login", provider]) == 0
|
||||
assert reached["flow"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["opencode", "OpenCode", "opencode-go", "zen"])
|
||||
def test_login_accepts_opencode_aliases(provider: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
reached = {"login": False}
|
||||
|
||||
def _fake_login(_console: Any) -> int:
|
||||
reached["login"] = True
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(auth_cli, "_login_opencode", _fake_login)
|
||||
assert auth_cli.run_auth(["login", provider]) == 0
|
||||
assert reached["login"] is True
|
||||
|
||||
|
||||
def test_login_opencode_validates_and_saves(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
saved: dict[str, str] = {}
|
||||
monkeypatch.setattr("rich.console.Console.input", lambda _self, *_a, **_k: " sk-oc-test ")
|
||||
monkeypatch.setattr(opencode, "validate_api_key", lambda key: saved.setdefault("checked", key))
|
||||
monkeypatch.setattr(opencode, "save_api_key", lambda key: saved.setdefault("key", key))
|
||||
|
||||
assert auth_cli.run_auth(["login", "opencode"]) == 0
|
||||
assert saved == {"checked": "sk-oc-test", "key": "sk-oc-test"}
|
||||
|
||||
|
||||
def test_login_opencode_rejects_bad_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("rich.console.Console.input", lambda _self, *_a, **_k: "bad")
|
||||
|
||||
def _reject(_key: str) -> None:
|
||||
raise opencode.OpencodeAuthError("invalid_key")
|
||||
|
||||
monkeypatch.setattr(opencode, "validate_api_key", _reject)
|
||||
assert auth_cli.run_auth(["login", "opencode"]) == 1
|
||||
assert opencode.is_authenticated() is False
|
||||
|
||||
|
||||
def test_logout_provider_scoped() -> None:
|
||||
codex.save_record(
|
||||
{
|
||||
"type": "oauth",
|
||||
"provider": "codex",
|
||||
"access": "a",
|
||||
"refresh": "r",
|
||||
"account_id": "acct",
|
||||
"expires_at": time.time() + 3600,
|
||||
}
|
||||
)
|
||||
opencode.save_api_key("sk-oc-test")
|
||||
|
||||
assert auth_cli.run_auth(["logout", "opencode"]) == 0
|
||||
assert opencode.is_authenticated() is False
|
||||
assert codex.is_authenticated() is True
|
||||
|
||||
assert auth_cli.run_auth(["logout"]) == 0
|
||||
assert codex.is_authenticated() is False
|
||||
|
||||
assert auth_cli.run_auth(["logout", "bogus"]) == 2
|
||||
|
||||
@@ -111,13 +111,6 @@ def test_make_model_settings_no_prompt_cache_for_non_claude(model_name: str) ->
|
||||
assert make_model_settings(None, model_name=model_name).extra_args is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["opencode/claude-sonnet-5", "opencode-go/claude-sonnet-5"])
|
||||
def test_no_prompt_cache_for_opencode_claude(model_name: str) -> None:
|
||||
# The OpenCode route uses the raw OpenAI SDK, whose create() rejects the
|
||||
# LiteLLM-only cache_control_injection_points argument.
|
||||
assert _cache_points(model_name) is None
|
||||
|
||||
|
||||
def test_no_prompt_cache_for_unmapped_bedrock_claude_model(monkeypatch: Any) -> None:
|
||||
# A Bedrock Claude model LiteLLM hasn't mapped must run uncached, not crash.
|
||||
unmapped = "bedrock/global.anthropic.claude-brand-new-9"
|
||||
|
||||
@@ -66,11 +66,6 @@ def test_recommended_models_are_matched_case_insensitively() -> None:
|
||||
"moonshot/kimi-k2.6",
|
||||
"kimi-k2.7-code",
|
||||
"moonshot/kimi-k3",
|
||||
"opencode/gpt-5.4",
|
||||
"opencode/claude-sonnet-5",
|
||||
"opencode-go/kimi-k3",
|
||||
"opencode-go/deepseek-v4-flash",
|
||||
"opencode-go/qwen3.8-max",
|
||||
],
|
||||
)
|
||||
def test_frontier_model_families_are_accepted(model_name: str) -> None:
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
"""Tests for OpenCode (Zen/Go) subscription auth: prefix parsing and key store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from strix.config import codex, opencode
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _tmp_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
path = tmp_path / "home" / ".strix" / "subscription-auth.json"
|
||||
monkeypatch.setattr(codex, "AUTH_PATH", path)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "slug", "base_url", "uses_responses"),
|
||||
[
|
||||
("opencode/claude-sonnet-5", "claude-sonnet-5", opencode.ZEN_BASE_URL, False),
|
||||
("opencode/gpt-5.4", "gpt-5.4", opencode.ZEN_BASE_URL, True),
|
||||
("opencode/grok-4.5", "grok-4.5", opencode.ZEN_BASE_URL, True),
|
||||
("OpenCode/Kimi-K3", "Kimi-K3", opencode.ZEN_BASE_URL, False),
|
||||
("opencode-go/kimi-k3", "kimi-k3", opencode.GO_BASE_URL, False),
|
||||
("opencode-go/gpt-5.6-luna", "gpt-5.6-luna", opencode.GO_BASE_URL, True),
|
||||
("opencode-go/grok-4.5", "grok-4.5", opencode.GO_BASE_URL, False),
|
||||
],
|
||||
)
|
||||
def test_subscription_model_parses_prefixes(
|
||||
model: str, slug: str, base_url: str, uses_responses: bool
|
||||
) -> None:
|
||||
parsed = opencode.subscription_model(model)
|
||||
assert parsed is not None
|
||||
assert parsed.slug == slug
|
||||
assert parsed.base_url == base_url
|
||||
assert parsed.uses_responses == uses_responses
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
["openai/gpt-5.4", "chatgpt/gpt-5.4", "opencode/", "opencode-go/", "opencode", "", None],
|
||||
)
|
||||
def test_subscription_model_rejects_non_opencode(model: str | None) -> None:
|
||||
assert opencode.subscription_model(model) is None
|
||||
|
||||
|
||||
def test_store_roundtrip_and_logout() -> None:
|
||||
assert opencode.read_record() is None
|
||||
assert opencode.is_authenticated() is False
|
||||
|
||||
opencode.save_api_key("sk-oc-test")
|
||||
record = opencode.read_record()
|
||||
assert record is not None
|
||||
assert record["key"] == "sk-oc-test"
|
||||
assert opencode.is_authenticated() is True
|
||||
assert opencode.get_api_key() == "sk-oc-test"
|
||||
|
||||
opencode.logout()
|
||||
assert opencode.read_record() is None
|
||||
opencode.logout() # no-op when already gone
|
||||
|
||||
|
||||
def test_store_coexists_with_chatgpt_record() -> None:
|
||||
codex.save_record({"type": "oauth", "access": "a", "refresh": "r", "account_id": "acct"})
|
||||
opencode.save_api_key("sk-oc-test")
|
||||
|
||||
assert codex.read_record() is not None
|
||||
assert opencode.get_api_key() == "sk-oc-test"
|
||||
|
||||
opencode.logout()
|
||||
assert codex.read_record() is not None
|
||||
assert opencode.read_record() is None
|
||||
|
||||
|
||||
def test_get_api_key_raises_when_not_signed_in() -> None:
|
||||
with pytest.raises(opencode.OpencodeAuthError) as exc:
|
||||
opencode.get_api_key()
|
||||
assert exc.value.code == "not_authenticated"
|
||||
|
||||
|
||||
def test_auth_mode_covers_both_subscriptions() -> None:
|
||||
assert opencode.auth_mode("opencode/claude-sonnet-5") == "subscription"
|
||||
assert opencode.auth_mode("opencode-go/kimi-k3") == "subscription"
|
||||
assert opencode.auth_mode("chatgpt/gpt-5.4") == "subscription"
|
||||
assert opencode.auth_mode("openai/gpt-5.4") == "api_key"
|
||||
assert opencode.auth_mode(None) == "api_key"
|
||||
|
||||
|
||||
def test_subscription_provider() -> None:
|
||||
assert opencode.subscription_provider("opencode/claude-sonnet-5") == "opencode"
|
||||
assert opencode.subscription_provider("opencode-go/kimi-k3") == "opencode"
|
||||
assert opencode.subscription_provider("chatgpt/gpt-5.4") == "chatgpt"
|
||||
assert opencode.subscription_provider("openai/gpt-5.4") is None
|
||||
assert opencode.subscription_provider(None) is None
|
||||
|
||||
|
||||
def _response(status_code: int, text: str = "") -> mock.MagicMock:
|
||||
response = mock.MagicMock()
|
||||
response.status_code = status_code
|
||||
response.text = text
|
||||
return response
|
||||
|
||||
|
||||
def test_validate_api_key_accepts_ok() -> None:
|
||||
with mock.patch.object(requests, "get", return_value=_response(200)) as get:
|
||||
opencode.validate_api_key("sk-oc-test")
|
||||
assert get.call_args.kwargs["headers"]["Authorization"] == "Bearer sk-oc-test"
|
||||
|
||||
|
||||
def test_validate_api_key_rejects_unauthorized() -> None:
|
||||
with (
|
||||
mock.patch.object(requests, "get", return_value=_response(401)),
|
||||
pytest.raises(opencode.OpencodeAuthError) as exc,
|
||||
):
|
||||
opencode.validate_api_key("bad-key")
|
||||
assert exc.value.code == "invalid_key"
|
||||
|
||||
|
||||
def test_validate_api_key_maps_network_errors() -> None:
|
||||
with (
|
||||
mock.patch.object(requests, "get", side_effect=requests.ConnectionError("boom")),
|
||||
pytest.raises(opencode.OpencodeAuthError) as exc,
|
||||
):
|
||||
opencode.validate_api_key("sk-oc-test")
|
||||
assert exc.value.code == "unavailable"
|
||||
Reference in New Issue
Block a user