Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c3862f7
Studio: pick the model and quant the API usage examples name
NilayYadav Sep 4, 2026
5b104ae
Studio: name a downloaded model when none is resident, and follow the…
NilayYadav Sep 4, 2026
7e01906
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
77b37fe
Studio: pin the quant a directory qualifies, and check residency at t…
NilayYadav Sep 4, 2026
880ea7c
Studio: ship the keyless warning string, and read the switch from the…
NilayYadav Sep 4, 2026
cca6c91
Studio: publish the resident quant the scan proves while the resolver…
NilayYadav Sep 4, 2026
fa7ce29
Studio: let only the copy the resolver reaches advertise a repo's quants
NilayYadav Sep 4, 2026
c7a9e8e
Studio: offer one picker row per repo, whatever case the catalog spel…
NilayYadav Sep 4, 2026
a317a65
Studio: withdraw the quant list when two copies of one id disagree
NilayYadav Sep 4, 2026
9dc3eee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
1ec78ad
Studio: count a quantless copy as a conflict, and read residency from…
NilayYadav Sep 4, 2026
13be7ed
Studio: match quant labels case-insensitively, as the resolver does
NilayYadav Sep 4, 2026
355da91
Studio: keep the server's withheld quant list withheld when merging i…
NilayYadav Sep 4, 2026
2f5c983
Merge remote-tracking branch 'upstream/main' into r10313
NilayYadav Sep 4, 2026
bae2130
Studio: compare quant spellings the way the list dedupes them
NilayYadav Sep 4, 2026
f2fc8d0
Studio: say withheld quants out loud so a client cannot read it as an…
NilayYadav Sep 4, 2026
c31a4b1
Studio: honour a withheld quant list when following, and only label l…
NilayYadav Sep 4, 2026
2cd9504
Studio: follow the resolver's new vouched-quant helper in the source …
NilayYadav Sep 4, 2026
565e3f5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2026
0f2b5cb
Studio: keep a repo's quant list when a loose .gguf sits beside it, a…
NilayYadav Sep 4, 2026
b4a60ae
Studio: make following the loaded model reachable again, and keep eve…
NilayYadav Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion studio/backend/routes/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -25258,6 +25258,12 @@ def _servable_catalog_rows(
]


def _quant_list(first: str, *quant_sets) -> list[str]:
"""`first`, then every other quant once, in the order the scans reported them."""
rest = (q for quants in quant_sets for q in quants if q != first)
return [first, *dict.fromkeys(rest)]


async def _openai_catalog_objects() -> list[dict]:
"""Every model the server knows about for ``GET /v1/models``: the loaded
model(s) plus locally available (downloaded/cached) models discovered by
Expand All @@ -25280,7 +25286,17 @@ async def _openai_catalog_objects() -> list[dict]:
_servable_catalog_rows, catalog, catalog_at
):
cid = getattr(info, "model_id", None) or public_model_id(getattr(info, "id", None))
if not cid or cid in by_id:
if not cid:
continue
if cid in by_id:
# Already listed: the resident row, or an earlier scan row for the same
# public id. It names one quant; add the on-disk quants found here beside
# it, keeping the ones it already advertises, so a client can pin any.
listed = by_id[cid]
if listed.get("quant") and quants:
listed["quants"] = _quant_list(
listed["quant"], listed.get("quants", ()), quants
)
continue
if loaded and not is_gguf:
if resident_id in by_id:
Expand All @@ -25298,8 +25314,10 @@ async def _openai_catalog_objects() -> list[dict]:
resident_quant = getattr(get_llama_cpp_backend(), "hf_variant", None)
if is_gguf and loaded and resident_quant:
Comment on lines +25314 to +25322

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare duplicate quant inventories without order sensitivity

When two physical copies of the same model ID contain the same quant labels but their scans return the non-default labels in a different order, these tuples compare unequal and the code publishes quants: []. Every explicit quant pin is valid against either copy in this scenario, so the picker unnecessarily loses all alternatives even though the change promises to expose every on-disk quant. Compare a normalized set of labels for ambiguity while retaining one scan's order for display.

Useful? React with 👍 / 👎.

obj["quant"] = resident_quant
obj["quants"] = _quant_list(resident_quant, quants)
elif quants:
obj["quant"] = quants[0]
obj["quants"] = list(quants)
display = getattr(info, "display_name", None)
if display:
obj["display_name"] = display
Expand Down Expand Up @@ -25355,6 +25373,17 @@ async def openai_retrieve_model(model_id: str, current_subject: str = Depends(ge
for entry in _loaded:
eid = entry["id"]
if isinstance(eid, str) and eid.lower() == model_id.lower():
# The catalog row for a loaded model is this entry plus the on-disk
# `quants`, so prefer it: LIST and RETRIEVE must describe one model the
# same way. It is cached for seconds, and a failed scan is no reason to
# 404 a model that is loaded, so fall back to the bare entry.
try:
for model in await _openai_catalog_objects():
mid = model.get("id")
if isinstance(mid, str) and mid.lower() == model_id.lower():
return {**model, "loaded": True}
except Exception:
logger.debug("openai.retrieve_model.catalog_failed", exc_info = True)
return {**entry, "loaded": True}

objects = await _openai_catalog_objects()
Expand Down
98 changes: 98 additions & 0 deletions studio/backend/tests/test_openai_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,31 @@ async def _fake_catalog():
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
assert ids["publisher/Qwen3"]["loaded"] is True
assert ids["publisher/Qwen3"]["quant"] == "Q8_0"
# Every on-disk quant of the repo is listed too, the resident one first.
assert ids["publisher/Qwen3"]["quants"] == ["Q8_0", "Q4_K_M"]


def test_catalog_lists_every_on_disk_quant_per_repo(monkeypatch):
monkeypatch.setattr(inf, "get_llama_cpp_backend", lambda: _FakeLlama(loaded = False))
monkeypatch.setattr(inf, "get_inference_backend", lambda: _FakeUnsloth())

async def _fake_catalog():
return [
_Info("models--org--Foo", "Foo", model_id = "org/Foo"),
_Info("/data/models/Mistral-7B", "Mistral-7B", is_gguf = False),
]

monkeypatch.setattr(inf, "_cached_local_catalog", _fake_catalog)
monkeypatch.setattr(
resolver,
"local_servable_model",
lambda info: (True, ("UD-Q4_K_XL", "Q8_0")) if info.is_gguf else (False, ()),
)
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
# `quant` stays the pin a client appends by default; `quants` names the alternatives.
assert ids["org/Foo"]["quant"] == "UD-Q4_K_XL"
assert ids["org/Foo"]["quants"] == ["UD-Q4_K_XL", "Q8_0"]
assert "quants" not in ids["Mistral-7B"]


def test_a_nested_model_directory_is_not_the_resident_one(monkeypatch):
Expand Down Expand Up @@ -490,3 +515,76 @@ async def _fake_catalog():
monkeypatch.setattr(resolver, "local_servable_model", lambda info: (True, ("Q4_K_M",)))
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
assert ids["publisher/Qwen3"]["loaded"] is True


def _resident_repo_catalog(monkeypatch, *, on_disk, resident = "Q8_0"):
"""A llama backend loaded straight from an HF repo, so the scan row for that repo
meets a row the loaded pass already published under the same public id."""

class _RepoLlama(_FakeLlama):
model_identifier = "org/Foo"
hf_variant = resident

monkeypatch.setattr(inf, "get_llama_cpp_backend", lambda: _RepoLlama())
monkeypatch.setattr(inf, "get_inference_backend", lambda: _FakeUnsloth())
monkeypatch.setattr(inf, "_quant_reference_resolves", lambda model_id, quant: True)

async def _fake_catalog():
return [_Info("models--org--Foo", "Foo", model_id = "org/Foo")]

monkeypatch.setattr(inf, "_cached_local_catalog", _fake_catalog)
monkeypatch.setattr(
resolver, "local_servable_model", lambda info: (True, tuple(on_disk))
)


def test_the_resident_repo_row_gains_the_other_on_disk_quants(monkeypatch):
# The loaded pass publishes org/Foo first, so the scan row for the same repo hits
# the already-listed branch. Without it the resident model is the one model whose
# alternative quants a client cannot see.
_resident_repo_catalog(monkeypatch, on_disk = ("BF16", "Q4_K_M"))
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
assert ids["org/Foo"]["loaded"] is True
# The quant actually serving stays first; it is what a bare id resolves to.
assert ids["org/Foo"]["quant"] == "Q8_0"
assert ids["org/Foo"]["quants"] == ["Q8_0", "BF16", "Q4_K_M"]


def test_an_already_listed_row_keeps_the_quants_it_advertises(monkeypatch):
# Two scan rows can share one public id (an HF-cache repo also reachable through a
# custom scan folder). Merging must not drop quants the first row proved on disk.
_resident_repo_catalog(monkeypatch, on_disk = ("BF16",))
quant_sets = [("BF16",), ("Q4_K_M", "BF16")]
monkeypatch.setattr(
resolver, "local_servable_model", lambda info: (True, quant_sets.pop(0))
)

async def _two_rows():
return [
_Info("models--org--Foo", "Foo", model_id = "org/Foo"),
_Info("/scan/org/Foo", "Foo", model_id = "org/Foo"),
]

monkeypatch.setattr(inf, "_cached_local_catalog", _two_rows)
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
# Every quant either copy holds, once each, resident first.
assert ids["org/Foo"]["quants"] == ["Q8_0", "BF16", "Q4_K_M"]


def test_a_resident_model_with_no_other_quant_advertises_no_list(monkeypatch):
# An empty scan is not evidence of alternatives; `quant` alone must survive.
_resident_repo_catalog(monkeypatch, on_disk = ())
ids = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}
assert ids["org/Foo"]["quant"] == "Q8_0"
assert "quants" not in ids["org/Foo"]


def test_retrieve_describes_a_loaded_model_exactly_like_the_listing(monkeypatch):
# A client that reads GET /v1/models/{id} must see the same quants the listing
# advertises, or it can only pin alternatives for models that are not loaded.
_resident_repo_catalog(monkeypatch, on_disk = ("BF16", "Q4_K_M"))
listed = {m["id"]: m for m in asyncio.run(inf._openai_catalog_objects())}["org/Foo"]
retrieved = asyncio.run(inf.openai_retrieve_model("org/Foo", None))
assert retrieved["quants"] == listed["quants"] == ["Q8_0", "BF16", "Q4_K_M"]
assert retrieved["quant"] == listed["quant"]
assert retrieved["loaded"] is True
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export type OpenAIModel = {
loaded?: boolean;
// On-disk GGUF quant. Ids stay bare for OpenAI compat, so append `:quant` to pin it.
quant?: string;
// Every on-disk quant of the repo, `quant` first.
quants?: string[];
};

type ApiOpenAIModelList = {
data?: { id?: unknown; loaded?: unknown; quant?: unknown; task?: unknown }[];
data?: {
id?: unknown;
loaded?: unknown;
quant?: unknown;
quants?: unknown;
task?: unknown;
}[];
};

const CHAT_TASKS = new Set(["text-generation"]);
Expand All @@ -29,6 +37,9 @@ export function chatModelsFromCatalog(body: unknown): OpenAIModel[] {
id: entry.id,
loaded: entry.loaded === true,
quant: typeof entry.quant === "string" ? entry.quant : undefined,
quants: Array.isArray(entry.quants)
? entry.quants.filter((q): q is string => typeof q === "string")
: undefined,
},
]
: [],
Expand Down
Loading
Loading