Files
darth-veitcher-comfydv/src/js/ollama.js
T
James VeitchandClaude Sonnet 4.6 4c64e2b6da feat(chat): I-commit — model→STRING, OLLAMA_HOST docker support, dynamic test fixtures
- OllamaChatCompletion.model changed from COMBO to plain STRING so it can
  receive a wired value from OllamaLoadModel without a separate model_name
  socket (removes dual-input UX confusion and COMBO validation errors)
- Removed model_name optional input — model STRING is the single entry point
- _load_default_models() tries OLLAMA_HOST env var → host.docker.internal →
  localhost so COMBO nodes pre-populate on server start inside Docker
- docker-compose.dev.yml: OLLAMA_HOST=http://host.docker.internal:11434 so
  model dropdowns are populated at startup without a manual Refresh click
- JS: refreshModelWidget handles both COMBO and STRING widget types; keeps
  user-typed/wired value when it is already a known model
- Tests: first_generative_model fixture dynamically selects the first
  installed model instead of hardcoding embeddinggemma:latest (which is an
  embedding-only model that rejects /api/generate with HTTP 400)
- Tests: test_multi_turn_receives_context passes think=False to prevent
  Qwen3 thinking-mode models from returning empty content fields
- Screenshots retaken: ollama_chat.png, ollama_lifecycle.png, ollama_workflow.png

All 80 unit + 9 integration tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWysE6jYn4YjNorLBQ1cXb
2026-06-29 12:49:53 +01:00

109 lines
3.9 KiB
JavaScript

/**
* ollama.js — ComfyUI frontend extension for comfydv Ollama nodes.
*
* Populates the model widget on Ollama nodes from a live call to
* GET /dv/ollama/models?host=<url>.
*
* OllamaModelSelector and OllamaLoadModel use a COMBO widget (dropdown).
* OllamaChatCompletion uses a plain STRING widget (accepts wired values).
* The Refresh button works the same way for both: it fetches the live list
* and sets the widget value / updates COMBO options as appropriate.
*/
import { app } from "../../scripts/app.js";
/** Nodes whose model widget is a COMBO dropdown. */
const OLLAMA_COMBO_NODES = new Set(["OllamaModelSelector", "OllamaLoadModel"]);
/** Nodes whose model widget is a plain STRING (accepts wired input). */
const OLLAMA_STRING_MODEL_NODES = new Set(["OllamaChatCompletion"]);
const OLLAMA_ALL_NODES = new Set([...OLLAMA_COMBO_NODES, ...OLLAMA_STRING_MODEL_NODES]);
/**
* Fetch model list and update the node's model widget.
* Works for both COMBO and STRING widgets:
* - COMBO: updates options.values + preserves selection if model still exists.
* - STRING: sets the value to the first model; keeps existing value if it
* still appears in the live list (user may have typed a valid name).
*/
async function refreshModelWidget(node, host) {
try {
const resp = await fetch(`/dv/ollama/models?host=${encodeURIComponent(host)}`);
if (!resp.ok) return;
const data = await resp.json();
const models = data.models ?? [];
if (!models.length) return;
const modelWidget = node.widgets?.find(w => w.name === "model");
if (!modelWidget) return;
const current = (modelWidget.value ?? "").trim();
if (Array.isArray(modelWidget.options?.values)) {
// COMBO widget
modelWidget.options.values = models;
modelWidget.value = models.includes(current) ? current : models[0];
} else {
// STRING widget — keep current value if it is a known model
if (!current || !models.includes(current)) {
modelWidget.value = models[0];
}
}
node.setDirtyCanvas(true, false);
} catch (_) {
// Ollama unreachable — leave widget unchanged
}
}
/**
* Locate the host string for a node.
*
* First checks the node's own widgets (OllamaClient has a "host" widget).
* Otherwise traverses graph links to find a connected OllamaClient node and
* reads its "host" widget — this is the common case for downstream nodes.
*/
function getHostFromNode(node) {
const ownHostWidget = node.widgets?.find(w => w.name === "host");
if (ownHostWidget) return ownHostWidget.value;
for (const input of node.inputs ?? []) {
if (!input.link) continue;
const link = node.graph?.links[input.link];
if (!link) continue;
const sourceNode = node.graph?.getNodeById(link.origin_id);
if (sourceNode?.type === "OllamaClient") {
const hostWidget = sourceNode.widgets?.find(w => w.name === "host");
if (hostWidget?.value) return hostWidget.value;
}
}
return "http://localhost:11434";
}
app.registerExtension({
name: "comfydv.ollama",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!OLLAMA_ALL_NODES.has(nodeData.name)) return;
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// Add a Refresh button below the model widget
this.addWidget("button", "⟳ Refresh models", null, () => {
const host = getHostFromNode(this);
refreshModelWidget(this, host);
});
// Initial population on node creation
const host = getHostFromNode(this);
refreshModelWidget(this, host);
return result;
};
},
});