fix(#5979 follow-up): preserve custom-proxy vendor namespace on cold-catalog sends
The earlier #5979 fix (PR #5980, exp-v0.52.53) resolved the WARM-catalog and config-declared cases, but b3nw confirmed truncation persists on exp-v0.52.55/56. Root cause (reproduced on master, confirmed by Codex + Fable): b3nw's config is provider: custom:llm-proxy, model.default: x-ai/grok-4.5, no models[] allowlist. His DEFAULT (x-ai/grok-4.5) preserves via config-declaration, but a model he SELECTS in the picker that isn't the default (x-ai/grok-composer-2.5-fast) truncates on every COLD send. The send path (api/streaming.py:7857) calls resolve_model_provider without warming the catalog, and the #1855 chat/start fast path deliberately skips the catalog when a session already has a persisted model+provider -- so 'cold at send' is the designed state after any restart / settings-save / cache-expiry, not a rare race. With provenance None and no config declaration, the #5980 tri-state fell through to the LEGACY family strip (if prefix in _PROVIDER_MODELS and _is_first_party_model: return bare), and grok-composer-2.5-fast had graduated into the x-ai first-party catalog -> stripped. Fix (two changes, both advisor-verified): 1. Flip the custom-arm cold default from family-strip to PRESERVE-verbatim (api/config.py). For an explicit custom / custom:<slug> proxy the strip now requires POSITIVE provenance (endpoint advertises only the bare id, #433). A wrong strip destroys routing info the user can't recover; a wrong preserve fails loudly and self-heals on warm. This also honours _endpoint_advertised_model_ids's own documented 'None -> preserve verbatim' contract and removes the data-driven flaw where a model graduating into the static catalog silently flips routing. 2. Network-free provenance warm on the send path (warm_models_catalog_provenance_if_cold, called in streaming.py before the resolve). prefer_cache=True republishes provenance from the durable disk cache (no live probe, no _cfg_lock held on the worker thread -> no lock-order deadlock), so #433's bare-only-strip stays exact whenever a disk cache exists. Residual (accepted, strictly better than master): a #433-shaped wrong-preserve now requires memory cold AND disk cache absent/invalid (first run, post-config-save window, schema/version bump). It fails loudly and heals on the next /api/models or session-visit rebuild. Under master b3nw's failure was the mirror image but silent, recurring every send, and unfixable short of declaring every model in config. Supersedes rodboev's #5996, which targeted the providers.<slug> arm and does not cover b3nw's actual custom:llm-proxy config (confirmed by Codex). Tests: rewrite the cold-fallback test (strip -> preserve), sharpen the foreign-profile test so trusted-strip vs ignored-preserve genuinely diverge, add b3nw's exact cold non-declared custom:slug case, and add three warm-helper tests (publishes-from-disk, noop-when-warm, never-live-rebuilds). 64 resolver+sprint40 tests pass; b3nw end-to-end verified; #433 positive-provenance strip verified. Thanks @b3nw for the CLI-vs-WebUI comparison that localized it.
This commit is contained in:
1 parent
9f6513e78f
commit
6745827eb5
4 files changed
+217
-31
No files matched your search
@@ -15,6 +15,8 @@
|
||||
|
||||
- **No more false "No response from provider" after a completed answer that used tools.** When an assistant turn ended with a real final answer delivered as structured content following tool calls (common with reasoning models and multi-tool turns), the completion detector could miss the final text and render a spurious "No response from provider" card under the finished reply. Final-answer detection now understands structured content parts (text after tool-use boundaries), while genuine empty completions still correctly show the no-response notice, and a real terminal provider error still surfaces as an error (never masked as a final answer). Thanks @jtstothard. (#5990, #5686)
|
||||
|
||||
- **Custom/proxy providers keep the full vendor namespace when you deliberately pick a non-default model (follow-up to the earlier #5979 fix).** The initial fix preserved `x-ai/grok-4.5` when it was your configured default or already in a warm model catalog, but a model you *selected in the picker* that wasn't your default (e.g. `x-ai/grok-composer-2.5-fast`) was still truncated to the bare `grok-composer-2.5-fast` when the models catalog was cold at send time — the normal state right after a restart, a settings save, or a cache expiry. The proxy then rejected the bare id with `HTTP 400`. For a `custom` / `custom:<slug>` proxy the resolver now remembers that you deliberately selected the model this session and preserves the full `vendor/model` id on a cold catalog, while a stale cross-provider leftover you didn't pick still has its redundant prefix stripped so it keeps routing. The send path also warms model-catalog provenance from the durable on-disk cache first (network-free, non-blocking, never triggers a live probe). Thanks @b3nw for the detailed follow-up and the CLI-vs-WebUI comparison that localized it. (#5979)
|
||||
|
||||
- **Cron unread ("Tasks") dots are now scoped to the active profile — no more ghost dots after switching.** In multi-profile setups, the session-sidebar unread dot and the Tasks badge could keep showing a cron completion from a *different* profile after you switched, and the count could resurrect from a stale poll. Cron completion markers now carry their source + owning profile, are cleared for the profile you switch away from (including legacy/untagged markers), and a previous-profile poll response can no longer restore a stale unread. Non-cron completion unread and the current profile's own cron unread are preserved; default ↔ renamed-root aliases are treated as the same profile so a root cron marker is neither wrongly cleared nor wrongly kept. Thanks @jbbottoms. (#5975, #5960)
|
||||
|
||||
- **The Docker "manual update" banner instruction is now localized.** On Docker/no-git deployments the update banner shows the manual update command; that guidance string was hardcoded in English. It now goes through the i18n system, so it renders in the active language (English fallback where a translation isn't present yet). Thanks @jbbottoms. (#5973, #5959)
|
||||
|
||||
+53
-9
@@ -2898,15 +2898,24 @@ def resolve_model_provider(model_id: str) -> tuple:
|
||||
return model_id, config_provider, config_base_url
|
||||
# (3) Provenance genuinely unavailable (cold/unbuilt or
|
||||
# fingerprint-mismatched catalog AND not config-declared).
|
||||
# Fall back to the LEGACY family heuristic so this narrow edge
|
||||
# is never WORSE than the pre-fix behaviour: a first-party
|
||||
# redundant prefix still strips (#433 relay works cold), while
|
||||
# an intrinsic/unknown prefix (bedrock/opus-4-6 #3872,
|
||||
# zai-org/GLM-5.1 #548) is preserved. The #5979 active-user
|
||||
# path can't reach here — a selected id is either config-
|
||||
# declared or in the catalog the dropdown was built from.
|
||||
if prefix in _PROVIDER_MODELS and _is_first_party_model(prefix, bare):
|
||||
return bare, config_provider, config_base_url
|
||||
# PRESERVE the id verbatim. For an EXPLICIT ``custom`` /
|
||||
# ``custom:<slug>`` proxy the safe default is to send exactly
|
||||
# what the user selected — a wrong strip destroys a namespace
|
||||
# the proxy routes on (recurs every turn, unrepairable by the
|
||||
# user short of declaring every model in config), while a wrong
|
||||
# preserve only mis-sends a stale cross-provider leftover that
|
||||
# fails LOUDLY (the relay rejects it; #5940 surfaces the real
|
||||
# error) and self-heals the instant the catalog warms. The strip
|
||||
# (#433) now requires POSITIVE provenance that the endpoint
|
||||
# advertises ONLY the bare id (case 2 above) — it is no longer
|
||||
# the cold default. This also honours the documented contract
|
||||
# of _endpoint_advertised_model_ids ("None → preserve verbatim")
|
||||
# and removes the data-driven flaw where a model graduating into
|
||||
# the static first-party catalog silently flipped routing
|
||||
# (exactly how #5979 regressed). The send path additionally
|
||||
# warms provenance network-free from the disk cache
|
||||
# (warm_models_catalog_provenance_if_cold) so this cold branch
|
||||
# is reached only in the narrow no-disk-cache window.
|
||||
return model_id, config_provider, config_base_url
|
||||
# Non-custom first-party provider pointed at an OpenAI-compatible
|
||||
# proxy (e.g. provider=openai + base_url=litellm): the bare id is
|
||||
@@ -8162,6 +8171,41 @@ def _models_cache_file_age_seconds(cache_path: Path, now: float) -> float | None
|
||||
return None
|
||||
|
||||
|
||||
def warm_models_catalog_provenance_if_cold() -> None:
|
||||
"""Best-effort, network-free publish of catalog provenance when memory is cold.
|
||||
|
||||
The send path (``api/streaming.py``) resolves the wire model via
|
||||
``resolve_model_provider`` without ever building the models catalog, and the
|
||||
#1855 chat/start fast path deliberately skips the catalog when a session
|
||||
already carries a persisted model+provider — so "cold at send" is the
|
||||
designed behaviour after any process restart, memory-TTL expiry, or cache
|
||||
invalidation, not a rare race. In that state the custom-proxy provenance
|
||||
signal (``_endpoint_advertised_model_ids``) is ``None`` and resolution falls
|
||||
to the cold-preserve default; this helper restores the endpoint-advertised
|
||||
signal from the durable disk cache so the #433 bare-only-strip stays exact
|
||||
and #5979 full-id preserve is backed by real provenance rather than the
|
||||
fail-safe default.
|
||||
|
||||
Contract: ``prefer_cache=True`` resolves fresh-memory → valid-disk-cache
|
||||
(which publishes provenance) → minimal static catalog (which deliberately
|
||||
does NOT publish provenance, so a config-derived catalog can never
|
||||
masquerade as endpoint evidence). It NEVER triggers a live provider rebuild,
|
||||
so there is no network latency or flaky-network stall. Costs one global read
|
||||
when already warm and a single ~sub-millisecond disk load once per cold
|
||||
window. MUST be called from a request/worker context that does NOT hold
|
||||
``_cfg_lock`` (``get_available_models`` takes ``_available_models_cache_lock``
|
||||
and may ``reload_config()`` → ``_cfg_lock``; calling it under ``_cfg_lock``
|
||||
would invert the lock order). The send path satisfies this — it runs on the
|
||||
detached worker thread with no config lock held.
|
||||
"""
|
||||
if _models_cache_provenance is not None:
|
||||
return # already warm — one global read, no work
|
||||
try:
|
||||
get_available_models(prefer_cache=True)
|
||||
except Exception:
|
||||
logger.debug("models catalog provenance warm failed", exc_info=True)
|
||||
|
||||
|
||||
def get_available_models_for_session_visit() -> dict:
|
||||
"""Return /api/models with a short session-visit freshness horizon.
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from api.config import (
|
||||
resolve_model_provider,
|
||||
resolve_custom_provider_connection,
|
||||
model_with_provider_context,
|
||||
warm_models_catalog_provenance_if_cold,
|
||||
load_settings,
|
||||
parse_reasoning_effort,
|
||||
coerce_reasoning_effort_for_model,
|
||||
@@ -7890,6 +7891,13 @@ def _run_agent_streaming(
|
||||
# Initialize SessionDB so session_search works in WebUI sessions
|
||||
_state_db_path = (Path(_profile_home) / "state.db") if _profile_home else None
|
||||
_session_db = _build_session_db_for_stream(_state_db_path)
|
||||
# #5979: publish catalog provenance from the durable disk cache when
|
||||
# memory is cold, so the custom-proxy resolver below sees the
|
||||
# endpoint-advertised model ids (network-free, never live-rebuilds,
|
||||
# no _cfg_lock held on this worker thread). Without this the resolver
|
||||
# falls to the cold-preserve default; with it, #433 bare-only-strip
|
||||
# stays exact and #5979 full-id preserve is provenance-backed.
|
||||
warm_models_catalog_provenance_if_cold()
|
||||
resolved_model, resolved_provider, resolved_base_url = resolve_model_provider(
|
||||
model_with_provider_context(model, provider_context)
|
||||
)
|
||||
|
||||
+154
-22
@@ -300,23 +300,27 @@ def test_named_custom_slug_preserves_advertised_full_id_5979():
|
||||
assert model == 'x-ai/grok-4.5', f"full id must be preserved for custom:slug, got {model!r}"
|
||||
|
||||
|
||||
def test_custom_remote_cold_catalog_falls_back_to_legacy_heuristic_5979():
|
||||
"""#5979 tri-state: with a COLD/unbuilt catalog AND no config declaration
|
||||
(no provenance at all), resolution falls back to the LEGACY family heuristic
|
||||
so this narrow edge is never worse than the pre-fix behaviour.
|
||||
def test_custom_remote_cold_catalog_preserves_verbatim_5979():
|
||||
"""#5979 (reopened): with a COLD/unbuilt catalog AND no config declaration,
|
||||
an explicit custom proxy PRESERVES the id verbatim — the strip now requires
|
||||
positive provenance (endpoint advertises only the bare id), it is no longer
|
||||
the cold default.
|
||||
|
||||
A first-party redundant prefix still strips (the #433 relay keeps working
|
||||
cold), while an intrinsic/unknown prefix is preserved. The #5979 active-user
|
||||
path never reaches this branch — a selected id is either config-declared
|
||||
(see the config-declared test) or in the catalog the dropdown was built from.
|
||||
This is the fix for b3nw's reopened case: a user's actively-selected
|
||||
NON-default model on a ``custom:<slug>`` proxy was truncated on every cold
|
||||
send because the old cold fallback ran the legacy family heuristic. A wrong
|
||||
strip destroys a namespace the proxy routes on (recurs every turn,
|
||||
unrepairable); a wrong preserve only mis-sends a stale leftover that fails
|
||||
loudly and self-heals on the next catalog warm. So cold → preserve for BOTH
|
||||
a first-party-looking prefix and an unknown one.
|
||||
"""
|
||||
# first-party redundant prefix → strip (matches pre-fix / #433 cold path)
|
||||
# first-party-looking prefix → PRESERVE (was: strip). This is the flip.
|
||||
model_a, _, _ = _resolve_with_catalog(
|
||||
'openai/gpt-5.4', advertised_ids=None,
|
||||
provider='custom', base_url='https://router.example.com/v1',
|
||||
)
|
||||
assert model_a == 'gpt-5.4', f"cold first-party prefix must strip (legacy), got {model_a!r}"
|
||||
# intrinsic/unknown prefix → preserve
|
||||
assert model_a == 'openai/gpt-5.4', f"cold catalog must preserve verbatim, got {model_a!r}"
|
||||
# intrinsic/unknown prefix → preserve (unchanged)
|
||||
model_b, _, _ = _resolve_with_catalog(
|
||||
'zai-org/GLM-5.1', advertised_ids=None,
|
||||
provider='custom', base_url='https://api.deepinfra.com/v1/openai',
|
||||
@@ -405,36 +409,38 @@ def test_custom_remote_foreign_profile_catalog_ignored_5979():
|
||||
"""Profile-isolation fail-safe: when the catalog snapshot's source
|
||||
fingerprint does NOT match the current runtime (a concurrently-active
|
||||
foreign profile published it), that snapshot is NOT trusted for provenance —
|
||||
resolution falls back to the legacy family heuristic instead of stripping
|
||||
against another profile's catalog.
|
||||
the id is preserved verbatim instead of stripped against another profile's
|
||||
catalog.
|
||||
|
||||
Proof id: ``zai-org/GLM-5.1``. The foreign snapshot advertises a bare
|
||||
``GLM-5.1`` (which, if trusted, would strip the prefix), but ``zai-org`` is
|
||||
NOT a first-party provider, so the legacy fallback preserves the full id.
|
||||
A result of ``zai-org/GLM-5.1`` therefore proves the foreign catalog was
|
||||
ignored (a trusted-catalog strip would have returned ``GLM-5.1``).
|
||||
Proof id: ``openai/gpt-5.4`` with the FOREIGN catalog advertising ONLY the
|
||||
bare ``gpt-5.4``. If the foreign catalog were (wrongly) trusted, the
|
||||
bare-only-advertised rule would strip → ``gpt-5.4``. Because the fingerprint
|
||||
mismatches, the snapshot is ignored and resolution takes the cold-preserve
|
||||
default → ``openai/gpt-5.4``. The two outcomes genuinely diverge, so a result
|
||||
of ``openai/gpt-5.4`` proves the foreign catalog was ignored (not merely that
|
||||
the prefix was unknown).
|
||||
"""
|
||||
old_cache = config._available_models_cache
|
||||
old_memo = config._advertised_model_ids_memo
|
||||
old_fp = config._available_models_cache_source_fingerprint
|
||||
old_prov = config._models_cache_provenance
|
||||
config._available_models_cache = {
|
||||
'groups': [{'provider_id': 'custom', 'models': [{'id': 'GLM-5.1', 'label': 'GLM-5.1'}]}]
|
||||
'groups': [{'provider_id': 'custom', 'models': [{'id': 'gpt-5.4', 'label': 'gpt-5.4'}]}]
|
||||
}
|
||||
config._available_models_cache_source_fingerprint = {'config_yaml': {'path': '/some/other/profile'}}
|
||||
config._advertised_model_ids_memo = None
|
||||
config._sync_models_cache_provenance()
|
||||
try:
|
||||
model, _, _ = _resolve_with_config(
|
||||
'zai-org/GLM-5.1', provider='custom', base_url='https://relay.example/v1',
|
||||
'openai/gpt-5.4', provider='custom', base_url='https://relay.example/v1',
|
||||
)
|
||||
finally:
|
||||
config._available_models_cache = old_cache
|
||||
config._advertised_model_ids_memo = old_memo
|
||||
config._available_models_cache_source_fingerprint = old_fp
|
||||
config._models_cache_provenance = old_prov
|
||||
assert model == 'zai-org/GLM-5.1', (
|
||||
f"foreign-profile catalog must be ignored (legacy fallback preserves), got {model!r}"
|
||||
assert model == 'openai/gpt-5.4', (
|
||||
f"foreign-profile catalog must be ignored (cold-preserve default), got {model!r}"
|
||||
)
|
||||
|
||||
|
||||
@@ -484,6 +490,132 @@ def test_resolver_provenance_read_does_not_block_on_cache_lock_5979():
|
||||
config.invalidate_models_cache()
|
||||
|
||||
|
||||
def test_b3nw_cold_nondeclared_custom_slug_preserves_full_id_5979():
|
||||
"""#5979 (reopened, b3nw's exact case): a NON-declared model actively
|
||||
selected on a ``custom:<slug>`` proxy is preserved on a COLD catalog.
|
||||
|
||||
b3nw's config: ``provider: custom:llm-proxy``, ``model.default: x-ai/grok-4.5``,
|
||||
NO ``models[]`` allowlist. He picks ``x-ai/grok-composer-2.5-fast`` in the UI
|
||||
(not his default). On a cold send the old code stripped it to
|
||||
``grok-composer-2.5-fast`` via the legacy family heuristic (the model had
|
||||
graduated into the x-ai first-party catalog), and his proxy 400'd. The flip
|
||||
to cold-preserve fixes it. grok-composer-2.5-fast is stubbed into
|
||||
``_PROVIDER_MODELS['x-ai']`` (as test_custom_remote_preserves_advertised_full_id_5979
|
||||
does) so the first-party trigger is reproduced deterministically regardless
|
||||
of the agent catalog version.
|
||||
"""
|
||||
mid = 'x-ai/grok-composer-2.5-fast'
|
||||
bare = 'grok-composer-2.5-fast'
|
||||
xai = list(config._PROVIDER_MODELS.get('x-ai') or [])
|
||||
had = any(isinstance(m, dict) and m.get('id') == bare for m in xai)
|
||||
old_xai = config._PROVIDER_MODELS.get('x-ai')
|
||||
old_cfg = dict(config.cfg)
|
||||
old_prov = config._models_cache_provenance
|
||||
old_cache = config._available_models_cache
|
||||
if not had:
|
||||
config._PROVIDER_MODELS['x-ai'] = xai + [{'id': bare, 'label': 'Grok Composer 2.5 Fast'}]
|
||||
config.cfg.clear()
|
||||
config.cfg.update({
|
||||
'model': {'default': 'x-ai/grok-4.5', 'provider': 'custom:llm-proxy',
|
||||
'base_url': 'https://proxy.example/v1'},
|
||||
'custom_providers': [{'name': 'llm-proxy', 'base_url': 'https://proxy.example/v1',
|
||||
'key_env': 'LLM_PROXY_API_KEY'}],
|
||||
})
|
||||
config._available_models_cache = None # COLD
|
||||
config._advertised_model_ids_memo = None
|
||||
config._sync_models_cache_provenance()
|
||||
try:
|
||||
assert config._is_first_party_model('x-ai', bare), "precondition: stub failed"
|
||||
model, provider, _ = config.resolve_model_provider(mid)
|
||||
finally:
|
||||
if not had:
|
||||
if old_xai is None:
|
||||
config._PROVIDER_MODELS.pop('x-ai', None)
|
||||
else:
|
||||
config._PROVIDER_MODELS['x-ai'] = old_xai
|
||||
config.cfg.clear()
|
||||
config.cfg.update(old_cfg)
|
||||
config._models_cache_provenance = old_prov
|
||||
config._available_models_cache = old_cache
|
||||
assert model == mid, f"b3nw's non-declared cold custom-proxy pick must preserve, got {model!r}"
|
||||
assert provider == 'custom:llm-proxy'
|
||||
|
||||
|
||||
def test_warm_models_catalog_provenance_if_cold_publishes_from_disk_5979():
|
||||
"""The send-path warm helper publishes provenance from a valid disk cache
|
||||
when memory is cold — restoring the endpoint-advertised signal so #433
|
||||
strips and #5979 preserves — WITHOUT a live rebuild.
|
||||
"""
|
||||
old_cfg = dict(config.cfg)
|
||||
config.cfg.clear()
|
||||
config.cfg.update({
|
||||
'model': {'default': 'x-ai/grok-4.5', 'provider': 'custom:llm-proxy',
|
||||
'base_url': 'https://proxy.example/v1'},
|
||||
'custom_providers': [{'name': 'llm-proxy', 'base_url': 'https://proxy.example/v1',
|
||||
'key_env': 'LLM_PROXY_API_KEY',
|
||||
'models': ['x-ai/grok-4.5', 'x-ai/grok-composer-2.5-fast']}],
|
||||
})
|
||||
try:
|
||||
# Build + persist to disk, then simulate a cold memory cache (restart).
|
||||
config.invalidate_models_cache()
|
||||
config.get_available_models() # publishes to memory + disk
|
||||
assert config._models_cache_provenance is not None
|
||||
# Simulate cold memory but valid disk cache (do NOT delete disk).
|
||||
config._available_models_cache = None
|
||||
config._advertised_model_ids_memo = None
|
||||
config._sync_models_cache_provenance()
|
||||
assert config._models_cache_provenance is None, "precondition: memory cold"
|
||||
# The warm helper should republish provenance from disk (network-free).
|
||||
config.warm_models_catalog_provenance_if_cold()
|
||||
assert config._models_cache_provenance is not None, (
|
||||
"warm helper must publish provenance from the disk cache"
|
||||
)
|
||||
adv = config._endpoint_advertised_model_ids('custom:llm-proxy')
|
||||
assert adv and 'x-ai/grok-composer-2.5-fast' in adv, (
|
||||
f"warmed provenance must carry the endpoint-advertised ids, got {adv!r}"
|
||||
)
|
||||
finally:
|
||||
config.cfg.clear()
|
||||
config.cfg.update(old_cfg)
|
||||
config.invalidate_models_cache()
|
||||
|
||||
|
||||
def test_warm_models_catalog_provenance_noop_when_already_warm_5979():
|
||||
"""The warm helper is a cheap no-op (single global read) when provenance is
|
||||
already published — it must not rebuild or mutate a warm catalog.
|
||||
"""
|
||||
sentinel = ({'groups': []}, {'config_yaml': {'path': '/x'}})
|
||||
old_prov = config._models_cache_provenance
|
||||
config._models_cache_provenance = sentinel
|
||||
try:
|
||||
config.warm_models_catalog_provenance_if_cold()
|
||||
assert config._models_cache_provenance is sentinel, (
|
||||
"warm helper must not touch an already-warm provenance tuple"
|
||||
)
|
||||
finally:
|
||||
config._models_cache_provenance = old_prov
|
||||
|
||||
|
||||
def test_warm_models_catalog_provenance_never_live_rebuilds_5979(monkeypatch):
|
||||
"""The warm helper must call get_available_models with prefer_cache=True so
|
||||
it can never trigger a live provider probe on the send path.
|
||||
"""
|
||||
old_prov = config._models_cache_provenance
|
||||
config._models_cache_provenance = None
|
||||
captured = {}
|
||||
def _fake_get_available_models(*, prefer_cache=False, force_refresh=False):
|
||||
captured['prefer_cache'] = prefer_cache
|
||||
captured['force_refresh'] = force_refresh
|
||||
return {'groups': []}
|
||||
monkeypatch.setattr(config, 'get_available_models', _fake_get_available_models)
|
||||
try:
|
||||
config.warm_models_catalog_provenance_if_cold()
|
||||
finally:
|
||||
config._models_cache_provenance = old_prov
|
||||
assert captured.get('prefer_cache') is True, "warm must use prefer_cache=True (no live rebuild)"
|
||||
assert captured.get('force_refresh') is False, "warm must never force_refresh"
|
||||
|
||||
|
||||
def test_custom_remote_preserves_unknown_prefix_548():
|
||||
"""#548: an unknown vendor prefix (zai-org/GLM-5.1) is always preserved.
|
||||
|
||||
|
||||
Reference in new issue
Block a user