Pre-warm-up unknown-model warning + LiteLLM streaming hardening
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 <provider>/<model> 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.
This commit is contained in:
+13
-1
@@ -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")
|
||||
|
||||
@@ -348,6 +348,7 @@ async def _run_cycle( # noqa: PLR0912
|
||||
hooks=hooks,
|
||||
)
|
||||
await coordinator.attach_stream(agent_id, stream)
|
||||
try:
|
||||
try:
|
||||
async for event in stream.stream_events():
|
||||
if event_sink is not None:
|
||||
@@ -355,6 +356,13 @@ async def _run_cycle( # noqa: PLR0912
|
||||
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:
|
||||
|
||||
+38
-2
@@ -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("<provider>/<model>", 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.",
|
||||
|
||||
Reference in New Issue
Block a user