|
| 1 | +# EA_LMStudio — agent notes |
| 2 | + |
| 3 | +A single ComfyUI custom node (`EA_LMStudio`, displayed as **EA LM Studio**) that |
| 4 | +runs text and vision generation against a local [LM Studio](https://lmstudio.ai/) |
| 5 | +server through the official `lmstudio` Python SDK. It returns three STRING |
| 6 | +outputs — `response`, `reasoning`, `troubleshooting` — and renders the response |
| 7 | +inside the node. |
| 8 | + |
| 9 | +## Current state |
| 10 | + |
| 11 | +_Last verified: 2026-08-09_ |
| 12 | + |
| 13 | +- **Status:** v2.0.0, published to the Comfy Registry from `main` on every |
| 14 | + `pyproject.toml` version change. |
| 15 | +- **Works:** model discovery + refresh (startup fetch and a live API route), |
| 16 | + text generation, multi-image VLM input, reasoning extraction (LM Studio's own |
| 17 | + split, with tag-regex fallback), structured JSON output, stop strings, |
| 18 | + context-overflow policy, speculative decoding with acceptance stats, |
| 19 | + cancellable streaming with a queue progress bar, VRAM unload of both the LLM |
| 20 | + and ComfyUI's own models, and migration of workflows saved by 1.x. |
| 21 | +- **In progress:** nothing outstanding. |
| 22 | +- **Known gaps:** LM Studio's `ttl`, tool/function calling (`.act()`), GBNF |
| 23 | + grammars, and load-time `seed` (real determinism, but it only applies when the |
| 24 | + node actually loads the model) are all supported by the SDK and not exposed. |
| 25 | + There is no automated frontend test — `web/ea_lmstudio.js` is verified by hand |
| 26 | + in a browser. |
| 27 | +- **Deep docs:** user-facing behaviour lives in `README.md`; nothing is |
| 28 | + duplicated here. |
| 29 | + |
| 30 | +## Build / test / run |
| 31 | + |
| 32 | +```bash |
| 33 | +pip install -r requirements-dev.txt # pulls requirements.txt too |
| 34 | +pytest -q # 100+ tests, no LM Studio needed |
| 35 | +``` |
| 36 | + |
| 37 | +The suite is dependency-light by design. `tests/conftest.py` stubs `lmstudio` |
| 38 | +and `comfy.model_management` **only when they are genuinely missing**, so a real |
| 39 | +install is never shadowed, and registers the repo root as a synthetic package so |
| 40 | +`LMStudio.py` (which uses relative imports) is importable without the checkout |
| 41 | +having to be named `EA_LMStudio`. |
| 42 | + |
| 43 | +CI runs the same command on Python 3.10 and 3.13 (`.github/workflows/test.yml`). |
| 44 | + |
| 45 | +To exercise the node for real, point a scratch script at a running LM Studio and |
| 46 | +stub `comfy.model_management` / `comfy.utils` — the node only needs |
| 47 | +`unload_all_models`, `soft_empty_cache`, `processing_interrupted`, |
| 48 | +`throw_exception_if_processing_interrupted`, `InterruptProcessingException` and |
| 49 | +`ProgressBar`. |
| 50 | + |
| 51 | +## Layout |
| 52 | + |
| 53 | +| File | Responsibility | |
| 54 | +|------|----------------| |
| 55 | +| `LMStudio.py` | The node: INPUT_TYPES, streaming, diagnostics, reasoning split | |
| 56 | +| `lms_params.py` | Pure widget-string → SDK-value helpers (stop strings, schema, fences) | |
| 57 | +| `lms_reasoning.py` | Tag/harmony reasoning regexes (fallback path) | |
| 58 | +| `lms_image.py` | ComfyUI IMAGE tensor → JPEG-safe PIL | |
| 59 | +| `model_fetcher.py` | `/v1/models` discovery, validation, cache | |
| 60 | +| `lms_config/` | `default_config.json` + gitignored `user_config.json` | |
| 61 | +| `web/ea_lmstudio.js` | Refresh toggle, in-node preview, 1.x workflow migration | |
| 62 | +| `example_workflows/` | Shipped examples, loadable by drag-and-drop | |
| 63 | + |
| 64 | +Everything except `LMStudio.py` is deliberately free of the `lmstudio` SDK and |
| 65 | +`comfy` imports so it stays unit-testable. |
| 66 | + |
| 67 | +## Things that will bite you |
| 68 | + |
| 69 | +**The SDK silently discards unknown prediction-config keys.** `LlmPredictionConfig` |
| 70 | +is built with msgspec and drops anything not in `LlmPredictionConfigDict` rather |
| 71 | +than raising, so a wrong or wished-for key becomes a no-op that looks like it |
| 72 | +worked. v1.x shipped `presencePenalty` and `enableThinking` this way for |
| 73 | +releases. Before adding a parameter, check it exists in `LlmPredictionConfigDict` |
| 74 | +in the installed SDK, then confirm it round-trips in |
| 75 | +`PredictionResult.prediction_config`. `generate()` performs that diff on every |
| 76 | +run and warns — do not remove it. |
| 77 | + |
| 78 | +**LM Studio has no presence penalty, no frequency penalty, no inference-time |
| 79 | +seed, and no thinking on/off flag.** `seed` exists only in the *load* config. |
| 80 | +The `seed` widget is a ComfyUI cache-buster and nothing more. |
| 81 | + |
| 82 | +**Removing or reordering a widget corrupts saved workflows.** ComfyUI serialises |
| 83 | +`widgets_values` positionally, so a removal or a regroup shifts every later |
| 84 | +value. The migration in `web/ea_lmstudio.js` keys the old array by the v1.5.x |
| 85 | +widget-name order (two variants, with and without the `control_after_generate` |
| 86 | +widget ComfyUI inserts after an INT named `seed`) and writes values onto current |
| 87 | +widgets **by name**, which is why v2.0.0 could both drop two widgets and regroup |
| 88 | +the rest. Any future removal or reorder needs the same treatment, and the legacy |
| 89 | +order table must be kept. |
| 90 | + |
| 91 | +**A stored node size is restored verbatim and is not re-checked against the |
| 92 | +widgets.** Adding a widget therefore leaves every previously saved workflow too |
| 93 | +short, and the overflow draws outside the node frame. `growToFitWidgets` in the |
| 94 | +frontend extension grows (never shrinks) the node on configure and after |
| 95 | +execution. Note that a stock ComfyUI `Note` node's textarea overhangs its own |
| 96 | +frame by ~13 units at any size — that is upstream behaviour, not a symptom of |
| 97 | +this, so don't chase it. |
| 98 | + |
| 99 | +**A streamed prediction must be drained, not broken out of.** Breaking the `for` |
| 100 | +loop closes the generator and `stream.result()` then raises `GeneratorExit`. |
| 101 | +Call `stream.cancel()` and keep iterating; it ends promptly with |
| 102 | +`stop_reason == "userStopped"`. |
| 103 | + |
| 104 | +**Every `LlmPredictionStats` field except `stop_reason` is Optional.** Formatting |
| 105 | +one with `:.2f` without a None check raised `TypeError` *after* a successful |
| 106 | +generation, which the outer handler then reported as a failure — throwing away |
| 107 | +text the model had already produced. |
| 108 | + |
| 109 | +**`{"type": "json"}` without a schema does not constrain decoding.** Models |
| 110 | +routinely answer with a ```` ```json ```` fence. Only `jsonSchema` constrains the |
| 111 | +sampler. |
| 112 | + |
| 113 | +## Conventions |
| 114 | + |
| 115 | +- Registry publishing is driven by the `version` in `pyproject.toml`; the |
| 116 | + workflow compares it against `HEAD^` and skips when unchanged. Bump it in the |
| 117 | + same commit as any change worth shipping. |
| 118 | +- `lms_config/user_config.json` and `worklogs/` are gitignored. Never commit |
| 119 | + either, and never put a server address or token in a tracked file. |
| 120 | +- Keep the `CUSTOM_MODEL_OPTION` literal in `web/ea_lmstudio.js` in sync with |
| 121 | + `model_fetcher.py`, which is the source of truth. |
0 commit comments