3030from __future__ import annotations
3131
3232import json
33+ import os
3334import re
3435import time
3536import uuid
@@ -59,9 +60,28 @@ def Field(default=None, **kwargs): # type: ignore
5960 "gpt-3.5-turbo-instruct" : "Qwen/Qwen2.5-3B-Instruct" ,
6061}
6162
63+ # Names that route to the server's configured default model. A caller that has
64+ # no particular model in mind (including the native client's zero-argument
65+ # ``chat("hi")``) can send ``"effgen-default"`` or ``"default"`` and get an
66+ # answer without knowing a concrete id. The target is read from
67+ # ``EFFGEN_DEFAULT_MODEL`` at request time, falling back to a small local model.
68+ DEFAULT_MODEL_ALIASES : frozenset [str ] = frozenset ({"effgen-default" , "default" })
69+ _FALLBACK_DEFAULT_MODEL = "Qwen/Qwen2.5-3B-Instruct"
70+
71+
72+ def default_model_id () -> str :
73+ """Return the model id the ``effgen-default``/``default`` names resolve to.
74+
75+ Controlled by ``EFFGEN_DEFAULT_MODEL``; defaults to a small local model when
76+ the variable is unset or blank.
77+ """
78+ return os .environ .get ("EFFGEN_DEFAULT_MODEL" , "" ).strip () or _FALLBACK_DEFAULT_MODEL
79+
6280
6381def resolve_model_alias (model : str ) -> str :
6482 """Resolve an OpenAI model name to a local effGen model id."""
83+ if model in DEFAULT_MODEL_ALIASES :
84+ return default_model_id ()
6585 return MODEL_ALIASES .get (model , model )
6686
6787
@@ -501,9 +521,10 @@ def _effgen_meta(requested: str, resolved: str) -> dict[str, Any]:
501521 return {
502522 "requested_model" : requested ,
503523 "resolved_model" : resolved ,
504- # True only when an OpenAI compatibility alias (gpt-4 → local model) was
505- # applied — not for internal provider/model routing normalization.
506- "alias_applied" : requested in MODEL_ALIASES ,
524+ # True when a compatibility alias (gpt-4 → local model) or the
525+ # effgen-default/default name was applied — not for internal
526+ # provider/model routing normalization.
527+ "alias_applied" : requested in MODEL_ALIASES or requested in DEFAULT_MODEL_ALIASES ,
507528 }
508529
509530
@@ -526,6 +547,32 @@ def _messages_to_prompt(messages: list[Any]) -> str:
526547 return "\n " .join (parts )
527548
528549
550+ def _has_actionable_content (messages : list [Any ]) -> bool :
551+ """Return True if there is anything for the model to act on.
552+
553+ A request whose every message has empty/whitespace/``None``/absent text
554+ content — and no image parts, no ``tool_calls``, and no tool result — gives
555+ the model nothing to answer. Such a request is rejected with a 400 before a
556+ billed model call, matching the Agent layer's empty-task guard and OpenAI's
557+ own handling. A message counts as actionable when it has non-blank text, a
558+ non-empty multimodal content list, ``tool_calls``, or is a ``tool`` result.
559+ """
560+ for msg in messages :
561+ is_dict = isinstance (msg , dict )
562+ role = msg .get ("role" ) if is_dict else getattr (msg , "role" , None )
563+ content = msg .get ("content" ) if is_dict else getattr (msg , "content" , None )
564+ tool_calls = msg .get ("tool_calls" ) if is_dict else getattr (msg , "tool_calls" , None )
565+ if isinstance (content , str ) and content .strip ():
566+ return True
567+ if isinstance (content , list ) and content :
568+ return True
569+ if tool_calls :
570+ return True
571+ if role == "tool" and content is not None :
572+ return True
573+ return False
574+
575+
529576def create_openai_router (
530577 runner : Runner , * , extra_models : Callable [[], list [str ]] | None = None
531578) -> Any :
@@ -559,15 +606,32 @@ def create_openai_router(
559606 def _error_response (exc : Exception ) -> Any :
560607 """Return an OpenAI-style, redacted error JSONResponse for *exc*."""
561608 status , err_type , code = _classify_http (exc )
609+ message = str (exc )
610+ # A bare/unknown model id falls through to the local loader and fails
611+ # with a Transformers "not a valid model identifier" message. Add a
612+ # one-line hint pointing at the id shapes the server accepts.
613+ low = message .lower ()
614+ if code == "model_not_found" and (
615+ "not a valid model identifier" in low or "is not a local folder" in low
616+ ):
617+ message += (
618+ " Pass a provider-prefixed model id (e.g. 'openai:gpt-5-nano', "
619+ "'groq:llama-3.1-8b-instant'), a valid local model id, or "
620+ "'effgen-default'."
621+ )
562622 return JSONResponse (
563623 status_code = status ,
564- content = _error_payload (str ( exc ) , err_type , code ),
624+ content = _error_payload (message , err_type , code ),
565625 )
566626
567627 router = APIRouter (prefix = "/v1" , tags = ["openai-compat" ])
568628
569629 @router .get ("/models" )
570630 async def list_models () -> dict [str , Any ]:
631+ # The list is the drop-in aliases plus the ids this process has actually
632+ # served a successful response for this run. It is not exhaustive: any
633+ # `provider:model` id the server can reach (e.g. "openai:gpt-5-nano",
634+ # "groq:llama-3.1-8b-instant") is callable whether or not it appears here.
571635 now = _now ()
572636 data = [
573637 {
@@ -579,10 +643,19 @@ async def list_models() -> dict[str, Any]:
579643 }
580644 for alias , target in MODEL_ALIASES .items ()
581645 ]
646+ # The names that route to the server's configured default model.
647+ for name in sorted (DEFAULT_MODEL_ALIASES ):
648+ data .append ({
649+ "id" : name ,
650+ "object" : "model" ,
651+ "created" : now ,
652+ "owned_by" : "effgen" ,
653+ "root" : default_model_id (),
654+ })
582655 # Alongside the drop-in legacy aliases, list the ids the server has
583- # actually loaded and served this run (e.g. "openai:gpt-5-nano"),
584- # so a client discovers real, currently-servable models instead of
585- # only the 6 hardcoded OpenAI-flagship aliases.
656+ # actually served this run (e.g. "openai:gpt-5-nano"), so a client
657+ # discovers real, currently-servable models instead of only the
658+ # hardcoded aliases.
586659 if extra_models is not None :
587660 try :
588661 served = extra_models ()
@@ -611,6 +684,17 @@ async def chat_completions(request: ChatCompletionRequest) -> Any:
611684 400 , "messages must not be empty" , code = "empty_messages"
612685 ),
613686 )
687+ # A request whose messages carry no text, no image, no tool_calls, and no
688+ # tool result gives the model nothing to answer. Reject it with a 400
689+ # before any billed model call rather than returning a paid-for
690+ # non-answer at 200.
691+ if not _has_actionable_content (request .messages ):
692+ return JSONResponse (
693+ status_code = 400 ,
694+ content = error_envelope (
695+ 400 , "message content must not be empty" , code = "empty_content"
696+ ),
697+ )
614698 resolved = resolve_model_alias (request .model )
615699 model = resolved # echoed in the response 'model' field (what actually ran)
616700 prompt = _messages_to_prompt (request .messages )
0 commit comments