From 143b9e7040d8e325bc1ff5f5f7aff507a48300ec Mon Sep 17 00:00:00 2001 From: 0xallam Date: Mon, 8 Jun 2026 02:12:56 +0300 Subject: [PATCH] Pre-warm-up unknown-model warning + LiteLLM streaming hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warn on bare unknown model names before warm-up. is_known_openai_bare_model consults litellm.model_cost and matches only entries whose litellm_provider == "openai". When the configured STRIX_LLM has no provider prefix, isn't a known OpenAI model, and no LLM_API_BASE is set, show a clear panel pointing the user at the / form and exit before issuing the doomed request — no more chasing an "Incorrect API key" 401 from OpenAI when the user actually meant deepseek/, anthropic/, etc. Custom-base configs are still allowed through unconfirmed. Disable LiteLLM's message-logging and streaming-logging knobs to cut noise and skip one of the two end-of-stream submit paths. The other path at streaming_handler.py:2206 schedules work on a global ThreadPoolExecutor that loses to atexit shutdown when the interpreter is winding down; the SDK's stream consumer surfaces that as a fatal "cannot schedule new futures after shutdown" RuntimeError even though the actual stream content was already delivered. Catch and swallow that specific RuntimeError in _run_cycle so the scan isn't killed by an upstream end-of-stream logging race. --- strix/config/models.py | 14 +++++++++++++- strix/core/execution.py | 20 ++++++++++++++------ strix/interface/main.py | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/strix/config/models.py b/strix/config/models.py index feedbe7..c6775e2 100644 --- a/strix/config/models.py +++ b/strix/config/models.py @@ -95,11 +95,13 @@ def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> Non def _configure_litellm_compatibility() -> None: - """Enable LiteLLM's permissive param-handling mode.""" + """Enable LiteLLM's permissive param handling and disable its callbacks.""" import litellm litellm.drop_params = True litellm.modify_params = True + litellm.turn_off_message_logging = True + litellm.disable_streaming_logging = True def _configure_litellm_default(name: str, value: str) -> None: @@ -115,3 +117,13 @@ def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bo if "/" in model and not model.startswith("openai/"): return True return bool(settings.llm.api_base) + + +def is_known_openai_bare_model(model_name: str) -> bool: + import litellm + + name = model_name.strip().lower() + if not name or "/" in name: + return False + entry = litellm.model_cost.get(name) + return bool(entry and entry.get("litellm_provider") == "openai") diff --git a/strix/core/execution.py b/strix/core/execution.py index 84fe72f..1017f70 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -349,12 +349,20 @@ async def _run_cycle( # noqa: PLR0912 ) await coordinator.attach_stream(agent_id, stream) try: - async for event in stream.stream_events(): - if event_sink is not None: - try: - event_sink(agent_id, event) - except Exception: - logger.exception("stream event sink failed for %s", agent_id) + try: + async for event in stream.stream_events(): + if event_sink is not None: + try: + event_sink(agent_id, event) + except Exception: + logger.exception("stream event sink failed for %s", agent_id) + except RuntimeError as stream_exc: + if "after shutdown" not in str(stream_exc): + raise + logger.warning( + "Ignoring LiteLLM end-of-stream shutdown race for %s", + agent_id, + ) if stream.run_loop_exception is not None: raise stream.run_loop_exception finally: diff --git a/strix/interface/main.py b/strix/interface/main.py index ac3a86f..b5c56ad 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -22,7 +22,11 @@ from strix.config import ( load_settings, persist_current, ) -from strix.config.models import StrixProvider, configure_sdk_model_defaults +from strix.config.models import ( + StrixProvider, + configure_sdk_model_defaults, + is_known_openai_bare_model, +) from strix.core.paths import run_dir_for, runtime_state_dir from strix.interface.cli import run_cli from strix.interface.tui import run_tui @@ -215,7 +219,39 @@ async def warm_up_llm() -> None: configure_sdk_model_defaults(settings) llm = settings.llm - model = StrixProvider().get_model((llm.model or "").strip()) + raw_model = (llm.model or "").strip() + if ( + raw_model + and "/" not in raw_model + and not is_known_openai_bare_model(raw_model) + and not llm.api_base + ): + warn_text = Text() + warn_text.append("UNKNOWN MODEL NAME", style="bold yellow") + warn_text.append("\n\n", style="white") + warn_text.append(f"'{raw_model}'", style="bold cyan") + warn_text.append( + " is not a known OpenAI model. Bare names route to OpenAI by default.\n" + "If you meant a non-OpenAI provider, use the '", + style="white", + ) + warn_text.append("/", style="bold cyan") + warn_text.append( + "' form, e.g. 'anthropic/claude-opus-4-7', 'deepseek/deepseek-v4-pro'.", + style="white", + ) + console.print( + Panel( + warn_text, + title="[bold white]STRIX", + title_align="left", + border_style="yellow", + padding=(1, 2), + ), + ) + sys.exit(1) + + model = StrixProvider().get_model(raw_model) await asyncio.wait_for( model.get_response( system_instructions="You are a helpful assistant.",