Skip to content

Invalid configured model path silently falls back to the cache #149

Closed

Description

@chiruu12

ModelStore.resolve_spec_path (sdk/src/unplug/ml/store.py:310) drops an explicitly configured spec.path when it fails validation and substitutes whatever is in the local cache. No warning, no error, and ml_degraded stays False.

Repro on any machine that has run unplug-models download tiny once:

import tempfile, pathlib, os
os.environ.pop("UNPLUG_MODEL_PATH", None)
from unplug.config import GuardConfig
from unplug.core.runtime.model_runtime import merge_catalog_models
from unplug import Guard

d = pathlib.Path(tempfile.mkdtemp()) / "corrupt"
d.mkdir()
(d / "config.json").write_text("not-json")

cfg = merge_catalog_models(
    GuardConfig(active_model="tiny", require_ml=False, auto_download_model=False)
)
m = dict(cfg.models)
m["tiny"] = m["tiny"].model_copy(update={"path": str(d)})
g = Guard(config=cfg.model_copy(update={"models": m}))

print(g.ml_model_loaded)   # True, expected False
print(g.ml_degraded)       # False

It loads ~/.cache/unplug/models/tiny instead. The code path:

if spec.path and self.is_valid_checkpoint(Path(spec.path)):
    return spec

cached = self.resolve_local_path(tier or spec.name)
if cached is not None:
    return spec.model_copy(update={"path": str(cached)})

The cache lookup is a fallback for when path is unset. It also fires when path is set and invalid.

Note the asymmetry with the branch twenty lines above: an invalid UNPLUG_MODEL_PATH logs "is set but is not a valid checkpoint ... ignoring" before falling through. An invalid configured path gets nothing.

require_ml=True does not help. The fallback loads, so require_ml is satisfied and no ModelError is raised.

Why it matters: an operator pins a vetted checkpoint and turns downloads off. A path typo or a bad mount gets them a different set of weights, with ml_model_loaded=True and ml_degraded=False. Nothing in the result says the loaded model is not the pinned one.

Two existing tests already assert the correct behaviour and fail on any machine with a populated cache:

tests/unit/ml/test_model_store.py::test_guard_survives_corrupt_checkpoint
tests/unit/ml/test_model_store.py::test_guard_require_ml_corrupt_checkpoint_raises_model_error

They pass in CI only because CI has no model cache, which is also why this went unnoticed.

Fix: when spec.path is set but invalid, mirror the env branch. Log the warning and do not substitute the cached checkpoint. Raise ModelError when require_ml=True.

Related: #107, since this is a case where degraded should be set and is not.

Activity

  1. chiruu12 commented on Aug 29, 2026

    @chiruu12
    CollaboratorAuthor

    Repro, and it is worse than the title says.

    import tempfile, os
    from pathlib import Path
    os.environ.pop('UNPLUG_MODEL_PATH', None)
    from unplug.config.guard import GuardConfig
    from unplug.core.runtime.model_runtime import merge_catalog_models, prepare_active_model_spec
    
    d = Path(tempfile.mkdtemp()) / 'corrupt'
    d.mkdir()
    (d / 'config.json').write_text('not-json')
    
    cfg = merge_catalog_models(GuardConfig(active_model='tiny', require_ml=False, auto_download_model=False))
    m = dict(cfg.models)
    m['tiny'] = m['tiny'].model_copy(update={'path': str(d)})
    spec = prepare_active_model_spec(cfg.model_copy(update={'models': m}))
    print(spec.path)

    Prints the cache path, not the configured one:

    configured: /var/folders/.../tmpd80jqqhh/corrupt
    resolved  : ~/.cache/unplug/models/tiny/checkpoint
    

    sdk/src/unplug/ml/store.py:310-315. When the configured path fails is_valid_checkpoint, the function falls through to resolve_local_path(tier) and returns whatever the cache holds. Nothing is logged.

    require_ml=True does not protect against this. The substituted model satisfies it, so the flag currently means "refuse to run without a model" rather than "refuse to run without the model I configured". Someone who audits a checkpoint, pins its path, and later has that path go missing gets a different model scanning their traffic and guard.ml_model_loaded reporting True.

    UNPLUG_MODEL_PATH reaches the same substitution through store.py:304. That branch does warn, but it says the value is being ignored and never says what got used instead, which reads as "running without ML" when it is not.

    Two tests on dev already fail because of this:

    FAILED tests/unit/ml/test_model_store.py::test_guard_survives_corrupt_checkpoint
    FAILED tests/unit/ml/test_model_store.py::test_guard_require_ml_corrupt_checkpoint_raises_model_error
    2 failed, 1250 passed, 3 skipped, 30 deselected
    

    They pass in CI only because the runner has no ~/.cache/unplug/models, so there is nothing available to substitute. Filed separately as the CI blind spot.

    Fix I plan to take: an explicitly configured path that is not a valid checkpoint is an error, never a fallback. Cache resolution stays for the case where no path was configured at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions