#3366 changed getModelLabel() to strip only the first /-segment (fixing #3360 multi-slash proxy IDs). That regressed URI-scheme IDs like Yandex gpt://${FOLDER}/deepseek-v4-flash/latest — indexOf('/') lands inside the :// and leaves /${FOLDER}/... path junk in the composer model chip. Detect a scheme:// id, drop scheme+authority, and take the last meaningful path segment (skipping ${...} env-var placeholders and bare version tails like latest). Non-URI multi-slash IDs keep the #3360 first-segment-strip behavior unchanged. Node-driven regression test covers the URI case + the #3360 non-regression.
This commit is contained in:
1 parent
6703b39a2d
commit
c81c9649dd
2 files changed
+105
-1
No files matched your search
+23
-1
@@ -2900,7 +2900,29 @@ function getModelLabel(modelId){
|
||||
if(STATIC_LABELS[modelId]) return STATIC_LABELS[modelId];
|
||||
// Safe Ollama-tag fallback: strip only the first slash-segment (provider
|
||||
// prefix) so multi-slash IDs preserve their vendor hierarchy (#3360).
|
||||
let _last = modelId.includes('/') ? (modelId.slice(modelId.indexOf('/')+1) || modelId) : modelId;
|
||||
// URI-scheme ids (e.g. `gpt://${FOLDER}/deepseek-v4-flash/latest`, provider
|
||||
// `yandex:gpt`) must NOT be first-segment-stripped — `indexOf('/')` would
|
||||
// land inside the `://` and leave `/${FOLDER}/...` path junk (#3429). For a
|
||||
// `scheme://` id, drop the scheme + authority and take the last meaningful
|
||||
// path segment (ignoring a bare version tail like `latest`/`stable`).
|
||||
let _last;
|
||||
const _uriMatch = /^[a-z][a-z0-9+.-]*:\/\/(.+)$/i.exec(modelId);
|
||||
if (_uriMatch) {
|
||||
const _segs = _uriMatch[1].split('/').filter(Boolean);
|
||||
const _isVersionTail = (s) => /^(latest|stable|current|default|v?\d[\w.-]*)$/i.test(s);
|
||||
let _pick = '';
|
||||
for (let _i = _segs.length - 1; _i >= 0; _i--) {
|
||||
// Skip env-var placeholders (${...}) and bare version tails; prefer the
|
||||
// last segment that actually looks like a model name.
|
||||
if (/\$\{[^}]*\}/.test(_segs[_i])) continue;
|
||||
if (_isVersionTail(_segs[_i]) && _pick) continue;
|
||||
if (!_pick) _pick = _segs[_i];
|
||||
if (!_isVersionTail(_segs[_i])) { _pick = _segs[_i]; break; }
|
||||
}
|
||||
_last = _pick || _segs[_segs.length - 1] || modelId;
|
||||
} else {
|
||||
_last = modelId.includes('/') ? (modelId.slice(modelId.indexOf('/')+1) || modelId) : modelId;
|
||||
}
|
||||
// Strip @provider: prefix if present (e.g. @ollama-cloud:kimi-k2.6)
|
||||
if (_last.startsWith('@') && _last.includes(':')) _last = _last.split(':').slice(1).join(':');
|
||||
const looksLikeOllamaTag = /^[a-z0-9][\w.-]*:[\w.-]+$/i.test(_last);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Regression test for #3429 — getModelLabel() mangles URI-scheme model IDs.
|
||||
|
||||
PR #3366 (fix for #3360) changed getModelLabel() to strip only the first
|
||||
``/``-segment instead of ``split('/').pop()``. That fixed multi-slash proxy IDs
|
||||
but regressed URI-scheme IDs (e.g. Yandex ``gpt://${FOLDER}/deepseek-v4-flash/latest``)
|
||||
because ``indexOf('/')`` lands inside the ``://`` and leaves ``/${FOLDER}/...``
|
||||
path junk in the composer model chip.
|
||||
|
||||
The fix detects a ``scheme://`` id and takes the last meaningful path segment
|
||||
(skipping ``${...}`` env-var placeholders and bare version tails like ``latest``),
|
||||
while NOT touching the #3360 multi-slash behavior for non-URI ids.
|
||||
|
||||
Runs the live getModelLabel() via Node so drift between the test and the real
|
||||
code is caught immediately.
|
||||
"""
|
||||
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; }
|
||||
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):
|
||||
import json
|
||||
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_uri_scheme_model_id_label_is_model_name_not_path_junk():
|
||||
"""#3429: a gpt://.../model/latest id must label as the model name."""
|
||||
out = _labels([
|
||||
"gpt://${YANDEX_FOLDER_ID}/deepseek-v4-flash/latest",
|
||||
"gpt://folder123/deepseek-v4-flash/latest",
|
||||
"openrouter://acct/qwen-3-coder",
|
||||
"gpt://${F}/qwen-3-coder",
|
||||
])
|
||||
assert out["gpt://${YANDEX_FOLDER_ID}/deepseek-v4-flash/latest"] == "deepseek-v4-flash"
|
||||
assert out["gpt://folder123/deepseek-v4-flash/latest"] == "deepseek-v4-flash"
|
||||
assert out["openrouter://acct/qwen-3-coder"] == "qwen-3-coder"
|
||||
assert out["gpt://${F}/qwen-3-coder"] == "qwen-3-coder"
|
||||
# The env-var placeholder must never leak into the label.
|
||||
for label in out.values():
|
||||
assert "${" not in label, f"env-var placeholder leaked into label: {label!r}"
|
||||
|
||||
|
||||
def test_uri_fix_does_not_regress_multi_slash_or_bare_ids():
|
||||
"""#3360 multi-slash hierarchy + single-slash/bare ids stay correct."""
|
||||
out = _labels([
|
||||
"vendor_a/deepseek/deepseek-v4-pro", # #3360: keep vendor hierarchy
|
||||
"claude-sonnet-4-6", # bare id unchanged
|
||||
])
|
||||
# Non-URI multi-slash id keeps everything after the first segment (#3360).
|
||||
assert out["vendor_a/deepseek/deepseek-v4-pro"] == "deepseek/deepseek-v4-pro"
|
||||
assert out["claude-sonnet-4-6"] == "claude-sonnet-4-6"
|
||||
Reference in new issue
Block a user