mirror of
https://github.com/usestrix/strix.git
synced 2026-08-21 10:48:59 +02:00
auth: make STRIX_LLM=openai/subscription the single switch
Replace the separate STRIX_AUTH_MODE flag with a sentinel model value: STRIX_LLM=openai/subscription selects the authenticated ChatGPT subscription, and any other value is a normal API-key model. The env vars that already run Strix are now the single source of truth — no second mode to keep in sync. Encapsulate the behavior instead of branching everywhere: - StrixProvider.get_model routes the sentinel to a _CodexResponsesModel backed by a cached OAuth client (no global default-client mutation, no per-call client churn). - _CodexResponsesModel self-enforces the backend's requirements — streaming, store=false, encrypted reasoning, and the configured reasoning effort — so the runner, warm-up, and make_model_settings no longer special-case subscription. Remove now-unneeded machinery: STRIX_AUTH_MODE/AuthMode, the "incompatible model" warning, the non-OpenAI model coercion, the make_model_settings codex flag, and the global set_default_openai_client wiring. run.json still records a derived auth_mode so the viewer/telemetry/cost display are unchanged. Switching modes is now just editing STRIX_LLM. Sentinel-only (no per-model override): a subscription run uses gpt-5.4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9f54b2f144
commit
30390628da
@@ -269,17 +269,19 @@ export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high,
|
||||
|
||||
#### Sign in with a ChatGPT subscription
|
||||
|
||||
Instead of a metered API key, you can run Strix on your ChatGPT Plus/Pro subscription:
|
||||
Instead of a metered API key, you can run Strix on your ChatGPT Plus/Pro subscription. Set `STRIX_LLM=openai/subscription` — that value is the only switch:
|
||||
|
||||
```bash
|
||||
strix auth login chatgpt # opens your browser to sign in with ChatGPT
|
||||
strix auth login chatgpt # sign in; sets STRIX_LLM=openai/subscription for you
|
||||
strix --target ./app-directory
|
||||
|
||||
strix auth status # show the active sign-in
|
||||
strix auth logout # revert to API-key billing
|
||||
strix auth logout # forget the sign-in
|
||||
```
|
||||
|
||||
This uses OpenAI's Codex OAuth flow: inference is billed to your ChatGPT plan rather than per token. Strix defaults to `gpt-5.4` here — newer models apply stricter content moderation that interferes with security-testing prompts, so `gpt-5.4` is recommended for scans. You can override the model with `strix auth login chatgpt --model <name>`. Note that the models a ChatGPT plan exposes are a narrower set than the OpenAI API. If the browser can't open, the command falls back to pasting the redirect URL by hand. Tokens are stored in `~/.strix/subscription-auth.json` (`0600`) and refreshed automatically.
|
||||
To switch back to a metered API key, just point `STRIX_LLM` at a normal model (e.g. `openai/gpt-5.4`) and set `LLM_API_KEY` — there's no separate mode to toggle.
|
||||
|
||||
This uses OpenAI's Codex OAuth flow: inference is billed to your ChatGPT plan rather than per token, and it runs on `gpt-5.4` (newer models apply stricter content moderation that interferes with security-testing prompts). If the browser can't open, the command falls back to pasting the redirect URL by hand. Tokens are stored in `~/.strix/subscription-auth.json` (`0600`) and refreshed automatically.
|
||||
|
||||
> [!NOTE]
|
||||
> Using a ChatGPT subscription outside OpenAI's own products is not officially supported by OpenAI and may be subject to its terms of use. For unattended/CI runs, prefer an API key.
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ ignore = [
|
||||
"strix/core/runner.py" = ["TC003", "PLR0912", "PLR0915", "PLC0415"]
|
||||
# ReportState carries scan artifact/report fields and
|
||||
# a runtime ``Callable`` annotation on ``vulnerability_found_callback``.
|
||||
"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401"]
|
||||
"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401", "PLC0415"]
|
||||
"strix/report/usage.py" = ["PLC0415"]
|
||||
"strix/config/models.py" = ["PLC0415"]
|
||||
# Heavy inference deps (httpx, openai) imported lazily so auth-status checks
|
||||
|
||||
+38
-42
@@ -57,19 +57,16 @@ CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
|
||||
ORIGINATOR = "codex_cli_rs"
|
||||
_ACCOUNT_CLAIM = "https://api.openai.com/auth"
|
||||
|
||||
# Models the ChatGPT Codex backend serves to subscription accounts (bare names,
|
||||
# no provider prefix). Note this set is NARROWER than the OpenAI API and is
|
||||
# account-specific: the ``*-codex`` variants are API-only and are rejected for a
|
||||
# ChatGPT account ("model is not supported when using Codex with a ChatGPT
|
||||
# account"). These are the general GPT-5.x models a Plus/Pro plan exposes.
|
||||
#
|
||||
# Default is gpt-5.4: gpt-5.5 and newer apply stricter content moderation that
|
||||
# interferes with security-testing prompts, so we prefer 5.4 for scans.
|
||||
# Setting STRIX_LLM to this value runs inference on the user's authenticated
|
||||
# ChatGPT subscription instead of a metered API key. It's the single switch —
|
||||
# there is no separate auth-mode flag.
|
||||
SUBSCRIPTION_MODEL = "openai/subscription"
|
||||
|
||||
# The actual model sent to the ChatGPT backend for a subscription run. gpt-5.4 is
|
||||
# used because gpt-5.5 and newer apply stricter content moderation that
|
||||
# interferes with security-testing prompts. (The subscription only exposes the
|
||||
# general GPT-5.x models, not the ``*-codex`` API variants.)
|
||||
DEFAULT_CODEX_MODEL = "gpt-5.4"
|
||||
CODEX_MODELS: tuple[str, ...] = (
|
||||
"gpt-5.4",
|
||||
"gpt-5.5",
|
||||
)
|
||||
|
||||
_TOKEN_TIMEOUT = 30
|
||||
# Refresh a little before the token actually expires so a request never goes out
|
||||
@@ -394,57 +391,56 @@ def build_openai_client() -> AsyncOpenAI:
|
||||
)
|
||||
|
||||
|
||||
def is_backend_compatible(model_name: str | None) -> bool:
|
||||
"""True if ``model_name`` could be served by the ChatGPT subscription backend.
|
||||
_subscription_client: AsyncOpenAI | None = None
|
||||
|
||||
A bare name or an ``openai/`` prefix is plausible (the backend validates the
|
||||
exact name against the account). Any other provider prefix — ``anthropic/``,
|
||||
``deepseek/``, ``vertex_ai/``, … — never can be, so it's treated as
|
||||
incompatible.
|
||||
|
||||
def get_subscription_client() -> AsyncOpenAI:
|
||||
"""Return the process-wide subscription client, building it once.
|
||||
|
||||
One client is reused across every agent in a run: its per-request auth hook
|
||||
refreshes the token itself, so there's no reason to rebuild it, and building
|
||||
one per agent would leak httpx clients.
|
||||
"""
|
||||
name = (model_name or "").strip()
|
||||
if not name:
|
||||
return False
|
||||
if "/" not in name:
|
||||
return True
|
||||
return name.split("/", 1)[0].lower() == "openai"
|
||||
global _subscription_client # noqa: PLW0603
|
||||
if _subscription_client is None:
|
||||
_subscription_client = build_openai_client()
|
||||
return _subscription_client
|
||||
|
||||
|
||||
def normalize_model(model_name: str | None) -> str:
|
||||
"""Return the bare model name to send to the ChatGPT backend.
|
||||
def is_subscription(model_name: str | None) -> bool:
|
||||
"""Whether ``STRIX_LLM`` selects the authenticated ChatGPT subscription."""
|
||||
return (model_name or "").strip().lower() == SUBSCRIPTION_MODEL
|
||||
|
||||
``openai/gpt-5.4`` → ``gpt-5.4``. A configured-but-unlisted OpenAI/bare name
|
||||
is passed through as-is (availability is account-specific and the backend is
|
||||
the authority). An empty value, or a model from another provider that a
|
||||
ChatGPT subscription can't serve, falls back to the default so a stray
|
||||
``STRIX_LLM`` never sends an impossible name to the backend. See
|
||||
``is_backend_compatible``.
|
||||
"""
|
||||
name = (model_name or "").strip()
|
||||
if not is_backend_compatible(name):
|
||||
return DEFAULT_CODEX_MODEL
|
||||
if "/" in name:
|
||||
name = name.rsplit("/", 1)[-1]
|
||||
return name
|
||||
|
||||
def resolve_subscription_model() -> str:
|
||||
"""The concrete model sent to the ChatGPT backend for a subscription run."""
|
||||
return DEFAULT_CODEX_MODEL
|
||||
|
||||
|
||||
def auth_mode_label(model_name: str | None) -> str:
|
||||
"""How a run authenticated, for recording in run.json / telemetry / the viewer."""
|
||||
return "subscription" if is_subscription(model_name) else "api_key"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CODEX_MODELS",
|
||||
"DEFAULT_CODEX_MODEL",
|
||||
"PROVIDER",
|
||||
"SUBSCRIPTION_MODEL",
|
||||
"CodexAuthError",
|
||||
"auth_mode_label",
|
||||
"build_authorize_url",
|
||||
"build_openai_client",
|
||||
"create_state",
|
||||
"exchange_code",
|
||||
"generate_pkce",
|
||||
"get_subscription_client",
|
||||
"get_valid_token",
|
||||
"is_authenticated",
|
||||
"is_backend_compatible",
|
||||
"is_subscription",
|
||||
"logout",
|
||||
"normalize_model",
|
||||
"parse_redirect_input",
|
||||
"read_record",
|
||||
"refresh_tokens",
|
||||
"resolve_subscription_model",
|
||||
"save_record",
|
||||
]
|
||||
|
||||
+63
-40
@@ -7,10 +7,10 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from agents import (
|
||||
set_default_openai_api,
|
||||
set_default_openai_client,
|
||||
set_default_openai_key,
|
||||
set_tracing_disabled,
|
||||
)
|
||||
from agents.model_settings import ModelSettings
|
||||
from agents.models.multi_provider import MultiProvider
|
||||
from agents.models.openai_responses import OpenAIResponsesModel
|
||||
from agents.retry import (
|
||||
@@ -19,12 +19,16 @@ from agents.retry import (
|
||||
RetryPolicyContext,
|
||||
retry_policies,
|
||||
)
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from strix.auth import codex
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.models.interface import Model, ModelProvider
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from strix.config.settings import Settings
|
||||
from strix.config.settings import ReasoningEffort, Settings
|
||||
|
||||
|
||||
def request_timeout_extra_args(timeout_s: float | None) -> dict[str, float] | None:
|
||||
@@ -43,17 +47,49 @@ def _retry_statusless_provider_errors(context: RetryPolicyContext) -> bool:
|
||||
|
||||
|
||||
class _CodexResponsesModel(OpenAIResponsesModel):
|
||||
"""Responses model for the ChatGPT Codex backend, which rejects non-streamed
|
||||
requests with ``{"detail": "Stream must be set to true"}``.
|
||||
"""A responses model wired for the ChatGPT subscription backend.
|
||||
|
||||
It always calls the API in streaming mode. The natively-streamed run path
|
||||
(``Runner.run_streamed`` → ``stream_response``) is forwarded unchanged; the
|
||||
non-streaming ``get_response`` path (warm-up, dedupe) transparently issues a
|
||||
streamed request and aggregates the events back into a single ``Response``,
|
||||
so those callers don't need to know the backend only speaks SSE.
|
||||
It owns everything that backend requires so the rest of Strix doesn't need
|
||||
to special-case subscription runs:
|
||||
|
||||
- It always calls the API streamed (the backend rejects non-streamed
|
||||
requests). The natively-streamed run path (``Runner.run_streamed``) is
|
||||
forwarded unchanged; the non-streaming ``get_response`` path (warm-up,
|
||||
dedupe) issues a streamed request and aggregates the events back into a
|
||||
single ``Response``.
|
||||
- It forces ``store=false`` + encrypted reasoning (the backend is stateless)
|
||||
and applies the configured reasoning effort, so callers can pass ordinary
|
||||
``ModelSettings``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
openai_client: AsyncOpenAI,
|
||||
*,
|
||||
reasoning_effort: ReasoningEffort | None = None,
|
||||
) -> None:
|
||||
super().__init__(model, openai_client)
|
||||
self._reasoning_effort = reasoning_effort
|
||||
|
||||
def _codex_settings(self, model_settings: ModelSettings) -> ModelSettings:
|
||||
overrides = ModelSettings(store=False, response_include=["reasoning.encrypted_content"])
|
||||
effort = self._reasoning_effort
|
||||
if effort and effort != "none":
|
||||
# The ChatGPT backend rejects "minimal" and only some models take
|
||||
# "xhigh"; clamp to what it accepts.
|
||||
if effort == "minimal":
|
||||
effort = "low"
|
||||
elif effort == "xhigh":
|
||||
effort = "high"
|
||||
overrides = overrides.resolve(ModelSettings(reasoning=Reasoning(effort=effort)))
|
||||
return model_settings.resolve(overrides)
|
||||
|
||||
async def _fetch_response(self, *args: Any, stream: bool = False, **kwargs: Any) -> Any:
|
||||
# model_settings is positional arg 2 for both get_response and
|
||||
# stream_response; force the backend's requirements onto it.
|
||||
if len(args) >= 3:
|
||||
args = (*args[:2], self._codex_settings(args[2]), *args[3:])
|
||||
# Always call the backend streamed (it rejects non-streamed requests).
|
||||
events = await super()._fetch_response(*args, stream=True, **kwargs) # type: ignore[call-overload]
|
||||
if stream:
|
||||
@@ -93,16 +129,18 @@ class StrixProvider(MultiProvider):
|
||||
return self._get_fallback_provider("litellm"), original_model_name
|
||||
|
||||
def get_model(self, model_name: str | None) -> Model:
|
||||
model = super().get_model(model_name)
|
||||
# In subscription mode the OpenAI route talks to the ChatGPT Codex
|
||||
# backend, which requires streamed requests. Promote the responses model
|
||||
# to the streaming-aware subclass (no added state, so the in-place class
|
||||
# swap is safe) rather than duplicating the SDK's provider wiring.
|
||||
from strix.config.loader import load_settings
|
||||
# STRIX_LLM=openai/subscription is self-contained: a model backed by the
|
||||
# OAuth client that talks to the ChatGPT subscription. Everything else
|
||||
# goes through the normal (LiteLLM/OpenAI) resolution unchanged.
|
||||
if codex.is_subscription(model_name):
|
||||
from strix.config.loader import load_settings
|
||||
|
||||
if load_settings().llm.auth_mode == "subscription" and type(model) is OpenAIResponsesModel:
|
||||
model.__class__ = _CodexResponsesModel
|
||||
return model
|
||||
return _CodexResponsesModel(
|
||||
codex.resolve_subscription_model(),
|
||||
codex.get_subscription_client(),
|
||||
reasoning_effort=load_settings().llm.reasoning_effort,
|
||||
)
|
||||
return super().get_model(model_name)
|
||||
|
||||
|
||||
DEFAULT_MODEL_RETRY = ModelRetrySettings(
|
||||
@@ -162,8 +200,10 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
"""Apply Strix config to SDK-native defaults."""
|
||||
llm = settings.llm
|
||||
set_tracing_disabled(True)
|
||||
if llm.auth_mode == "subscription":
|
||||
_configure_subscription_defaults()
|
||||
if codex.is_subscription(llm.model):
|
||||
# Subscription runs are self-contained: StrixProvider builds the OAuth
|
||||
# client and the model enforces the backend's requirements, so there are
|
||||
# no LiteLLM/API-key globals to configure here.
|
||||
return
|
||||
_configure_litellm_compatibility()
|
||||
_configure_openrouter_attribution(llm.model)
|
||||
@@ -179,23 +219,6 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
|
||||
set_default_openai_api("responses")
|
||||
|
||||
|
||||
def _configure_subscription_defaults() -> None:
|
||||
"""Route inference through a model subscription instead of an API key.
|
||||
|
||||
Builds the subscription-backed OpenAI client (currently ChatGPT/Codex) and
|
||||
installs it as the SDK default, so ``StrixProvider`` uses it for the OpenAI
|
||||
route. The client carries the OAuth bearer token and refreshes it per
|
||||
request; the Codex backend speaks the Responses API. Statelessness
|
||||
(``store=false`` and encrypted reasoning) is applied per call in
|
||||
``make_model_settings``.
|
||||
"""
|
||||
from strix.auth import codex
|
||||
|
||||
client = codex.build_openai_client()
|
||||
set_default_openai_client(client, use_for_tracing=False)
|
||||
set_default_openai_api("responses")
|
||||
|
||||
|
||||
def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> None:
|
||||
if not model_name:
|
||||
return
|
||||
@@ -276,9 +299,9 @@ def _configure_litellm_default(name: str, value: str) -> None:
|
||||
|
||||
def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool:
|
||||
"""Return whether the resolved SDK route can only receive JSON function tools."""
|
||||
# Subscription mode (ChatGPT/Codex) speaks the Responses API, so it takes the
|
||||
# native reasoning-model tool schema regardless of the model name's shape.
|
||||
if settings.llm.auth_mode == "subscription":
|
||||
# The ChatGPT subscription speaks the Responses API, so it takes the native
|
||||
# reasoning-model tool schema regardless of the model name's shape.
|
||||
if codex.is_subscription(model_name):
|
||||
return False
|
||||
model = model_name.strip().lower()
|
||||
if "/" in model and not model.startswith("openai/"):
|
||||
|
||||
@@ -9,7 +9,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
|
||||
AuthMode = Literal["api_key", "subscription"]
|
||||
|
||||
_BASE_CONFIG = SettingsConfigDict(
|
||||
case_sensitive=False,
|
||||
@@ -21,11 +20,10 @@ _BASE_CONFIG = SettingsConfigDict(
|
||||
class LlmSettings(BaseSettings):
|
||||
model_config = _BASE_CONFIG
|
||||
|
||||
# Set to ``openai/subscription`` to run inference on an authenticated ChatGPT
|
||||
# subscription (see ``strix/auth``; ``strix auth login`` sets it); any other
|
||||
# value is a normal API-key model.
|
||||
model: str | None = Field(default=None, alias="STRIX_LLM")
|
||||
# "api_key" (default) reads a provider API key from ``api_key`` below.
|
||||
# "subscription" ignores the API key and authenticates with a model
|
||||
# subscription instead (see ``strix/auth``); set by ``strix auth login``.
|
||||
auth_mode: AuthMode = Field(default="api_key", alias="STRIX_AUTH_MODE")
|
||||
api_key: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"),
|
||||
|
||||
+4
-30
@@ -128,7 +128,6 @@ def make_model_settings(
|
||||
model_name: str,
|
||||
force_required_tool_choice: bool = False,
|
||||
request_timeout: float | None = None,
|
||||
codex_subscription: bool = False,
|
||||
) -> ModelSettings:
|
||||
model_settings = ModelSettings(
|
||||
parallel_tool_calls=False,
|
||||
@@ -136,16 +135,10 @@ def make_model_settings(
|
||||
include_usage=True,
|
||||
extra_args=request_timeout_extra_args(request_timeout),
|
||||
)
|
||||
if codex_subscription:
|
||||
# The ChatGPT Codex backend is stateless: it requires store=false and
|
||||
# carries reasoning context forward via encrypted reasoning content
|
||||
# replayed in the input rather than server-side state.
|
||||
model_settings = model_settings.resolve(
|
||||
ModelSettings(store=False, response_include=["reasoning.encrypted_content"]),
|
||||
)
|
||||
reasoning_effort = _resolve_reasoning_effort(reasoning_effort, codex_subscription)
|
||||
if reasoning_effort is not None and (
|
||||
codex_subscription or model_supports_reasoning(model_name)
|
||||
if (
|
||||
reasoning_effort is not None
|
||||
and reasoning_effort != "none"
|
||||
and model_supports_reasoning(model_name)
|
||||
):
|
||||
model_settings = model_settings.resolve(
|
||||
ModelSettings(reasoning=Reasoning(effort=reasoning_effort)),
|
||||
@@ -155,25 +148,6 @@ def make_model_settings(
|
||||
return model_settings
|
||||
|
||||
|
||||
def _resolve_reasoning_effort(
|
||||
reasoning_effort: ReasoningEffort | None, codex_subscription: bool
|
||||
) -> ReasoningEffort | None:
|
||||
"""Normalize the configured effort, clamping to what the Codex backend accepts.
|
||||
|
||||
The ChatGPT Codex backend rejects ``minimal`` and only some models accept
|
||||
``xhigh``; both are clamped rather than passed through, so a valid Strix
|
||||
config doesn't fail at the provider.
|
||||
"""
|
||||
if reasoning_effort is None or reasoning_effort == "none":
|
||||
return None
|
||||
if codex_subscription:
|
||||
if reasoning_effort == "minimal":
|
||||
return "low"
|
||||
if reasoning_effort == "xhigh":
|
||||
return "high"
|
||||
return reasoning_effort
|
||||
|
||||
|
||||
def child_initial_input(
|
||||
*,
|
||||
name: str,
|
||||
|
||||
@@ -148,14 +148,7 @@ async def run_strix_scan(
|
||||
|
||||
settings = load_settings()
|
||||
configure_sdk_model_defaults(settings)
|
||||
codex_subscription = settings.llm.auth_mode == "subscription"
|
||||
resolved_model = (model or settings.llm.model or "").strip()
|
||||
if codex_subscription:
|
||||
# Map whatever is configured to a name the ChatGPT Codex backend accepts,
|
||||
# so a stray STRIX_LLM value can't send an unsupported model.
|
||||
from strix.auth import codex
|
||||
|
||||
resolved_model = codex.normalize_model(resolved_model)
|
||||
if not resolved_model:
|
||||
raise RuntimeError(
|
||||
"No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().",
|
||||
@@ -223,7 +216,6 @@ async def run_strix_scan(
|
||||
model_name=resolved_model,
|
||||
force_required_tool_choice=settings.llm.force_required_tool_choice,
|
||||
request_timeout=settings.llm.timeout,
|
||||
codex_subscription=codex_subscription,
|
||||
)
|
||||
run_config = RunConfig(
|
||||
model=resolved_model,
|
||||
|
||||
+35
-52
@@ -2,17 +2,17 @@
|
||||
|
||||
Subcommands:
|
||||
|
||||
- ``strix auth login chatgpt [--model NAME] [--manual]`` — OAuth sign-in with a
|
||||
ChatGPT Plus/Pro subscription. Opens a browser and catches the redirect on a
|
||||
local server; ``--manual`` (or a failure to open the browser / bind the port)
|
||||
falls back to pasting the redirect URL by hand.
|
||||
- ``strix auth login chatgpt [--manual]`` — OAuth sign-in with a ChatGPT
|
||||
Plus/Pro subscription. Opens a browser and catches the redirect on a local
|
||||
server; ``--manual`` (or a failure to open the browser / bind the port) falls
|
||||
back to pasting the redirect URL by hand.
|
||||
- ``strix auth status`` — show whether a subscription sign-in is active.
|
||||
- ``strix auth logout`` — forget the stored sign-in.
|
||||
|
||||
Signing in persists ``STRIX_AUTH_MODE=subscription`` and a Codex model to
|
||||
``~/.strix/cli-config.json`` so subsequent ``strix`` runs use the subscription.
|
||||
Tokens live separately in ``~/.strix/subscription-auth.json`` (see
|
||||
``strix/auth/store.py``); they are never written to the env-var config.
|
||||
Signing in sets ``STRIX_LLM=openai/subscription`` in ``~/.strix/cli-config.json``
|
||||
so subsequent ``strix`` runs use the subscription. Tokens live separately in
|
||||
``~/.strix/subscription-auth.json`` (see ``strix/auth/store.py``); they are never
|
||||
written to the env-var config.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -46,12 +46,7 @@ _CALLBACK_TIMEOUT_S = 300
|
||||
LOGIN_PROVIDER = "chatgpt"
|
||||
_ACCEPTED_PROVIDERS = frozenset({LOGIN_PROVIDER, codex.PROVIDER})
|
||||
|
||||
_USAGE = (
|
||||
"Usage:\n"
|
||||
" strix auth login chatgpt [--model NAME] [--manual]\n"
|
||||
" strix auth status\n"
|
||||
" strix auth logout"
|
||||
)
|
||||
_USAGE = "Usage:\n strix auth login chatgpt [--manual]\n strix auth status\n strix auth logout"
|
||||
|
||||
|
||||
def run_auth(argv: list[str]) -> int:
|
||||
@@ -86,11 +81,6 @@ def _login(console: Console, argv: list[str]) -> int:
|
||||
default=LOGIN_PROVIDER,
|
||||
help="Model provider to sign in with (default: chatgpt).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default=codex.DEFAULT_CODEX_MODEL,
|
||||
help=f"ChatGPT model to use (default: {codex.DEFAULT_CODEX_MODEL}).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manual",
|
||||
action="store_true",
|
||||
@@ -109,8 +99,8 @@ def _login(console: Console, argv: list[str]) -> int:
|
||||
return 2
|
||||
|
||||
# Capture any shell-exported STRIX_LLM before we persist our own, so we can
|
||||
# warn if it would override the model chosen here (env wins over the config
|
||||
# file we write). This must be read before _persist_subscription_config sets it.
|
||||
# warn if it would override subscription mode (env wins over the config file
|
||||
# we write). This must be read before _persist_subscription_config sets it.
|
||||
preexisting_llm = os.environ.get("STRIX_LLM")
|
||||
|
||||
verifier, challenge = codex.generate_pkce()
|
||||
@@ -133,30 +123,27 @@ def _login(console: Console, argv: list[str]) -> int:
|
||||
return 130
|
||||
|
||||
codex.save_record(record)
|
||||
_persist_subscription_config(args.model)
|
||||
_warn_if_env_overrides_model(console, preexisting_llm, args.model)
|
||||
_persist_subscription_config()
|
||||
_warn_if_env_overrides(console, preexisting_llm)
|
||||
|
||||
stored_model = load_settings().llm.model or f"openai/{codex.normalize_model(args.model)}"
|
||||
_print_success(console, stored_model)
|
||||
_print_success(console)
|
||||
return 0
|
||||
|
||||
|
||||
def _warn_if_env_overrides_model(console: Console, preexisting: str | None, chosen: str) -> None:
|
||||
"""Warn when a shell-exported STRIX_LLM will override the model just saved.
|
||||
def _warn_if_env_overrides(console: Console, preexisting: str | None) -> None:
|
||||
"""Warn when a shell-exported STRIX_LLM will override subscription mode.
|
||||
|
||||
The config file we write loses to an environment variable at load time, so a
|
||||
lingering ``export STRIX_LLM=…`` would silently win. Only warn when it
|
||||
resolves to a different model than the one chosen here (same value, or an
|
||||
incompatible one that coerces to the same default, is not a real conflict).
|
||||
lingering ``export STRIX_LLM=…`` pointing somewhere else would silently win.
|
||||
"""
|
||||
if not preexisting or not preexisting.strip():
|
||||
return
|
||||
if codex.normalize_model(preexisting) == codex.normalize_model(chosen):
|
||||
if codex.is_subscription(preexisting):
|
||||
return
|
||||
console.print(
|
||||
f"[yellow]Note:[/] STRIX_LLM is set in your shell to "
|
||||
f"[bold]{preexisting.strip()}[/], which overrides the model just saved. "
|
||||
f"Run [bold cyan]unset STRIX_LLM[/] so the subscription model is used."
|
||||
f"[bold]{preexisting.strip()}[/], which overrides the subscription just saved. "
|
||||
f"Run [bold cyan]unset STRIX_LLM[/] to use the subscription."
|
||||
)
|
||||
|
||||
|
||||
@@ -287,27 +274,25 @@ def _first(query: dict[str, list[str]], key: str) -> str | None:
|
||||
return values[0] if values else None
|
||||
|
||||
|
||||
def _persist_subscription_config(model: str) -> None:
|
||||
"""Persist subscription mode + model to cli-config.json for later runs."""
|
||||
normalized = codex.normalize_model(model)
|
||||
os.environ["STRIX_AUTH_MODE"] = "subscription"
|
||||
os.environ["STRIX_LLM"] = f"openai/{normalized}"
|
||||
def _persist_subscription_config() -> None:
|
||||
"""Point STRIX_LLM at the subscription and persist it to cli-config.json."""
|
||||
os.environ["STRIX_LLM"] = codex.SUBSCRIPTION_MODEL
|
||||
persist_current()
|
||||
|
||||
|
||||
def _status(console: Console) -> int:
|
||||
record = codex.read_record()
|
||||
if record is None:
|
||||
console.print("[yellow]Not signed in.[/] Run [cyan]strix auth login[/] to sign in.")
|
||||
console.print("[yellow]Not signed in.[/] Run [cyan]strix auth login chatgpt[/] to sign in.")
|
||||
return 1
|
||||
settings = load_settings()
|
||||
console.print("[green]Signed in[/] with a ChatGPT subscription (Codex).")
|
||||
console.print(f" Account: [bold]{record.get('account_id')}[/]")
|
||||
console.print(f" Model: [bold]{settings.llm.model or codex.DEFAULT_CODEX_MODEL}[/]")
|
||||
if settings.llm.auth_mode != "subscription":
|
||||
if not codex.is_subscription(settings.llm.model):
|
||||
console.print(
|
||||
" [yellow]Note:[/] STRIX_AUTH_MODE is not 'subscription'; "
|
||||
"runs will use API-key billing until you re-run [cyan]strix auth login[/]."
|
||||
" [yellow]Note:[/] STRIX_LLM is not 'openai/subscription', so runs won't use "
|
||||
"the subscription. Set [cyan]STRIX_LLM=openai/subscription[/] (or re-run "
|
||||
"[cyan]strix auth login chatgpt[/])."
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -316,8 +301,8 @@ def _logout(console: Console) -> int:
|
||||
codex.logout()
|
||||
console.print("[green]Signed out.[/] Stored subscription credentials removed.")
|
||||
console.print(
|
||||
"[dim]Runs still set to subscription mode will ask you to sign in again. "
|
||||
"Set STRIX_AUTH_MODE=api_key (and LLM_API_KEY) to use metered billing.[/]"
|
||||
"[dim]Runs with STRIX_LLM=openai/subscription will ask you to sign in again. "
|
||||
"Point STRIX_LLM at an API-key model (and set LLM_API_KEY) to use metered billing.[/]"
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -340,22 +325,20 @@ def _fail(console: Console, exc: codex.CodexAuthError) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def _print_success(console: Console, model: str) -> None:
|
||||
def _print_success(console: Console) -> None:
|
||||
text = Text()
|
||||
text.append("Signed in with your ChatGPT subscription", style="bold #22c55e")
|
||||
text.append("\n\n", style="white")
|
||||
text.append("Model", style="dim")
|
||||
text.append(" ")
|
||||
text.append(model, style="bold white")
|
||||
text.append("STRIX_LLM", style="dim")
|
||||
text.append(" ")
|
||||
text.append(codex.SUBSCRIPTION_MODEL, style="bold white")
|
||||
text.append("\n")
|
||||
text.append("Billing", style="dim")
|
||||
text.append(" ")
|
||||
text.append(" ")
|
||||
text.append("your ChatGPT plan (no per-token API charges)", 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")
|
||||
text.append("\nChange model with ", style="dim")
|
||||
text.append("strix auth login chatgpt --model <name>", style="cyan")
|
||||
console.print()
|
||||
console.print(
|
||||
Panel(
|
||||
|
||||
+23
-70
@@ -18,6 +18,7 @@ from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from strix.auth import codex
|
||||
from strix.config import (
|
||||
apply_config_override,
|
||||
load_settings,
|
||||
@@ -85,7 +86,7 @@ def validate_environment() -> None:
|
||||
|
||||
settings = load_settings()
|
||||
|
||||
if settings.llm.auth_mode == "subscription":
|
||||
if codex.is_subscription(settings.llm.model):
|
||||
_validate_subscription_environment(console)
|
||||
return
|
||||
|
||||
@@ -208,32 +209,29 @@ def validate_environment() -> None:
|
||||
|
||||
|
||||
def _validate_subscription_environment(console: Console) -> None:
|
||||
"""Gate a subscription-mode run on being signed in.
|
||||
"""Gate a ``STRIX_LLM=openai/subscription`` run on being signed in.
|
||||
|
||||
In subscription mode there is no API key to check; instead we require a
|
||||
stored sign-in. The model name is optional here — the runner falls back to a
|
||||
default model the subscription backend accepts.
|
||||
There's no API key to check in this mode; instead we require a stored
|
||||
sign-in. The concrete model is fixed by the subscription backend, so there's
|
||||
nothing else to validate here.
|
||||
"""
|
||||
from strix.auth import codex
|
||||
|
||||
if codex.is_authenticated():
|
||||
_warn_if_incompatible_subscription_model(console)
|
||||
logger.info("Environment OK (subscription mode, signed in)")
|
||||
logger.info("Environment OK (subscription, signed in)")
|
||||
return
|
||||
|
||||
logger.error("Subscription mode but not signed in")
|
||||
logger.error("Subscription selected but not signed in")
|
||||
error_text = Text()
|
||||
error_text.append("NOT SIGNED IN", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append(
|
||||
"Strix is set to use a model subscription, but no sign-in was found.\n",
|
||||
"STRIX_LLM is set to 'openai/subscription', but no sign-in was found.\n",
|
||||
style="white",
|
||||
)
|
||||
error_text.append("Sign in with:\n\n", style="white")
|
||||
error_text.append("strix auth login", style="bold cyan")
|
||||
error_text.append("\n\nOr switch back to API-key billing with:\n", style="white")
|
||||
error_text.append("export STRIX_AUTH_MODE=api_key", style="bold cyan")
|
||||
error_text.append(" # and set LLM_API_KEY\n", style="dim white")
|
||||
error_text.append("strix auth login chatgpt", style="bold cyan")
|
||||
error_text.append("\n\nOr set STRIX_LLM to an API-key model (e.g. ", style="white")
|
||||
error_text.append("openai/gpt-5.4", style="bold cyan")
|
||||
error_text.append(") and set LLM_API_KEY.\n", style="white")
|
||||
|
||||
console.print("\n")
|
||||
console.print(
|
||||
@@ -249,44 +247,6 @@ def _validate_subscription_environment(console: Console) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _warn_if_incompatible_subscription_model(console: Console) -> None:
|
||||
"""Warn (once, at startup) when the configured model can't run on the plan.
|
||||
|
||||
In subscription mode a model from another provider (e.g. ``anthropic/…``) is
|
||||
coerced to the default Codex model so the run still works; surface that so the
|
||||
override isn't silent. A bare/``openai/`` name is left alone — the backend
|
||||
validates it.
|
||||
"""
|
||||
from strix.auth import codex
|
||||
|
||||
configured = (load_settings().llm.model or "").strip()
|
||||
if not configured or codex.is_backend_compatible(configured):
|
||||
return
|
||||
|
||||
effective = codex.normalize_model(configured)
|
||||
warn_text = Text()
|
||||
warn_text.append("MODEL NOT AVAILABLE ON SUBSCRIPTION", style="bold yellow")
|
||||
warn_text.append("\n\n", style="white")
|
||||
warn_text.append("STRIX_LLM is set to ", style="white")
|
||||
warn_text.append(f"'{configured}'", style="bold cyan")
|
||||
warn_text.append(", which a ChatGPT subscription can't serve.\n", style="white")
|
||||
warn_text.append("Using ", style="white")
|
||||
warn_text.append(f"'{effective}'", style="bold cyan")
|
||||
warn_text.append(" instead.\n\n", style="white")
|
||||
warn_text.append("Pick a supported model with: ", style="white")
|
||||
warn_text.append("strix auth login chatgpt --model gpt-5.4", style="bold cyan")
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
warn_text,
|
||||
title="[bold white]STRIX",
|
||||
title_align="left",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def check_docker_installed() -> None:
|
||||
if shutil.which("docker") is None:
|
||||
logger.error("Docker CLI not found in PATH")
|
||||
@@ -358,14 +318,13 @@ def _subscription_error_hint(exc: BaseException) -> str | None:
|
||||
other conditions, with clear 400s. In subscription mode we translate those
|
||||
into a fix rather than surfacing a raw provider traceback.
|
||||
"""
|
||||
if load_settings().llm.auth_mode != "subscription":
|
||||
if not codex.is_subscription(load_settings().llm.model):
|
||||
return None
|
||||
joined = " ".join(_exception_messages(exc)).lower()
|
||||
if "not supported when using codex with a chatgpt account" in joined:
|
||||
return (
|
||||
"The selected model isn't available on your ChatGPT subscription.\n"
|
||||
"Sign in again with a supported model, for example:\n"
|
||||
" strix auth login chatgpt --model gpt-5.4"
|
||||
"This model isn't available on your ChatGPT subscription. This is likely "
|
||||
"an internal Strix issue — please report it."
|
||||
)
|
||||
if "stream must be set to true" in joined:
|
||||
return (
|
||||
@@ -394,18 +353,12 @@ async def warm_up_llm(show_model_warning: bool = True) -> None:
|
||||
settings = load_settings()
|
||||
configure_sdk_model_defaults(settings)
|
||||
llm = settings.llm
|
||||
subscription = llm.auth_mode == "subscription"
|
||||
|
||||
subscription = codex.is_subscription(llm.model)
|
||||
raw_model = (llm.model or "").strip()
|
||||
# The subscription model self-enforces the backend's requirements
|
||||
# (streaming, store=false, encrypted reasoning), so a plain warm-up call
|
||||
# is fine.
|
||||
warm_model_settings = ModelSettings()
|
||||
if subscription:
|
||||
from strix.auth import codex
|
||||
|
||||
raw_model = codex.normalize_model(llm.model)
|
||||
warm_model_settings = ModelSettings(
|
||||
store=False, response_include=["reasoning.encrypted_content"]
|
||||
)
|
||||
else:
|
||||
raw_model = (llm.model or "").strip()
|
||||
|
||||
if (
|
||||
not subscription
|
||||
@@ -812,7 +765,7 @@ def _persist_run_record(args: argparse.Namespace) -> None:
|
||||
"status": "running",
|
||||
"start_time": datetime.now(UTC).isoformat(),
|
||||
"end_time": None,
|
||||
"auth_mode": load_settings().llm.auth_mode,
|
||||
"auth_mode": codex.auth_mode_label(load_settings().llm.model),
|
||||
"targets_info": args.targets_info,
|
||||
"scan_mode": args.scan_mode,
|
||||
"instruction": args.instruction,
|
||||
@@ -1097,7 +1050,7 @@ def main() -> None:
|
||||
|
||||
_telemetry_start_kwargs = {
|
||||
"model": load_settings().llm.model,
|
||||
"auth_mode": load_settings().llm.auth_mode,
|
||||
"auth_mode": codex.auth_mode_label(load_settings().llm.model),
|
||||
"scan_mode": args.scan_mode,
|
||||
"is_whitebox": is_whitebox_scan(args.targets_info),
|
||||
"interactive": not args.non_interactive,
|
||||
|
||||
@@ -262,7 +262,9 @@ def _is_subscription(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"
|
||||
return load_settings().llm.auth_mode == "subscription"
|
||||
from strix.auth import codex
|
||||
|
||||
return codex.is_subscription(load_settings().llm.model)
|
||||
|
||||
|
||||
def _int_stat(usage: dict[str, Any], key: str) -> int:
|
||||
|
||||
@@ -10,6 +10,7 @@ from uuid import uuid4
|
||||
|
||||
from agents.usage import Usage
|
||||
|
||||
from strix.auth import codex
|
||||
from strix.config.loader import load_settings
|
||||
from strix.core.paths import run_dir_for
|
||||
from strix.report.sarif import write_sarif
|
||||
@@ -118,10 +119,11 @@ class ReportState:
|
||||
self.scan_results: dict[str, Any] | None = None
|
||||
self.scan_config: dict[str, Any] | None = None
|
||||
self._llm_usage = LLMUsageLedger()
|
||||
# In subscription mode inference is covered by the user's plan, so there
|
||||
# is no metered cost — track tokens but report $0.
|
||||
auth_mode = load_settings().llm.auth_mode
|
||||
self._llm_usage.zero_cost = auth_mode == "subscription"
|
||||
# A subscription run is covered by the user's plan, so there is no metered
|
||||
# cost — track tokens but report $0.
|
||||
model = load_settings().llm.model
|
||||
auth_mode = codex.auth_mode_label(model)
|
||||
self._llm_usage.zero_cost = codex.is_subscription(model)
|
||||
self.run_record: dict[str, Any] = {
|
||||
"run_id": self.run_id,
|
||||
"run_name": self.run_name,
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_login_accepts_provider_aliases(provider: str, monkeypatch: pytest.Monke
|
||||
|
||||
monkeypatch.setattr(auth_cli, "_run_oauth_flow", _fake_flow)
|
||||
monkeypatch.setattr(codex, "save_record", lambda _record: None)
|
||||
monkeypatch.setattr(auth_cli, "_persist_subscription_config", lambda _model: None)
|
||||
monkeypatch.setattr(auth_cli, "_persist_subscription_config", lambda: None)
|
||||
|
||||
assert auth_cli.run_auth(["login", provider]) == 0
|
||||
assert reached["flow"] is True
|
||||
|
||||
+14
-25
@@ -69,36 +69,25 @@ def test_parse_redirect_input(value: str, expected: tuple[str | None, str | None
|
||||
@pytest.mark.parametrize(
|
||||
("model", "expected"),
|
||||
[
|
||||
("openai/gpt-5.5", "gpt-5.5"),
|
||||
("gpt-5.4", "gpt-5.4"),
|
||||
# A configured-but-unlisted OpenAI/bare name is passed through; the backend validates.
|
||||
("openai/gpt-5.6", "gpt-5.6"),
|
||||
# Another provider can't be served by a ChatGPT subscription → coerced to default.
|
||||
("anthropic/claude-opus-4-8", codex.DEFAULT_CODEX_MODEL),
|
||||
("deepseek/deepseek-v4-pro", codex.DEFAULT_CODEX_MODEL),
|
||||
("vertex_ai/gemini-3-pro", codex.DEFAULT_CODEX_MODEL),
|
||||
(None, codex.DEFAULT_CODEX_MODEL),
|
||||
("", codex.DEFAULT_CODEX_MODEL),
|
||||
],
|
||||
)
|
||||
def test_normalize_model(model: str | None, expected: str) -> None:
|
||||
assert codex.normalize_model(model) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "compatible"),
|
||||
[
|
||||
("openai/gpt-5.5", True),
|
||||
("gpt-5.4", True),
|
||||
("gpt-5.1-codex", True), # bare name: backend is the authority
|
||||
("openai/subscription", True),
|
||||
("OpenAI/Subscription", True), # case-insensitive
|
||||
(" openai/subscription ", True), # surrounding whitespace
|
||||
("openai/gpt-5.4", False),
|
||||
("anthropic/claude-opus-4-8", False),
|
||||
("deepseek/deepseek-v4-pro", False),
|
||||
("openai/subscription/gpt-5.5", False), # only the bare sentinel selects it
|
||||
("", False),
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
def test_is_backend_compatible(model: str | None, compatible: bool) -> None:
|
||||
assert codex.is_backend_compatible(model) is compatible
|
||||
def test_is_subscription(model: str | None, expected: bool) -> None:
|
||||
assert codex.is_subscription(model) is expected
|
||||
|
||||
|
||||
def test_resolve_and_label() -> None:
|
||||
assert codex.resolve_subscription_model() == codex.DEFAULT_CODEX_MODEL
|
||||
assert codex.auth_mode_label("openai/subscription") == "subscription"
|
||||
assert codex.auth_mode_label("openai/gpt-5.4") == "api_key"
|
||||
assert codex.auth_mode_label(None) == "api_key"
|
||||
|
||||
|
||||
def test_account_id_from_jwt() -> None:
|
||||
|
||||
@@ -63,6 +63,9 @@ def _response_payload() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
_CAPTURED: dict[str, Any] = {}
|
||||
|
||||
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args: Any) -> None:
|
||||
pass
|
||||
@@ -70,6 +73,8 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None:
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length) or b"{}")
|
||||
_CAPTURED.clear()
|
||||
_CAPTURED.update(body)
|
||||
if not body.get("stream"):
|
||||
payload = json.dumps({"detail": "Stream must be set to true"}).encode()
|
||||
self.send_response(400)
|
||||
@@ -136,3 +141,21 @@ async def test_codex_model_streams_and_aggregates(backend_url: str) -> None:
|
||||
response = await model.get_response(**_call_kwargs())
|
||||
assert response.output[0].content[0].text == "OK"
|
||||
assert response.usage.total_tokens == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_codex_model_self_enforces_backend_requirements(backend_url: str) -> None:
|
||||
# The caller passes ordinary settings; the model must impose the backend's
|
||||
# requirements (stream, store=false, encrypted reasoning) and the configured
|
||||
# reasoning effort itself.
|
||||
model = _CodexResponsesModel(
|
||||
model="gpt-5.4", openai_client=_client(backend_url), reasoning_effort="high"
|
||||
)
|
||||
kwargs = _call_kwargs()
|
||||
kwargs["model_settings"] = ModelSettings() # nothing special from the caller
|
||||
await model.get_response(**kwargs)
|
||||
|
||||
assert _CAPTURED["stream"] is True
|
||||
assert _CAPTURED["store"] is False
|
||||
assert _CAPTURED["include"] == ["reasoning.encrypted_content"]
|
||||
assert _CAPTURED["reasoning"] == {"effort": "high"}
|
||||
|
||||
@@ -35,7 +35,6 @@ async def test_persistent_rate_limit_stops_gracefully(
|
||||
settings = types.SimpleNamespace(
|
||||
llm=types.SimpleNamespace(
|
||||
model="openai/gpt-4o",
|
||||
auth_mode="api_key",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
timeout=300,
|
||||
|
||||
@@ -43,7 +43,6 @@ def _patch_engine_scaffold(
|
||||
settings = types.SimpleNamespace(
|
||||
llm=types.SimpleNamespace(
|
||||
model="openai/gpt-4o",
|
||||
auth_mode="api_key",
|
||||
reasoning_effort="high",
|
||||
force_required_tool_choice=False,
|
||||
timeout=300,
|
||||
|
||||
Reference in New Issue
Block a user