Studio: hint at Model auto-switch in the OpenAI "No model loaded" 400 (#7006)
This commit is contained in:
parent
fef37cb25b
commit
7bfa209623
2 changed files with 79 additions and 7 deletions
|
|
@ -3344,6 +3344,21 @@ def _automatic_model_load_may_run() -> bool:
|
|||
return get_openai_auto_switch_enabled() or get_auto_unload_idle_seconds() > 0
|
||||
|
||||
|
||||
def _no_model_loaded_detail(base: str) -> str:
|
||||
"""Append a pointer to the opt-in auto-switch toggle to a "no model loaded"
|
||||
error, but only when it's off. Auto-switch (default off) cold-loads a
|
||||
requested downloaded GGUF, so an off toggle is the usual reason a request
|
||||
naming a listed model still 400/503s; surface the fix. With it on the name
|
||||
simply didn't resolve to a local GGUF, so the hint would mislead and is omitted."""
|
||||
from utils.openai_auto_switch_settings import get_openai_auto_switch_enabled
|
||||
|
||||
if get_openai_auto_switch_enabled():
|
||||
return base
|
||||
return base + (
|
||||
" Or enable Model auto-switch (Settings > API) to load a requested model automatically."
|
||||
)
|
||||
|
||||
|
||||
async def _maybe_auto_switch_model(
|
||||
requested_model: Optional[str],
|
||||
fastapi_request: Request,
|
||||
|
|
@ -6451,7 +6466,7 @@ async def openai_chat_completions(
|
|||
if not backend.active_model_name:
|
||||
raise HTTPException(
|
||||
status_code = 400,
|
||||
detail = "No model loaded. Call POST /inference/load first.",
|
||||
detail = _no_model_loaded_detail("No model loaded. Call POST /inference/load first."),
|
||||
)
|
||||
# Clean public id so the response never echoes a local path; the audio
|
||||
# branch below receives this sanitized label too.
|
||||
|
|
@ -9184,7 +9199,7 @@ async def openai_completions(request: Request, current_subject: str = Depends(ge
|
|||
if not llama_backend.is_loaded:
|
||||
raise HTTPException(
|
||||
status_code = 503,
|
||||
detail = "No GGUF model loaded. Load a GGUF model first.",
|
||||
detail = _no_model_loaded_detail("No GGUF model loaded. Load a GGUF model first."),
|
||||
)
|
||||
if not isinstance(body, dict):
|
||||
# Re-read to re-raise a malformed-body error (post-503, pre-feature behavior);
|
||||
|
|
@ -9400,7 +9415,7 @@ async def openai_embeddings(request: Request, current_subject: str = Depends(get
|
|||
if not llama_backend.is_loaded:
|
||||
raise HTTPException(
|
||||
status_code = 503,
|
||||
detail = "No GGUF model loaded. Load a GGUF model first.",
|
||||
detail = _no_model_loaded_detail("No GGUF model loaded. Load a GGUF model first."),
|
||||
)
|
||||
if not isinstance(body, dict):
|
||||
# Re-read to re-raise a malformed-body error (post-503, pre-feature behavior);
|
||||
|
|
@ -10140,7 +10155,7 @@ async def _responses_stream(
|
|||
# so the client sees a useful error instead of a dangling stream.
|
||||
raise HTTPException(
|
||||
status_code = 400,
|
||||
detail = (
|
||||
detail = _no_model_loaded_detail(
|
||||
"Streaming /v1/responses requires a GGUF model loaded via "
|
||||
"llama-server. Use non-streaming /v1/responses, "
|
||||
"/v1/chat/completions, or load a GGUF model."
|
||||
|
|
@ -11379,7 +11394,7 @@ async def anthropic_count_tokens(
|
|||
if not llama_backend.is_loaded:
|
||||
raise HTTPException(
|
||||
status_code = 503,
|
||||
detail = "No GGUF model loaded. Load a GGUF model first.",
|
||||
detail = _no_model_loaded_detail("No GGUF model loaded. Load a GGUF model first."),
|
||||
)
|
||||
|
||||
# Same Anthropic → OpenAI translation as anthropic_messages: system is
|
||||
|
|
@ -11452,7 +11467,7 @@ async def anthropic_messages(
|
|||
if not llama_backend.is_loaded and not _automatic_model_load_may_run():
|
||||
raise HTTPException(
|
||||
status_code = 503,
|
||||
detail = "No GGUF model loaded. Load a GGUF model first.",
|
||||
detail = _no_model_loaded_detail("No GGUF model loaded. Load a GGUF model first."),
|
||||
)
|
||||
|
||||
# max_tokens is a required field on the Anthropic Messages API; real Anthropic
|
||||
|
|
@ -11503,7 +11518,7 @@ async def anthropic_messages(
|
|||
if not llama_backend.is_loaded:
|
||||
raise HTTPException(
|
||||
status_code = 503,
|
||||
detail = "No GGUF model loaded. Load a GGUF model first.",
|
||||
detail = _no_model_loaded_detail("No GGUF model loaded. Load a GGUF model first."),
|
||||
)
|
||||
|
||||
# Advertised repo id after an auto-switch load, else a clean public id, never
|
||||
|
|
|
|||
|
|
@ -3037,3 +3037,60 @@ def test_acquire_swap_gate_is_cancellation_safe():
|
|||
inference_route._auto_switch_process_lock.release()
|
||||
|
||||
asyncio.run(asyncio.wait_for(main(), timeout = 5))
|
||||
|
||||
|
||||
def test_no_model_loaded_detail_appends_hint_only_when_off(monkeypatch):
|
||||
# The "no model loaded" errors point at the opt-in auto-switch toggle so a
|
||||
# request naming a listed-but-unloaded model is self-explanatory -- but only
|
||||
# when it's off. With it on the name simply didn't resolve, so no hint.
|
||||
base = "No GGUF model loaded. Load a GGUF model first."
|
||||
|
||||
monkeypatch.setattr(settings, "get_openai_auto_switch_enabled", lambda: False)
|
||||
off = inference_route._no_model_loaded_detail(base)
|
||||
assert off.startswith(base)
|
||||
assert "Model auto-switch" in off and "Settings > API" in off
|
||||
|
||||
monkeypatch.setattr(settings, "get_openai_auto_switch_enabled", lambda: True)
|
||||
assert inference_route._no_model_loaded_detail(base) == base
|
||||
|
||||
|
||||
def _run_responses_stream_no_model(monkeypatch, *, enabled, active_model_name):
|
||||
# Drive _responses_stream's GGUF-not-loaded guard: llama backend unloaded,
|
||||
# inference backend maybe holding a non-GGUF model. Returns the 400 detail.
|
||||
from fastapi import HTTPException
|
||||
from models.inference import ResponsesRequest, ChatMessage
|
||||
|
||||
monkeypatch.setattr(settings, "get_openai_auto_switch_enabled", lambda: enabled)
|
||||
monkeypatch.setattr(
|
||||
inference_route, "get_llama_cpp_backend", lambda: _FakeBackend(loaded_id = None)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
inference_route,
|
||||
"get_inference_backend",
|
||||
lambda: type("_B", (), {"active_model_name": active_model_name})(),
|
||||
)
|
||||
payload = ResponsesRequest(model = "unsloth/Qwen3.5-4B-GGUF", stream = True)
|
||||
messages = [ChatMessage(role = "user", content = "hi")]
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
asyncio.run(inference_route._responses_stream(payload, messages, None))
|
||||
assert exc.value.status_code == 400
|
||||
return exc.value.detail
|
||||
|
||||
|
||||
def test_responses_stream_hint_matches_toggle_regardless_of_active_model(monkeypatch):
|
||||
# Streaming /v1/responses shares the GGUF-only 400 with the other "no model
|
||||
# loaded" sites, so the auto-switch hint attaches whenever the toggle is
|
||||
# off -- including while a non-GGUF model is active, since auto-switch
|
||||
# evicts it to load a resolved GGUF (_maybe_auto_switch_model's resolver
|
||||
# branch has no active-model guard, unlike its reload-stash branch). Only
|
||||
# the toggle being on suppresses it.
|
||||
hinted = _run_responses_stream_no_model(monkeypatch, enabled = False, active_model_name = None)
|
||||
assert "Model auto-switch" in hinted
|
||||
|
||||
on = _run_responses_stream_no_model(monkeypatch, enabled = True, active_model_name = None)
|
||||
assert "Model auto-switch" not in on
|
||||
|
||||
non_gguf_loaded = _run_responses_stream_no_model(
|
||||
monkeypatch, enabled = False, active_model_name = "unsloth/Llama-3.2-1B-Instruct"
|
||||
)
|
||||
assert "Model auto-switch" in non_gguf_loaded
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue