runpod: a seeded pod filed every model under text (v0.3.83)

In models.json a model's kind is the dict KEY it sits under. Only the
ENTRY travels to a pod, and the pod's seeder reads

    section = e.get("model_type") or "text_models"

A models.json entry carries no `model_type` -- so every capability pod
registered every seeded model as a TEXT model, whatever it was.

Found on a real A40 immediately after the gateway fix finally let a
request reach a pod. It booted in 303s, served /v1/models, listed
BAAI/bge-m3, and answered:

    Model 'BAAI/bge-m3' is a text model and cannot be used for embedding
    generation. Use the text endpoint instead.

Which is the expensive part: nothing looks broken. The routing is right,
the image is right, the catalogue is right, and the error blames the
caller for calling the wrong endpoint.

Both seed builders now carry the section -- `capability_seed_entries`
already had it and dropped it at `out.append(entry)`, and
`seed_model_env` gets it from the new `entry_model_type()` (by identity,
then by id, "" on any failure). Stamped on a COPY, so nothing writes a
field into models.json that this install never had, and an entry that
already declares its own `model_type` is not second-guessed.

Text pods were unaffected, which is why this survived: a vLLM pod serves
text, and OCR engines are picked by name rather than registered at all.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXRMe9ZPUQCCCtBai9uqt3
parent 94067d2c
......@@ -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.3.82"
__version__ = "0.3.83"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
......@@ -412,6 +412,41 @@ MODEL_TYPE_CAPABILITY = {
_LLM_MODEL_TYPES = ("text_models", "gguf_models", "vision_models")
def entry_model_type(entry: dict) -> str:
"""The models.json SECTION this entry lives in ("embedding_models", ...), or "".
In models.json a model's kind is the dict KEY it sits under, not a field on
the entry -- and the entry is all that travels to a pod. Matched by IDENTITY
first (the same dict object the loader holds), falling back to id/path so a
copied or rebuilt entry still resolves. Returns "" on any failure: this
feeds a seeding path, and a guess is never worth an exception there.
"""
try:
from codai.admin.routes import config_manager as _cm
md = getattr(_cm, "models_data", None) if _cm else None
if not isinstance(md, dict):
return ""
for section, lst in md.items():
if not isinstance(lst, list):
continue
for m in lst:
if m is entry:
return str(section)
want = str(entry.get("id") or entry.get("path") or "").strip().lower()
if not want:
return ""
for section, lst in md.items():
if not isinstance(lst, list):
continue
for m in lst:
if isinstance(m, dict) and str(
m.get("id") or m.get("path") or "").strip().lower() == want:
return str(section)
except Exception:
pass
return ""
def _in_embedding_section(entry: dict) -> bool:
"""Whether this entry lives in models.json's `embedding_models`.
......@@ -1620,6 +1655,19 @@ def seed_model_env(entry: dict, served: str = "", source: str = "",
)
out = {k: entry[k] for k in keep if k in entry and entry[k] is not None}
out["path"] = path
if not out.get("model_type"):
# WITHOUT THIS the pod registers it as a text model. `model_type` is in
# `keep` above, but a models.json entry does not carry one -- the kind
# is the section it sits under, and only the entry travels. The pod's
# seeder (ConfigManager._seed_models) reads `model_type` and falls back
# to "text_models", so an embeddings pod answered
# "Model 'BAAI/bge-m3' is a text model and cannot be used for
# embedding generation. Use the text endpoint instead."
# after a clean boot and a correct /v1/models -- which reads as the
# caller's mistake rather than a seeding bug.
_mt = entry_model_type(entry)
if _mt:
out["model_type"] = _mt
if engine:
# The pod runs this native engine for the model, whichever way it was
# said here (the runpod block's engine, or the model's own pin).
......@@ -4382,7 +4430,18 @@ def capability_seed_entries(capability: str, limit: int = 40) -> list:
path = str(entry.get("path") or "")
if not (_looks_like_repo_id(path) or path.startswith(("http://", "https://"))):
continue # the pod could never fetch it
out.append(entry)
# CARRY THE SECTION. In models.json the kind of a model is the dict
# KEY it sits under, not a field on the entry -- and the entry is
# all that travels. The pod's seeder reads `model_type` and falls
# back to "text_models", so without this every seeded model was
# registered on the pod as a TEXT model whatever it actually was.
#
# The symptom is not a routing error, which is what makes it
# expensive: the pod boots, serves /v1/models, lists the model --
# and then answers "Model 'BAAI/bge-m3' is a text model and cannot
# be used for embedding generation. Use the text endpoint instead."
# That reads as a configuration mistake on the caller's side.
out.append(dict(entry, model_type=section))
if len(out) >= limit:
return out
return out
......
"""A pod registers a seeded model as a TEXT model unless told otherwise.
In models.json a model's kind is the dict KEY it sits under — `embedding_models`,
`image_models`, … — not a field on the entry. Only the ENTRY travels to a pod
(`CODERAI_SEED_MODELS`), and the pod's seeder reads `model_type` with
section = e.get("model_type") or "text_models"
So every seeded model landed in `text_models` whatever it actually was.
Measured on a real A40 running coderai-embeddings: the pod booted, served
/v1/models, listed `BAAI/bge-m3` — and answered
Model 'BAAI/bge-m3' is a text model and cannot be used for embedding
generation. Use the text endpoint instead.
That is the expensive part. Nothing looks broken from outside: the routing is
right, the image is right, the catalogue is right, and the error blames the
caller for using the wrong endpoint.
Text-serving pods were unaffected, which is why this survived: a vLLM pod
serves text, and OCR engines are picked by name rather than registered as
models at all.
"""
import json
import pathlib
import sys
import pytest
ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from codai.api import runpod_worker as rw # noqa: E402
class _CM:
def __init__(self, data):
self.models_data = data
BGE = {"id": "BAAI/bge-m3", "alias": "bge-m3", "path": "BAAI/bge-m3",
"backend": "runpod", "runpod": {"engine": "coderai"}}
SDXL = {"id": "sdxl", "path": "stabilityai/sdxl-turbo"}
LLM = {"id": "qwen", "path": "Qwen/Qwen3-8B"}
@pytest.fixture
def models(monkeypatch):
data = {"text_models": [LLM], "image_models": [SDXL],
"embedding_models": [BGE]}
import codai.admin.routes as routes
monkeypatch.setattr(routes, "config_manager", _CM(data), raising=False)
return data
# ------------------------------------------------------- finding the section #
def test_the_section_is_found_by_identity(models):
assert rw.entry_model_type(BGE) == "embedding_models"
assert rw.entry_model_type(SDXL) == "image_models"
assert rw.entry_model_type(LLM) == "text_models"
def test_a_copied_entry_still_resolves_by_id(models):
"""The entry that reaches the seeder is not always the loader's own dict."""
assert rw.entry_model_type(dict(BGE)) == "embedding_models"
def test_an_unknown_entry_resolves_to_nothing(models):
assert rw.entry_model_type({"id": "nope", "path": "nope"}) == ""
assert rw.entry_model_type({}) == ""
def test_it_never_raises(monkeypatch):
"""It feeds a seeding path. A guess is not worth an exception there."""
import codai.admin.routes as routes
monkeypatch.setattr(routes, "config_manager", None, raising=False)
assert rw.entry_model_type(BGE) == ""
monkeypatch.setattr(routes, "config_manager", _CM("not a dict"), raising=False)
assert rw.entry_model_type(BGE) == ""
monkeypatch.setattr(routes, "config_manager",
_CM({"embedding_models": "not a list"}), raising=False)
assert rw.entry_model_type(BGE) == ""
# -------------------------------------------------- what the pod is told #
def test_a_per_model_pod_seed_carries_its_section(models):
"""THE regression: without model_type the pod files bge-m3 under text."""
got = json.loads(rw.seed_model_env(BGE, served="BAAI/bge-m3"))
entries = got if isinstance(got, list) else [got]
assert entries[0]["model_type"] == "embedding_models"
def test_an_explicit_model_type_on_the_entry_wins(models):
"""An entry that already says what it is is not second-guessed."""
e = dict(BGE, model_type="image_models")
got = json.loads(rw.seed_model_env(e, served="BAAI/bge-m3"))
entries = got if isinstance(got, list) else [got]
assert entries[0]["model_type"] == "image_models"
def test_a_seed_for_an_unknown_entry_is_still_produced(models):
"""Not knowing the section must not cost the pod its catalogue entry —
that would turn a wrong kind into no model at all."""
e = {"id": "stray", "path": "some-org/some-model"}
got = json.loads(rw.seed_model_env(e, served="some-org/some-model"))
entries = got if isinstance(got, list) else [got]
assert entries[0]["path"] == "some-org/some-model"
def test_a_capability_pod_seed_carries_its_section(models):
"""The capability path knows the section already — it iterates them — and
was dropping it on the floor at `out.append(entry)`."""
got = rw.capability_seed_entries("embeddings")
assert got and all(e.get("model_type") == "embedding_models" for e in got)
def test_a_capability_seed_does_not_mutate_models_json(models):
"""The stamp goes on a COPY. Writing it into the live entry would persist
a field this install never had."""
rw.capability_seed_entries("embeddings")
assert "model_type" not in BGE
def test_the_pod_seeder_defaults_to_text_which_is_why_this_matters():
"""Pin the far side of the contract: if the fallback ever changes, the
reason these tests exist changes with it."""
src = (ROOT / "codai/config.py").read_text()
assert 'e.get("model_type") or "text_models"' in src
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