103 lines
4.6 KiB
Python
103 lines
4.6 KiB
Python
"""Regression test for #7240 — model dropdown truncates custom model names
|
|
containing colons.
|
|
|
|
getModelLabel() derived the label of a ``@custom:`` id from the substring
|
|
following the LAST ``:``, so a tag/variant suffix like ``:free``/``:31b``/
|
|
``:397b`` inside the model's own name became the whole label
|
|
(``@custom:omni:kg/stepfun/step-3.7-flash:free`` rendered as ``free``).
|
|
|
|
The fix peels only the leading provider segment (``@custom:<slug>:`` — the
|
|
``<slug>`` is itself optional in the plain custom lane, and may be an endpoint
|
|
authority ``host:port``), leaving the full configured model id as the label.
|
|
|
|
Runs the live getModelLabel() via Node so drift between the test and the real
|
|
code is caught immediately.
|
|
"""
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
|
|
NODE = shutil.which("node")
|
|
|
|
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
|
|
|
_DRIVER = r"""
|
|
const fs = require('fs');
|
|
const ui = fs.readFileSync(process.argv[1], 'utf8');
|
|
// Slice getModelLabel() by function boundaries (regex literals inside it defeat
|
|
// a naive brace counter, so bound it by the next top-level function instead).
|
|
const start = ui.indexOf('function getModelLabel(');
|
|
if (start < 0) throw new Error('getModelLabel not found');
|
|
const after = ui.indexOf('\nfunction _gatewayProviderName(', start);
|
|
if (after < 0) throw new Error('getModelLabel end boundary not found');
|
|
const fnSrc = ui.slice(start, after);
|
|
const _dynamicModelLabels = {};
|
|
function _fmtOllamaLabel(s){ return s; }
|
|
// getModelLabel() calls the dotted Bedrock/Vertex prefix normalizer, which lives
|
|
// just above it with two Sets it closes over. Pull all three in, or the eval'd
|
|
// function ReferenceErrors on the first dotted id.
|
|
const _stripStart = ui.indexOf('const _BEDROCK_REGION_PREFIXES');
|
|
const _stripEnd = ui.indexOf('function getModelLabel(');
|
|
if (_stripStart < 0 || _stripStart > _stripEnd) throw new Error('_stripDottedModelPrefix block not found');
|
|
eval(ui.slice(_stripStart, _stripEnd));
|
|
eval(fnSrc);
|
|
const out = {};
|
|
for (const m of JSON.parse(process.argv[2])) out[m] = getModelLabel(m);
|
|
process.stdout.write(JSON.stringify(out));
|
|
"""
|
|
|
|
|
|
def _labels(model_ids):
|
|
proc = subprocess.run(
|
|
[NODE, "-e", _DRIVER, str(UI_JS_PATH), json.dumps(model_ids)],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
assert proc.returncode == 0, f"node driver failed: {proc.stderr}"
|
|
return json.loads(proc.stdout)
|
|
|
|
|
|
def test_custom_model_label_keeps_colon_tag_suffixes():
|
|
"""#7240: the full configured model id is the label — a trailing ``:tag``
|
|
inside the model name must never become the whole label."""
|
|
out = _labels([
|
|
# Repro ids from the issue: tag/variant suffix after the model name.
|
|
"@custom:omni:kg/stepfun/step-3.7-flash:free",
|
|
"@custom:omni:kg/tencent/hy3:free",
|
|
"@custom:omni:ollamacloud/gemma4:31b",
|
|
"@custom:omni:ollamacloud/qwen3.5:397b",
|
|
# Non-colon model names must keep working.
|
|
"@custom:omni:gemini/gemini-3.1-flash-lite",
|
|
"@custom:ai_gateway:Qwen3.6-35B-A3B",
|
|
])
|
|
assert out["@custom:omni:kg/stepfun/step-3.7-flash:free"] == "kg/stepfun/step-3.7-flash:free"
|
|
assert out["@custom:omni:kg/tencent/hy3:free"] == "kg/tencent/hy3:free"
|
|
assert out["@custom:omni:ollamacloud/gemma4:31b"] == "ollamacloud/gemma4:31b"
|
|
assert out["@custom:omni:ollamacloud/qwen3.5:397b"] == "ollamacloud/qwen3.5:397b"
|
|
assert out["@custom:omni:gemini/gemini-3.1-flash-lite"] == "gemini/gemini-3.1-flash-lite"
|
|
assert out["@custom:ai_gateway:Qwen3.6-35B-A3B"] == "Qwen3.6-35B-A3B"
|
|
|
|
|
|
def test_custom_model_label_plain_lane_and_host_port_slugs():
|
|
"""#7240 regression guards: the plain custom lane (no provider slug) keeps
|
|
the whole remainder, and an endpoint-authority slug (``host:port``) is
|
|
consumed as provider plumbing rather than leaking into the label."""
|
|
out = _labels([
|
|
# Plain custom lane: '@custom:<model>' with no slug separator.
|
|
"@custom:qwen397b-64k",
|
|
# Endpoint-style slug: ':port' belongs to the provider segment.
|
|
"@custom:10.8.71.41:8080:Qwen3-235B",
|
|
"@custom:localhost:11434:llama3",
|
|
# A digit-leading model after a NAMED slug is not a port — must not be
|
|
# peeled as if the slug were host:port.
|
|
"@custom:omni:11434:Qwen3",
|
|
])
|
|
assert out["@custom:qwen397b-64k"] == "qwen397b-64k"
|
|
assert out["@custom:10.8.71.41:8080:Qwen3-235B"] == "Qwen3-235B"
|
|
assert out["@custom:localhost:11434:llama3"] == "llama3"
|
|
assert out["@custom:omni:11434:Qwen3"] == "11434:Qwen3"
|