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:
|
def _configure_litellm_compatibility() -> None:
|
||||||
"""Enable LiteLLM's permissive param-handling mode."""
|
"""Enable LiteLLM's permissive param handling and disable its callbacks."""
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
litellm.drop_params = True
|
litellm.drop_params = True
|
||||||
litellm.modify_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:
|
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/"):
|
if "/" in model and not model.startswith("openai/"):
|
||||||
return True
|
return True
|
||||||
return bool(settings.llm.api_base)
|
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")
|
||||||
|
|||||||
+14
-6
@@ -349,12 +349,20 @@ async def _run_cycle( # noqa: PLR0912
|
|||||||
)
|
)
|
||||||
await coordinator.attach_stream(agent_id, stream)
|
await coordinator.attach_stream(agent_id, stream)
|
||||||
try:
|
try:
|
||||||
async for event in stream.stream_events():
|
try:
|
||||||
if event_sink is not None:
|
async for event in stream.stream_events():
|
||||||
try:
|
if event_sink is not None:
|
||||||
event_sink(agent_id, event)
|
try:
|
||||||
except Exception:
|
event_sink(agent_id, event)
|
||||||
logger.exception("stream event sink failed for %s", agent_id)
|
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:
|
if stream.run_loop_exception is not None:
|
||||||
raise stream.run_loop_exception
|
raise stream.run_loop_exception
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+38
-2
@@ -22,7 +22,11 @@ from strix.config import (
|
|||||||
load_settings,
|
load_settings,
|
||||||
persist_current,
|
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.core.paths import run_dir_for, runtime_state_dir
|
||||||
from strix.interface.cli import run_cli
|
from strix.interface.cli import run_cli
|
||||||
from strix.interface.tui import run_tui
|
from strix.interface.tui import run_tui
|
||||||
@@ -215,7 +219,39 @@ async def warm_up_llm() -> None:
|
|||||||
configure_sdk_model_defaults(settings)
|
configure_sdk_model_defaults(settings)
|
||||||
llm = settings.llm
|
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(
|
await asyncio.wait_for(
|
||||||
model.get_response(
|
model.get_response(
|
||||||
system_instructions="You are a helpful assistant.",
|
system_instructions="You are a helpful assistant.",
|
||||||
|
|||||||
Reference in New Issue
Block a user