v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
"""Tests for #1106 — custom_providers[].models dict keys populate model dropdown."""
|
|
|
|
|
import pytest
|
|
|
|
|
import api.config as config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _reset():
|
|
|
|
|
try:
|
|
|
|
|
config.invalidate_models_cache()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _models_with_cfg(model_cfg=None, custom_providers=None, active_provider=None):
|
|
|
|
|
"""Temporarily patch config.cfg, call get_available_models(), restore.
|
|
|
|
|
|
|
|
|
|
Also pins _cfg_mtime to prevent reload_config() from overwriting patches.
|
|
|
|
|
"""
|
|
|
|
|
old_cfg = dict(config.cfg)
|
|
|
|
|
old_mtime = config._cfg_mtime
|
2026-07-05 21:08:20 +00:00
|
|
|
old_path = getattr(config, "_cfg_path", None)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
config.cfg.clear()
|
|
|
|
|
if model_cfg:
|
|
|
|
|
config.cfg["model"] = model_cfg
|
|
|
|
|
if custom_providers is not None:
|
|
|
|
|
config.cfg["custom_providers"] = custom_providers
|
|
|
|
|
try:
|
|
|
|
|
config._cfg_mtime = config.Path(config._get_config_path()).stat().st_mtime
|
|
|
|
|
except Exception:
|
|
|
|
|
config._cfg_mtime = 0.0
|
2026-07-05 21:08:20 +00:00
|
|
|
config._cfg_path = config._get_config_path()
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
try:
|
|
|
|
|
return config.get_available_models()
|
|
|
|
|
finally:
|
|
|
|
|
config.cfg.clear()
|
|
|
|
|
config.cfg.update(old_cfg)
|
|
|
|
|
config._cfg_mtime = old_mtime
|
2026-07-05 21:08:20 +00:00
|
|
|
config._cfg_path = old_path
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _all_model_ids(result):
|
2026-05-01 18:30:41 +00:00
|
|
|
"""Extract all model IDs from all groups (raw form — may include @provider: prefix)."""
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
ids = []
|
|
|
|
|
for g in result.get("groups", []):
|
|
|
|
|
for m in g.get("models", []):
|
|
|
|
|
ids.append(m["id"])
|
|
|
|
|
return ids
|
|
|
|
|
|
|
|
|
|
|
2026-05-01 18:30:41 +00:00
|
|
|
def _strip_at_prefix(model_id):
|
|
|
|
|
"""Strip the optional ``@provider:`` (or ``@provider:subname:``) prefix
|
|
|
|
|
from a model id so legacy assertions can compare against the bare form.
|
|
|
|
|
|
|
|
|
|
PR #1415 introduced provider-qualified IDs (``@custom:NAME:model``) for
|
|
|
|
|
named custom providers when the active provider differs. The bare-ID
|
|
|
|
|
assertions in this test module pre-date that change and need normalization
|
|
|
|
|
to keep checking the same invariant: "does model X appear in the picker".
|
|
|
|
|
"""
|
|
|
|
|
s = str(model_id or "")
|
|
|
|
|
if s.startswith("@") and ":" in s:
|
|
|
|
|
return s.rsplit(":", 1)[1]
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _all_model_ids_bare(result):
|
|
|
|
|
"""Same as _all_model_ids but with @provider: prefixes stripped."""
|
|
|
|
|
return [_strip_at_prefix(mid) for mid in _all_model_ids(result)]
|
|
|
|
|
|
|
|
|
|
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
def _group_for(result, provider_name):
|
|
|
|
|
"""Get a group by provider name."""
|
|
|
|
|
for g in result.get("groups", []):
|
|
|
|
|
if g.get("provider") == provider_name:
|
|
|
|
|
return g
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCustomProvidersModelsDict:
|
|
|
|
|
"""custom_providers entries with a 'models' dict should populate all keys in the dropdown."""
|
|
|
|
|
|
|
|
|
|
def test_models_dict_keys_appear_in_dropdown(self):
|
|
|
|
|
"""Each key in custom_providers[].models should appear as a selectable model."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "Llama-swap",
|
|
|
|
|
"base_url": "http://llama-swap:8880/v1",
|
|
|
|
|
"model": "unsloth-qwen3.6-35b-a3b",
|
|
|
|
|
"models": {
|
|
|
|
|
"unsloth-qwen3.6-35b-a3b": {"context_length": 262144},
|
|
|
|
|
"gemma4-26b": {},
|
|
|
|
|
"qwen3.5-27b": {},
|
|
|
|
|
"qwen3-coder-30b": {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-01 18:30:41 +00:00
|
|
|
ids = _all_model_ids_bare(result)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
for expected in ["unsloth-qwen3.6-35b-a3b", "gemma4-26b", "qwen3.5-27b", "qwen3-coder-30b"]:
|
|
|
|
|
assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}"
|
|
|
|
|
|
|
|
|
|
def test_models_dict_without_model_field_still_works(self):
|
|
|
|
|
"""If only 'models' dict is present (no singular 'model'), all dict keys should appear."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "Local-LLM",
|
|
|
|
|
"base_url": "http://localhost:8080/v1",
|
|
|
|
|
"models": {
|
|
|
|
|
"llama-3-8b": {},
|
|
|
|
|
"mistral-7b": {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-01 18:30:41 +00:00
|
|
|
ids = _all_model_ids_bare(result)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
assert "llama-3-8b" in ids
|
|
|
|
|
assert "mistral-7b" in ids
|
|
|
|
|
|
|
|
|
|
def test_no_duplicates_when_model_and_models_overlap(self):
|
|
|
|
|
"""If 'model' value also appears in 'models' dict, it should not be duplicated."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "MyServer",
|
|
|
|
|
"base_url": "http://myserver:8000/v1",
|
|
|
|
|
"model": "base-model",
|
|
|
|
|
"models": {
|
|
|
|
|
"base-model": {},
|
|
|
|
|
"other-model": {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-01 18:30:41 +00:00
|
|
|
ids = _all_model_ids_bare(result)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
assert ids.count("base-model") == 1, f"'base-model' should appear exactly once, got {ids.count('base-model')}"
|
|
|
|
|
assert "other-model" in ids
|
|
|
|
|
|
|
|
|
|
def test_unnamed_provider_models_dict_works(self):
|
|
|
|
|
"""custom_providers without 'name' should still populate 'Custom' group."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"model": "my-model",
|
|
|
|
|
"models": {
|
|
|
|
|
"extra-model-a": {},
|
|
|
|
|
"extra-model-b": {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids(result)
|
|
|
|
|
for expected in ["my-model", "extra-model-a", "extra-model-b"]:
|
|
|
|
|
assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}"
|
|
|
|
|
|
|
|
|
|
def test_empty_models_dict_is_ignored(self):
|
|
|
|
|
"""An empty 'models' dict should not break anything."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "TestServer",
|
|
|
|
|
"model": "only-model",
|
|
|
|
|
"models": {},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-01 18:30:41 +00:00
|
|
|
ids = _all_model_ids_bare(result)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
assert "only-model" in ids
|
|
|
|
|
|
|
|
|
|
def test_non_string_models_keys_are_skipped(self):
|
|
|
|
|
"""Non-string keys in models dict should be silently skipped."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "TestServer",
|
|
|
|
|
"model": "valid-model",
|
|
|
|
|
"models": {
|
|
|
|
|
"another-valid": {},
|
|
|
|
|
123: {}, # non-string key
|
|
|
|
|
None: {}, # non-string key
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-01 18:30:41 +00:00
|
|
|
ids = _all_model_ids_bare(result)
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
assert "valid-model" in ids
|
|
|
|
|
assert "another-valid" in ids
|
|
|
|
|
|
|
|
|
|
def test_multiple_custom_providers_each_keep_models_separate(self):
|
|
|
|
|
"""Multiple named custom_providers should each have their own models."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "Server-A",
|
|
|
|
|
"model": "model-a1",
|
|
|
|
|
"models": {"model-a2": {}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "Server-B",
|
|
|
|
|
"model": "model-b1",
|
|
|
|
|
"models": {"model-b2": {}},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
group_a = _group_for(result, "Server-A")
|
|
|
|
|
group_b = _group_for(result, "Server-B")
|
|
|
|
|
assert group_a is not None, "Server-A group missing"
|
|
|
|
|
assert group_b is not None, "Server-B group missing"
|
2026-05-01 18:30:41 +00:00
|
|
|
# PR #1415 prefixes model IDs with @custom:NAME: when the active provider
|
|
|
|
|
# is different from the named slug — strip for the bare-id invariant.
|
|
|
|
|
ids_a = [_strip_at_prefix(m["id"]) for m in group_a["models"]]
|
|
|
|
|
ids_b = [_strip_at_prefix(m["id"]) for m in group_b["models"]]
|
v0.50.221: copy HTTP fix, inline images, mobile tap, custom providers x2 (#1117)
* fix(#1096): copy buttons fall back to execCommand on HTTP contexts
- Add _copyText() helper: tries navigator.clipboard first, falls back to
document.execCommand('copy') with hidden textarea when not in secure context
- Update copyMsg() and addCopyButtons() to use helper instead of direct
navigator.clipboard.writeText()
- Code block copy button now has .catch() handler (was silently failing)
- Error messages use t('copy_failed') for i18n instead of hardcoded string
- Add copy_failed key to all 6 locale blocks (en, ru, es, de, zh, zh-Hant)
- Add 10 regression tests
* fix(#1095): render pasted/dragged images as inline preview instead of paperclip badge
- User message attachments with image extensions now render as <img> via
api/media endpoint, with click-to-fullscreen support
- Non-image attachments still show paperclip + filename badge
- Extracts filename from full path for display
- Add 5 regression tests
* fix: hoist _IMAGE_EXTS to module scope, add avif (absorb fix)
* fix: improve mobile touch responsiveness for session list items
iPad Safari has known issues with the click/dblclick pattern on touch:
- :hover-triggered padding-right layout shift causes the first tap click
to target the wrong element (actions button that just appeared)
- No touch-action:manipulation means iOS still delays taps for
double-tap zoom detection
- The old onclick+ondblclick pattern is designed for mouse, not touch
Changes:
- CSS: Remove :hover from padding-right rule to prevent layout shift
- CSS: Add touch-action:manipulation and -webkit-tap-highlight-color
to .session-item for immediate tap response
- JS: Replace onclick/ondblclick with onpointerup + manual 350ms
double-tap detection — works consistently on mouse and touch
* fix(#1106): iterate custom_providers[].models dict keys for dropdown population
- After reading singular 'model' field, also iterate 'models' dict keys
- Deduplicate: model field value not repeated if also in models dict
- Skip non-string keys gracefully
- Works for both named and unnamed custom_providers entries
- Add 7 regression tests
* fix(#1105): allow custom_providers hostnames through SSRF check
- Build trusted hostname set from custom_providers[].base_url in config.yaml
- These are user-explicitly configured endpoints — not SSRF risks
- Hardcoded allowlist (ollama, localhost, 127.0.0.1, lmstudio) still active
- Unknown private IPs still blocked
- Add 7 tests (5 source analysis + 2 functional with mocked socket)
* fix(tests): update hover padding assertions for #1110 touch fix (absorb)
* fix(css): restore hover padding via @media (hover:hover) for mouse devices (absorb)
* fix: filter right/middle-click from pointerup handler (absorb)
* docs: v0.50.221 release notes and version bump
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: sheng <378978764@qq.com>
2026-04-26 17:36:59 +00:00
|
|
|
assert "model-a1" in ids_a and "model-a2" in ids_a
|
|
|
|
|
assert "model-b1" in ids_b and "model-b2" in ids_b
|
|
|
|
|
# No cross-contamination
|
|
|
|
|
assert "model-b1" not in ids_a
|
|
|
|
|
assert "model-a1" not in ids_b
|
2026-05-01 15:14:12 +00:00
|
|
|
|
|
|
|
|
def test_named_custom_models_are_prefixed_when_not_active_provider(self):
|
|
|
|
|
"""Named custom provider models must carry a routing prefix when DeepSeek is active."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "deepseek", "default": "deepseek-v4-pro"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "sub2api",
|
|
|
|
|
"base_url": "http://127.0.0.1:8080/v1",
|
|
|
|
|
"model": "gpt-5.4-mini",
|
|
|
|
|
"models": {
|
|
|
|
|
"gpt-5.4-mini": {},
|
|
|
|
|
"gpt-5.4": {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
group = _group_for(result, "sub2api")
|
|
|
|
|
assert group is not None, "sub2api group missing"
|
|
|
|
|
assert group["provider_id"] == "custom:sub2api"
|
|
|
|
|
ids = [m["id"] for m in group["models"]]
|
|
|
|
|
assert "@custom:sub2api:gpt-5.4-mini" in ids
|
|
|
|
|
assert "@custom:sub2api:gpt-5.4" in ids
|
2026-05-16 04:44:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCustomProvidersModelsList:
|
|
|
|
|
"""custom_providers entries with a 'models' list should also populate the dropdown."""
|
|
|
|
|
|
|
|
|
|
def test_models_list_of_strings_appear_in_dropdown(self):
|
|
|
|
|
"""Each entry in custom_providers[].models list of strings should appear as a selectable model."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "MultiModel",
|
|
|
|
|
"base_url": "http://multi:8080/v1",
|
|
|
|
|
"model": "base-v1",
|
|
|
|
|
"models": ["model-a", "model-b", "model-c"],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
for expected in ["base-v1", "model-a", "model-b", "model-c"]:
|
|
|
|
|
assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}"
|
|
|
|
|
|
|
|
|
|
def test_models_list_of_dicts_with_id_appear_in_dropdown(self):
|
|
|
|
|
"""Each dict entry in models list with 'id' key should appear."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "ApiHub",
|
|
|
|
|
"models": [
|
|
|
|
|
{"id": "gpt-5", "label": "GPT-5"},
|
|
|
|
|
{"id": "claude-4", "label": "Claude Opus 4"},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
assert "gpt-5" in ids
|
|
|
|
|
assert "claude-4" in ids
|
|
|
|
|
|
|
|
|
|
def test_list_of_dicts_falls_back_to_model_name_when_no_id(self):
|
|
|
|
|
"""Dict entries in models list should fall back to 'model' or 'name' when 'id' is absent."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "FlexAPI",
|
|
|
|
|
"models": [
|
|
|
|
|
{"model": "via-model-key", "label": "From Model"},
|
|
|
|
|
{"name": "via-name-key", "label": "From Name"},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
assert "via-model-key" in ids
|
|
|
|
|
assert "via-name-key" in ids
|
|
|
|
|
|
|
|
|
|
def test_model_plus_list_dedup(self):
|
|
|
|
|
"""When singular 'model' also appears in 'models' list, it should not be duplicated."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "MyServer",
|
|
|
|
|
"model": "shared-model",
|
|
|
|
|
"models": ["shared-model", "unique-model"],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
assert ids.count("shared-model") == 1, f"'shared-model' should appear exactly once, got {ids.count('shared-model')}"
|
|
|
|
|
assert "unique-model" in ids
|
|
|
|
|
|
|
|
|
|
def test_empty_models_list_is_ignored(self):
|
|
|
|
|
"""An empty 'models' list should not break anything."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "TestServer",
|
|
|
|
|
"model": "only-model",
|
|
|
|
|
"models": [],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
assert "only-model" in ids
|
|
|
|
|
|
|
|
|
|
def test_unnamed_provider_models_list_works(self):
|
|
|
|
|
"""custom_providers without 'name' and with a 'models' list should still populate 'Custom' group."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"models": ["anon-a", "anon-b"],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids(result)
|
|
|
|
|
for expected in ["anon-a", "anon-b"]:
|
|
|
|
|
assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}"
|
|
|
|
|
|
|
|
|
|
def test_list_and_dict_providers_together(self):
|
|
|
|
|
"""A mix of list-format and dict-format custom providers should all contribute models."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "ListProv",
|
|
|
|
|
"models": ["list-m1", "list-m2"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "DictProv",
|
|
|
|
|
"models": {"dict-m1": {}, "dict-m2": {}},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
for expected in ["list-m1", "list-m2", "dict-m1", "dict-m2"]:
|
|
|
|
|
assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}"
|
|
|
|
|
|
|
|
|
|
def test_mixed_list_items_string_and_dict(self):
|
|
|
|
|
"""A single models list mixing strings and dicts should produce all entries."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "custom"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "MixedProv",
|
|
|
|
|
"models": [
|
|
|
|
|
"plain-string",
|
|
|
|
|
{"id": "dict-id", "label": "Dict Label"},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
ids = _all_model_ids_bare(result)
|
|
|
|
|
assert "plain-string" in ids
|
|
|
|
|
assert "dict-id" in ids
|
|
|
|
|
|
|
|
|
|
def test_named_custom_list_models_prefixed_when_not_active_provider(self):
|
|
|
|
|
"""List-format custom provider models must carry routing prefix when another provider is active."""
|
|
|
|
|
result = _models_with_cfg(
|
|
|
|
|
model_cfg={"provider": "deepseek", "default": "deepseek-v4-pro"},
|
|
|
|
|
custom_providers=[
|
|
|
|
|
{
|
|
|
|
|
"name": "sub2api",
|
|
|
|
|
"base_url": "http://127.0.0.1:8080/v1",
|
|
|
|
|
"models": ["gpt-5.4-mini", "gpt-5.4"],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
group = _group_for(result, "sub2api")
|
|
|
|
|
assert group is not None, "sub2api group missing"
|
|
|
|
|
assert group["provider_id"] == "custom:sub2api"
|
|
|
|
|
ids = [m["id"] for m in group["models"]]
|
|
|
|
|
assert "@custom:sub2api:gpt-5.4-mini" in ids, f"Expected @-prefixed ID, got {ids}"
|
|
|
|
|
assert "@custom:sub2api:gpt-5.4" in ids, f"Expected @-prefixed ID, got {ids}"
|