From 8ed24335740316392e655ab25e2e8cdd49142a77 Mon Sep 17 00:00:00 2001 From: yoni Date: Fri, 14 Aug 2026 17:55:55 +0000 Subject: [PATCH] Fix OpenCode routes: drop LiteLLM-only prompt-cache arg, persist subscription provider for viewer label --- strix/config/opencode.py | 9 +++++++++ strix/core/inputs.py | 5 +++++ strix/interface/scan_setup.py | 1 + .../viewer/frontend/src/components/RunDetails.tsx | 8 +++++--- strix/report/state.py | 1 + tests/test_inputs.py | 7 +++++++ tests/test_opencode_auth.py | 8 ++++++++ 7 files changed, 36 insertions(+), 3 deletions(-) diff --git a/strix/config/opencode.py b/strix/config/opencode.py index dd647e9f..f7de0fe4 100644 --- a/strix/config/opencode.py +++ b/strix/config/opencode.py @@ -147,3 +147,12 @@ def auth_mode(model_name: str | None) -> str: 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 diff --git a/strix/core/inputs.py b/strix/core/inputs.py index 3dd0d701..1123be43 100644 --- a/strix/core/inputs.py +++ b/strix/core/inputs.py @@ -8,6 +8,7 @@ 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, @@ -325,6 +326,10 @@ def _prompt_cache_extra_args(model_name: str) -> dict[str, Any] | None: """ if not is_claude_model(model_name) or not routes_through_litellm(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 diff --git a/strix/interface/scan_setup.py b/strix/interface/scan_setup.py index 80b9b5de..21699230 100644 --- a/strix/interface/scan_setup.py +++ b/strix/interface/scan_setup.py @@ -248,6 +248,7 @@ def _persist_run_record(args: argparse.Namespace) -> None: "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), "targets_info": args.targets_info, "scan_mode": args.scan_mode, "instruction": args.instruction, diff --git a/strix/interface/viewer/frontend/src/components/RunDetails.tsx b/strix/interface/viewer/frontend/src/components/RunDetails.tsx index 65887b21..c169d0de 100644 --- a/strix/interface/viewer/frontend/src/components/RunDetails.tsx +++ b/strix/interface/viewer/frontend/src/components/RunDetails.tsx @@ -101,9 +101,11 @@ export function RunDetails({ const totalTokens = num(usage.total_tokens); const cost = num(usage.cost); const subscription = str(raw.auth_mode) === "subscription"; - const subscriptionLabel = models.some((m) => m.toLowerCase().startsWith("opencode")) - ? "OpenCode subscription" - : "ChatGPT 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) => ( ({formatNumber(n)} {word}) diff --git a/strix/report/state.py b/strix/report/state.py index bbe027ff..3ed2154c 100644 --- a/strix/report/state.py +++ b/strix/report/state.py @@ -161,6 +161,7 @@ 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(), } diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 5a483edf..a99a3a47 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -122,6 +122,13 @@ 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" diff --git a/tests/test_opencode_auth.py b/tests/test_opencode_auth.py index 80241760..3c120cd4 100644 --- a/tests/test_opencode_auth.py +++ b/tests/test_opencode_auth.py @@ -94,6 +94,14 @@ def test_auth_mode_covers_both_subscriptions() -> None: 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