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.",