query transformers' mapping the way transformers defines it

Reported from production within the hour: `Qwen3.5-9B` now REFUSES pictures,
"served without a vision tower" — the exact opposite of what the vision
loading was for.

transformers' `_LazyAutoMapping.get` is not `dict.get`. Its signature is
`get(self, key, default)` and `default` is REQUIRED, so

    AutoModelForImageTextToText._model_mapping.get(type(cfg))

raises TypeError. `is_vision_checkpoint` caught it and answered "no vision
class" — for every checkpoint, always. Every vision model therefore loaded as
text, and then had its pictures refused by the guard added for exactly that
case. The probe I verified it with passed two arguments, so it tested a call
the code does not make.

Two tests now call what the code calls: one with a stub whose `get` mirrors
the required `default`, and one through the real mapping with a config class
this transformers actually ships.

The silence was the other half. `is_vision_checkpoint` returned False without
a word, so the engine log said nothing at all and the only symptom was a 400
that blamed the model's configuration — which was fine. `vision_report` now
carries the reason at every step, a checkpoint that looks like a vision model
and is not loaded as one says so in the log, and the refusal quotes what was
actually found rather than listing three things it might be.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 82b647de
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.5.26"
__version__ = "0.5.27"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
......@@ -1638,11 +1638,33 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
if any(isinstance(_it, dict) and _it.get('type') in ('image_url', 'input_image')
for _it in _c):
_release_instance()
# Say what was found, not just what is missing. The first
# version of this message named three possible causes and the
# engine logged nothing, so when the vision loading itself was
# broken (a one-argument call to transformers' mapping, which
# raises) the only symptom was this refusal -- and it pointed
# at the model's configuration, which was fine.
_why = ""
try:
from codai.backends import hf_vision as _hv
_r = _hv.vision_report(
getattr(current_manager, 'model_name', None)
or requested_model)
_why = _r.get("reason") or ""
if _r.get("cls") is not None:
_why = ("this checkpoint HAS a vision class "
f"({_r['cls'].__name__ if hasattr(_r['cls'], '__name__') else _r['cls']}) "
"but the loaded model is not using it — the "
"engine log says why")
except Exception:
_why = ""
raise HTTPException(status_code=400, detail=(
f"Model '{requested_model}' cannot read images: it is served "
f"without a vision tower (an HF checkpoint with no "
f"vision_config or processor, or a GGUF with no mmproj). "
f"Send the image to a model whose capabilities list "
f"without a vision tower"
+ (f" ({_why})" if _why else
" (an HF checkpoint with no vision_config or processor, "
"or a GGUF with no mmproj)")
+ f". Send the image to a model whose capabilities list "
f"image_to_text, or configure an mmproj for this one."))
# Inject system prompt if --system-prompt flag was provided
......
......@@ -73,34 +73,89 @@ def _config_for(model_name: str):
try:
from transformers import AutoConfig
return AutoConfig.from_pretrained(model_name, trust_remote_code=True)
except Exception:
except Exception as exc:
print(f"[vision] {model_name}: its config could not be read "
f"({type(exc).__name__}: {str(exc)[:160]})", flush=True)
return None
def is_vision_checkpoint(model_name: str, config=None) -> bool:
"""Whether this checkpoint carries a vision tower.
def vision_report(model_name: str, config=None) -> dict:
"""Why this checkpoint is, or is not, loaded with a vision tower.
Decided from the config, not from the model id: ``Qwen3.5-9B`` has no
"vl"/"vision" in its name and is a vision model, while plenty of models
with "vision" in the name are not image-text-to-text at all. The two
signals are a nested ``vision_config`` and an ``image_token_id`` -- a
decoder that has somewhere to put an image.
Every step is reported because every one of them used to fail silently.
Measured on zeiss, 2026-10-29: after the vision loading shipped, an image
sent to ``Qwen3.5-9B`` was refused with "served without a vision tower" —
and the engine logged nothing at all about why, because
:func:`is_vision_checkpoint` answered False and said nothing. A decision
this consequential has to leave a trace.
Keys: ``config`` (read or not), ``vision_config``, ``image_token_id``,
``cls`` (the class that would load it, or None), ``reason``.
"""
out = {"config": False, "vision_config": False, "image_token_id": False,
"cls": None, "reason": ""}
cfg = config if config is not None else _config_for(model_name)
if cfg is None:
return False
if getattr(cfg, "vision_config", None) is None and \
getattr(cfg, "image_token_id", None) is None:
return False
out["reason"] = "its config could not be read"
return out
out["config"] = True
out["cfg_class"] = type(cfg).__name__
out["vision_config"] = getattr(cfg, "vision_config", None) is not None
out["image_token_id"] = getattr(cfg, "image_token_id", None) is not None
if not out["vision_config"] and not out["image_token_id"]:
out["reason"] = ("its config has neither a vision_config nor an "
"image_token_id — it is a text checkpoint")
return out
# The config says there is a tower; the installed transformers must also
# have a class that loads it. Without this check a brand-new family would
# pick the vision class and fail the load outright, which is worse than
# serving it as text.
try:
from transformers import AutoModelForImageTextToText
return AutoModelForImageTextToText._model_mapping.get(type(cfg)) is not None
# BOTH arguments. transformers' `_LazyAutoMapping.get` is not dict.get:
# its signature is `get(self, key, default)` and `default` is
# REQUIRED. Calling it with one argument raises
#
# TypeError: _LazyAutoMapping.get() missing 1 required positional
# argument: 'default'
#
# which this function caught and reported as "no vision class" — for
# every checkpoint, always. So every vision model loaded as text and
# then had its pictures refused by the guard added for exactly that
# case. Measured on zeiss, 2026-10-29, against Qwen3.5-9B.
cls = AutoModelForImageTextToText._model_mapping.get(type(cfg), None)
except Exception as exc:
out["reason"] = (f"the image-text-to-text mapping could not be read "
f"({type(exc).__name__}: {str(exc)[:120]})")
return out
if cls is None:
out["reason"] = (f"this transformers has no image-text-to-text class "
f"for {type(cfg).__name__} ({_tf_version()})")
return out
out["cls"] = cls
return out
def _tf_version() -> str:
try:
import transformers
return f"transformers {transformers.__version__}"
except Exception:
return False
return "transformers version unknown"
def is_vision_checkpoint(model_name: str, config=None) -> bool:
"""Whether this checkpoint carries a vision tower.
Decided from the config, not from the model id: ``Qwen3.5-9B`` has no
"vl"/"vision" in its name and is a vision model, while plenty of models
with "vision" in the name are not image-text-to-text at all. The two
signals are a nested ``vision_config`` and an ``image_token_id`` -- a
decoder that has somewhere to put an image.
See :func:`vision_report` for the same decision with its reasons.
"""
return vision_report(model_name, config)["cls"] is not None
def auto_model_class(model_name: str):
......@@ -112,11 +167,23 @@ def auto_model_class(model_name: str):
"""
from transformers import AutoModelForCausalLM
try:
if is_vision_checkpoint(model_name):
report = vision_report(model_name)
if report["cls"] is not None:
from transformers import AutoModelForImageTextToText
return AutoModelForImageTextToText, True
except Exception:
pass
# Only worth a line when the checkpoint LOOKS like a vision model and
# is not being loaded as one: that is the case an operator has to be
# able to see, and the one that used to be silent.
if report.get("vision_config") or report.get("image_token_id"):
print(f"[vision] {model_name} looks like a vision checkpoint "
f"(vision_config={report['vision_config']}, "
f"image_token_id={report['image_token_id']}, "
f"config={report.get('cfg_class')}) but "
f"{report['reason']} — loading it as text", flush=True)
except Exception as exc:
print(f"[vision] {model_name}: could not decide on a model class "
f"({type(exc).__name__}: {str(exc)[:160]}) — loading it as text",
flush=True)
return AutoModelForCausalLM, False
......
......@@ -221,10 +221,15 @@ def test_an_image_to_a_blind_model_is_refused_before_it_is_flattened():
assert guard < flatten, (
"the placeholder is built before the refusal, so a blind model can "
"still answer a picture")
body = src[guard:guard + 1400]
# The whole guard block, not a fixed slice: a slice breaks the moment the
# message grows, which says nothing about the behaviour under test.
body = src[guard:src.index("# Inject system prompt", guard)]
assert "status_code=400" in body
assert "_release_instance()" in body, (
"refusing without releasing the instance slot leaks it")
assert "vision_report(" in body, (
"the refusal does not say what was actually found, which is how a "
"broken vision loader looked like a misconfigured model")
def test_a_gguf_without_an_mmproj_does_not_advertise_vision():
......@@ -234,3 +239,83 @@ def test_a_gguf_without_an_mmproj_does_not_advertise_vision():
assert '.gguf' in block and 'mmproj' in block
# a remote/pod-served model is the remote's business, not this listing's
assert "vllm" in block and "runpod" in block
# --------------------------------------------------------------------------
# The mapping call itself
# --------------------------------------------------------------------------
#
# Shipped broken and found in production the same day: transformers'
# `_LazyAutoMapping.get` is NOT dict.get — its signature is
# `get(self, key, default)` with `default` REQUIRED. Called with one argument
# it raises
#
# TypeError: _LazyAutoMapping.get() missing 1 required positional argument
#
# which this module caught and reported as "no vision class" — for every
# checkpoint, always. So every vision model loaded as text and then had its
# pictures refused by the guard added for exactly that case.
#
# The probe that "verified" it passed two arguments, so it tested a call the
# code does not make. These tests call what the code calls.
def test_the_mapping_is_queried_the_way_transformers_defines_it():
"""A stub whose `get` mirrors _LazyAutoMapping's required `default`."""
import transformers
from codai.backends import hf_vision
class _Cfg:
vision_config = {"depth": 2}
image_token_id = 7
class _Cls:
pass
class _StrictMapping:
def get(self, key, default): # default is REQUIRED, as upstream
return {_Cfg: _Cls}.get(key, default)
class _Auto:
_model_mapping = _StrictMapping()
real = transformers.AutoModelForImageTextToText
try:
transformers.AutoModelForImageTextToText = _Auto
report = hf_vision.vision_report("x/y", config=_Cfg())
finally:
transformers.AutoModelForImageTextToText = real
assert report["cls"] is _Cls, report["reason"]
def test_a_real_installed_vision_config_resolves_to_a_class():
"""No stub, no network, no weights: a config class this transformers
actually ships, through the real mapping."""
import transformers
from codai.backends import hf_vision
cfg = None
for name in ("LlavaConfig", "Qwen2VLConfig", "Blip2Config", "PaliGemmaConfig"):
klass = getattr(transformers, name, None)
if klass is None:
continue
try:
cfg = klass()
except Exception:
continue
break
if cfg is None:
pytest.skip("this transformers ships none of the probe configs")
report = hf_vision.vision_report("x/y", config=cfg)
assert report["cls"] is not None, report["reason"]
def test_a_failure_to_decide_is_never_silent():
"""The 400 that refuses a picture was the only trace of this. A decision
that consequential has to leave one of its own."""
src = _src("codai/backends/hf_vision.py")
rep = src.split("def vision_report", 1)[1].split("\ndef ", 1)[0]
assert '"reason"' in rep
auto = src.split("def auto_model_class", 1)[1].split("\ndef ", 1)[0]
assert "print(" in auto, (
"a vision checkpoint loaded as text says nothing in the log")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment