Skip to content

[Bug]: LM Studio probe returns None when model not currently loaded — ignores max_context_length fallback #47678

Description

@nachilau

Bug Description

_query_local_context_length() in agent/model_metadata.py queries LM Studio's native /api/v1/models endpoint and reads loaded_instances[0].config.context_length, but only when the model is currently loaded. When the model isn't loaded (e.g. JIT enabled, fresh server start, model just unloaded by TTL), loaded_instances is [] and the probe returns None — even though the same response always carries max_context_length (the model's training-time max), which would be a usable fallback.

The probe then falls through to the hardcoded family default (e.g. 256K for gemma-4, 64K for unknowns). For a user who actually loads the model with a smaller context window than the family default (very common — LM Studio's loader UI defaults to 4K–8K depending on free VRAM), this lets Hermes treat the model as having far more context than its KV cache can hold. The conversation grows past the loaded window before any compression triggers, and on the next turn LM Studio either silently truncates or — under VRAM pressure — the GPU driver fails to allocate KV cache buffers and the system hangs/BSODs.

This is distinct from #47200 (remote LM Studio is is_local_endpoint()-gated), #25989 (manual preload uses MINIMUM_CONTEXT_LENGTH), and #30178 (per-model override regression). Those affect different code paths; this one is in the probe itself.

Steps to Reproduce

  1. Start LM Studio locally on port 1234 with JIT enabled. Do not load any model into memory yet.
  2. Configure Hermes with an LM Studio custom_provider but without any per-model context_length override:
    custom_providers:
      - name: LM Studio
        base_url: http://localhost:1234/v1
        api_key: lm-studio
        discover_models: false
        model: google/gemma-4-12b-qat
        models:
          - google/gemma-4-12b-qat
  3. Run a one-shot resolver call against the unloaded model:
    from agent.model_metadata import get_model_context_length
    ctx = get_model_context_length(
        model="google/gemma-4-12b-qat",
        base_url="http://localhost:1234/v1",
        config_context_length=None,
        provider="lmstudio",
        custom_providers=[...],  # as above
    )
    print(ctx)
  4. Observe Hermes' INFO log lines:
    Could not detect context length for model 'google/gemma-4-12b-qat' at http://localhost:1234/v1 — defaulting to 256,000 tokens (probe-down). Set model.context_length in config.yaml to override.
    Using hardcoded context length 256,000 for model 'google/gemma-4-12b-qat' (custom endpoint, catalog match on 'gemma-4')
    
  5. Manually inspect what LM Studio actually returns:
    curl -s http://localhost:1234/api/v1/models | jq '.models[] | select(.key=="google/gemma-4-12b-qat")'
    Output (verified locally on LM Studio 0.3.x, model unloaded):
    {
      "key": "google/gemma-4-12b-qat",
      "loaded_instances": [],
      "max_context_length": 262144,
      ...
    }

Expected Behavior

When loaded_instances is empty, the probe should fall back to max_context_length from the same response. That gives the training-time max (262,144 in this example), which is still better than ignoring a known-good number and resolving via a coarser fallback.

Optionally and even better: when loaded_instances is non-empty, prefer loaded_instances[0].config.context_length (current behaviour); when it's empty, return max_context_length. Either way, never return None if the model is listed and max_context_length is present.

Actual Behavior

_query_local_context_length returns None whenever loaded_instances is [], even though max_context_length is in the same JSON response. The resolver falls through to family-default heuristics (256K for gemma-4, etc.), which can be much larger than what the user will actually load the model with at JIT time. Hermes then treats the conversation budget as far larger than the actual loaded KV cache, and inputs sized to the wrong budget can cause LM Studio truncation or — when VRAM is already tight — drive nvlddmkm into STATUS_INSUFFICIENT_RESOURCES (VIDEO_TDR_FAILURE 0x116) on NVIDIA hardware.

Affected Component

Configuration (config.yaml, .env, hermes setup); Agent Core (conversation loop, context compression, memory)

Operating System

Windows 11 (RTX 3090, 24 GB VRAM)

Hermes Version

(version stamp will be filled in by the issue submission flow)

Root Cause Analysis

Probe code in agent/model_metadata.py at _query_local_context_length() (around lines 1337–1349 on current main):

if server_type == "lm-studio":
    resp = client.get(f"{server_url}/api/v1/models")
    if resp.status_code == 200:
        data = resp.json()
        for m in data.get("models", []):
            if _model_id_matches(m.get("key", ""), model) or _model_id_matches(m.get("id", ""), model):
                # Prefer loaded instance context (actual runtime value)
                for inst in m.get("loaded_instances", []):
                    cfg = inst.get("config", {})
                    ctx = cfg.get("context_length")
                    if ctx and isinstance(ctx, (int, float)):
                        return int(ctx)
                break    # <-- exits without checking m["max_context_length"]

Once the matching model entry is found and loaded_instances is empty, the inner for doesn't run, and break exits the outer loop. The function falls through to the OpenAI-compat /v1/models/{model} and /v1/models paths — neither of which carries context info on LM Studio — and ultimately returns None.

Proposed Fix

Inside the if server_type == "lm-studio": block, after the loaded_instances loop, fall back to m["max_context_length"] if it's present and positive, and only break (without returning) when neither is available. Roughly:

if server_type == "lm-studio":
    resp = client.get(f"{server_url}/api/v1/models")
    if resp.status_code == 200:
        data = resp.json()
        for m in data.get("models", []):
            if _model_id_matches(m.get("key", ""), model) or _model_id_matches(m.get("id", ""), model):
                # 1) Prefer the runtime ctx of a currently-loaded instance.
                for inst in m.get("loaded_instances", []):
                    cfg = inst.get("config", {})
                    ctx = cfg.get("context_length")
                    if ctx and isinstance(ctx, (int, float)):
                        return int(ctx)
                # 2) Otherwise fall back to the model's training-time max,
                #    which LM Studio always reports even when not loaded.
                max_ctx = m.get("max_context_length")
                if max_ctx and isinstance(max_ctx, (int, float)) and max_ctx > 0:
                    return int(max_ctx)
                break

Notes for reviewers:

  • This complements [Bug]: fail to detect context length from remote LMStudio instances #47200's fix (which handles the remote-instance gating) — the two together let unloaded and remote LM Studio instances resolve correctly without manual config.
  • The training-time max is an over-estimate of what JIT will actually load, so users who load with a smaller context still need a per-model context_length override under custom_providers[].models.<id> to be safe (see [Bug]: LM Studio custom_providers per-model context_length broken in 0.14.0 — regressed to 64K #30178). But returning the training max is strictly better than returning None — it bounds the budget instead of unbounding it via the family-default heuristic.
  • The cache-bypass for provider == "lmstudio" at line ~1670 (# LM Studio is excluded — its loaded context length is transient...) is correct and should remain. The fix only affects the fallback ordering inside the probe itself.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

(I have a workaround in place — explicit per-model context_length: 8192 under custom_providers[].models.<id> — but that's a config-side band-aid, not a fix.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low — cosmetic, nice to havearea/configConfig system, migrations, profilescomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointtype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions