Commit Graph
110 Commits
Author SHA1 Message Date
James VeitchandClaude Sonnet 5 09fe013ff9 fix(tests): update Dockerfile test to expect python:3.13-slim
docker/Dockerfile was bumped from 3.11 to 3.13 in 50076d7 without
updating the assertion, breaking the suite on main. Verified 3.13
compatibility by building the image and booting ComfyUI 0.27.0 in it
(custom node loads, DB migrations run, no errors) before updating the
pinned version the test enforces.

Fixes #20

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YArD9ZjBWKsAvazmS48amA
2026-07-22 20:20:33 +01:00
James Veitch 87ec79be1b Merge pull request #23 from darth-veitcher/fix/retry-blank-output-with-seed-and-backoff
fix: retry blank/failed LLM responses with a new seed and backoff
2026-07-20 09:03:16 +01:00
James VeitchandClaude Sonnet 5 1dcef77a2b fix: keep chat_structured retry's nested seed in sync with the top-level one
beacon-reviewer caught a real gap: when a caller pins options["seed"],
the retry loop set the new seed on ModelSettings' top-level "seed"
field but left the same request's extra_body.options.seed (the
Ollama-native passthrough) at the original pinned value. A backend
that honors the nested field over the top-level OpenAI one would keep
sending the identical effective seed on every retry, silently
defeating this fix for exactly the pinned-seed case it needs to cover.

Now both fields are kept in sync on every retry attempt, built via
fresh copies rather than mutating the caller's options dict in place
(that dict is shared across every attempt, and possibly across other
calls). Added a regression test asserting the caller's options dict is
untouched after a multi-attempt retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-20 08:59:06 +01:00
James VeitchandClaude Sonnet 5 09a173e552 fix: retry blank/failed LLM responses with a new seed and backoff
Reported live on a fresh runpod: structured_output extraction on
ChatCompletion would fail with

  RuntimeError: chat_structured: response failed validation against
  schema after 3 attempt(s) ... Last error: Exceeded maximum output
  retries (0)

...but succeed if a plain (non-structured) chat call with the same
prompt was run first. Root cause: pydantic-ai's Agent is built with
retries=0 (comfydv drives its own outer retry loop, not pydantic-ai's
internal one), so a single bad/empty tool-call response instantly
exhausts one attempt. comfydv's own retry loop then retried with the
*exact same request* every time — if the model is genuinely stuck
(e.g. still warming up on a freshly-started backend) or produces a
degenerate response for a given seed, every attempt fails identically.
Confirmed the seed was unset in the reporting user's workflow (Ollama
picks its own per call) and it still failed 3/3 in a row, which rules
out "stuck on one bad seed" and points at request-level determinism
plus zero backoff between attempts as the real gap.

Plain chat() had zero retry/validation logic at all before this change
— it silently returned whatever content came back, even blank. Given
the underlying failure mode (a fresh model's first response sometimes
being blank) is identical for both chat modes, this fix applies to
both, not just structured output.

Adds src/comfydv/_llm/retry.py: next_seed() (deterministic, starting
from any user-pinned options["seed"], incrementing per retry — attempt
1 is always the caller's original, untouched request) and
RETRY_BACKOFF_SECS (a flat delay between retries, giving a still-
loading model time to finish rather than hammering it with identical
requests back-to-back).

Both OllamaProvider.chat() and LlamaCppProvider.chat() now retry (per
the node's existing max_retries widget) when a response comes back
blank, injecting the incremented seed each retry — Ollama via the
native options.seed field, llama.cpp via the OpenAI spec's top-level
"seed" field (nesting it under "options" like the rest of the
Ollama-native passthrough would silently not work — llama-server's
OpenAI-compatible endpoint doesn't read seed from there, a pre-existing,
documented non-goal for general options passthrough that doesn't apply
to this internal retry mechanism). Neither raises if every retry stays
blank — matches chat()'s existing never-validates contract.

chat_structured() in _llm/chat.py now injects the same incrementing
seed via pydantic-ai's ModelSettings["seed"] (which correctly maps to
the OpenAI API's top-level seed param for both backends) and adds the
same backoff between attempts, on top of its existing validation-retry
loop.

Live-verified against real Ollama: the happy path (model responds
normally first try) still makes exactly one network call for chat()
and completes chat_structured() normally — no regression from the new
retry scaffolding. The specific cold/stuck-model failure itself is
runpod-cold-start-dependent and wasn't independently reproducible in
this session's environment (a second live attempt succeeded on the
very first try) — unit tests directly exercise the retry/seed/backoff
logic instead (test_llm_retry.py, and new cases in
test_ollama_provider.py, test_llamacpp_provider.py, and
test_llm_chat_structured.py), verified against both providers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-20 08:49:35 +01:00
James Veitch 92da4fc117 Merge pull request #22 from darth-veitcher/fix/run-async-real-comfyui-event-loop
fix: chat_structured() crashes in real ComfyUI's async execution engine
2026-07-12 10:33:50 +01:00
James VeitchandClaude Sonnet 5 1fe3bef663 docs(tests): stop overclaiming what the _run_async regression tests prove
beacon-reviewer caught it: reconstructing the old buggy _run_async and
running it against test_run_async_works_when_called_from_within_a_running_loop
and test_run_async_propagates_exceptions_from_within_a_running_loop shows
both pass against the old code too. asyncio.get_running_loop() never
spuriously raises under plain CPython/pytest, so the old conditional also
takes the safe worker-thread branch there — the actual reported bug is not
reproducible outside real ComfyUI's execution engine, and only the live
end-to-end run against it proved the fix.

Corrects the block comment to say what the tests actually do: lock in the
documented contract so a regression to a no-thread asyncio.run(coro) call
is caught immediately, without claiming they reproduce the specific
environment-dependent failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-12 09:39:55 +01:00
James VeitchandClaude Sonnet 5 3bd857f0ab fix: chat_structured() crashes with real asyncio.run() error in live ComfyUI
Found by running a genuine end-to-end test — real docker-compose
ComfyUI harness, real Ollama, real llama-server (router mode),
actual workflow graphs submitted via POST /prompt and polled via
/history, not direct Python calls to provider classes. Every
structured_output=True ChatCompletion call failed with:

  RuntimeError: asyncio.run() cannot be called from a running event loop

Root cause: _run_async() tried asyncio.get_running_loop() first and
only spun up an isolated worker thread if that succeeded; otherwise
it called asyncio.run(coro) directly on the current thread. Under
pytest or a standalone script, get_running_loop() never spuriously
raises, so this worked and the worker-thread path was rarely
exercised for real. Under ComfyUI's actual async execution engine
(Python 3.13, node functions invoked synchronously from inside an
already-running event loop), get_running_loop() sometimes raised
anyway — routing straight into asyncio.run(coro) on the one thread
guaranteed to already have a loop running, reproducing the crash
every time. Confirmed via a live-instrumented debug run against the
real container: the coroutine executed correctly whenever the
worker-thread path was taken (get_running_loop() succeeding), and
crashed only via the direct-call fallback.

Fixed by removing the conditional entirely: always run the coroutine
in a freshly spawned worker thread. A new thread never has an
ambient loop, so asyncio.run() is safe there unconditionally,
regardless of what the calling thread's loop state actually is.

Also traced down a second symptom from the same end-to-end run:
non-structured chat() calls sometimes returned an empty response
with no error. Verified via direct curl calls to Ollama and
llama-server (bypassing comfydv entirely) that this was a real,
transient GPU/Metal memory-pressure issue on the test machine
(`ggml_metal_synchronize: error: Insufficient Memory`, Ollama's own
backend left in a wedged state returning HTTP 200 with an empty
zero-value body) — not a comfydv bug. Restarting the backend
processes resolved it; no code change was needed for that part.

Added tests/test_ollama_provider.py coverage for the specific shape
that broke: _run_async called synchronously from within code that is
itself already executing inside asyncio.run() — the actual pattern
ComfyUI's execution engine uses, which no prior test exercised (every
existing call site invoked _run_async from a plain synchronous pytest
function with no ambient loop).

Re-verified end-to-end after the fix: 5/5 real workflows passing
through actual ComfyUI execution — Ollama basic chat, Ollama full
lifecycle (Load->Chat->Unload), Ollama structured output, llama.cpp
basic chat, llama.cpp structured output — all with real generated
content, not mocks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-12 09:36:21 +01:00
James Veitch 834ae3e9e3 Merge pull request #18 from darth-veitcher/chore/finish-llm-provider-abstraction-epic
chore: finish llm-provider-abstraction epic
2026-07-11 21:55:08 +01:00
copilot-swe-agent[bot] 2df9e87838 chore: resolve merge conflicts with main 2026-07-11 20:47:06 +00:00
James Veitch 27b26ec7a4 Merge pull request #21 from darth-veitcher/fix/comfyui-relative-import-loading 2026-07-11 18:09:56 +01:00
James VeitchandClaude Sonnet 5 16b7e944f0 fix(tests): make the import-compat proof order-independent
beacon-reviewer caught a real gap: tests/test_comfyui_import_compat.py
ERRORed (not passed) when run in isolation or under a file-sharded
runner — the autouse _clear_ollama_caches fixture from conftest.py
does `from comfydv._llm.ollama_provider import ...`, and this file
was the one case where nothing had already bound `comfydv` in
sys.modules correctly first, so it resolved to the wrong package.
Only the full single-process suite happened to be green.

Fixed by shadowing that fixture with a local no-op override scoped
to this module: these tests exercise package import resolution
itself, not OllamaProvider/ChatCompletion state, so there is nothing
for the original fixture to reset here — and depending on it made
the test order-dependent on an unrelated file's import order, which
is exactly the kind of fragility this file exists to guard against.
Verified: passes standalone (`pytest tests/test_comfyui_import_compat.py`)
and as part of the full suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 18:02:57 +01:00
James VeitchandClaude Sonnet 5 36e2cd123c docs: rebuild README around live screenshots, fix stale doc tooling
Full README/docs/index.md rewrite from a product/UX-first structure
(what-is-this, why, then a deep-dive per node group with screenshots)
instead of an incremental changelog-style read.

All screenshots regenerated from a real, running ComfyUI instance —
not reused. This surfaced and fixed two more bugs in
scripts/take_screenshots.py, the project's own screenshot-automation
tool, which nobody had run since before the ADR-007 rename (the old
committed screenshots still showed pre-rename node names, e.g.
"Ollama Chat Completion" instead of "Chat Completion"):

- Every LiteGraph.createNode() call used the pre-rename node type
  strings (OllamaChatCompletion/OllamaLoadModel/OllamaUnloadModel);
  fixed to the current names.
- The canvas locator ("canvas#graph-canvas, canvas") silently
  grabbed a 250x200 minimap canvas ComfyUI's frontend added since
  this script was last verified, instead of the real graph — every
  screenshot failed with a clipping error. Fixed to target
  #graph-canvas explicitly.

Two new scenes added, addressing the actual gap: existing
screenshots showed connectivity and options, never the two things
this whole epic sequence was about — structured output (the live
dynamic output sockets appearing as output_schema is edited, via the
real js/ollama.js widget callbacks the previous commit fixed) and
llama.cpp (LlamaCppClient wired into the same generic ChatCompletion
node Ollama uses — the actual point of the adapter pattern).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 17:48:37 +01:00
James VeitchandClaude Sonnet 5 1e9a8a3b6a fix: comfydv failed to load in real ComfyUI since PR #17 (critical)
Every internal `from comfydv._llm.X import Y`-style absolute
self-import broke the entire plugin the moment ComfyUI actually
loaded it — every node, not just the LLM ones, since the whole
src/comfydv/__init__.py chain aborted on the first such import.

ComfyUI's custom_nodes loader imports this plugin via a *relative*
chain (repo-root __init__.py does `from .src.comfydv import ...`),
nesting comfydv under whatever top-level name the folder gets —
never `comfydv` itself. An absolute `from comfydv...` self-import
only resolves if `src/` has separately been placed on sys.path,
which conftest.py does for every test — masking this completely.
No test ever exercised the real loading shape.

Confirmed via git bisection against the actual docker-compose dev
harness (built and ran real ComfyUI): this predates spec 008
entirely — checking out the commit right after PR #17 merged,
before llamacpp.py existed, reproduces the identical failure at
ollama.py's own absolute import. Fixed by converting every internal
self-import across src/comfydv/ to a relative import, which resolves
correctly under both loading shapes. Verified fixed by rebuilding
the harness and confirming all 21 nodes register via /object_info.

Added tests/test_comfyui_import_compat.py: a subprocess-based test
reproducing ComfyUI's exact nested-relative-import shape (not
conftest.py's sys.path-patched shape), plus a static AST guard
against any future absolute self-import creeping back in.

Also fixed, found via the same live-harness investigation:
- src/js/ollama.js matched on pre-rename node names
  (OllamaModelSelector/OllamaLoadModel/OllamaChatCompletion), so the
  "Refresh models" button and live structured-output socket preview
  were silently absent from every node in the real UI.
- The /dv/ollama/models route always spoke Ollama's wire protocol
  regardless of which backend was actually connected — pointing it
  at a llama.cpp host could never populate real models. Route now
  takes a `backend` param; getHostAndBackendFromNode() in ollama.js
  determines it from the connected client node's registered type;
  LlamaCppProvider gained a matching _fetch_models() name-only view
  (mirrors OllamaProvider's, same graceful-degradation contract).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 17:48:20 +01:00
James Veitch d59a85dba6 Merge pull request #19 from darth-veitcher/008-llamacpp-integration
feat: llama.cpp support via LLMProvider adapter (issue #15)
2026-07-11 16:48:09 +01:00
James VeitchandClaude Sonnet 5 15bb25efa8 test(llamacpp): live-verify chat_structured(), strengthen idempotency tests
Ran a second live smoke test — load_model() then chat_structured()
with a real pydantic.BaseModel schema — against the same router-mode
llama-server used for the earlier fix. Confirmed pydantic-ai's
Agent/OpenAIProvider mechanism works against llama-server's
OpenAI-compatible endpoint, not just Ollama's (the only one verified
live in the prerequisite epic). No gap found; every LlamaCppProvider
method has now been exercised against a real server, not just mocks.

Also strengthened the two idempotency regression tests added in the
previous commit: they previously asserted only "does not raise", which
would also pass if the method silently no-op'd for an unrelated bug.
Now assert the mocked _post_json was actually called with the expected
request, so the test verifies real behavior, not just absence of a
crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 16:24:52 +01:00
James VeitchandClaude Sonnet 5 282198a7df fix(llamacpp): make load_model/unload_model actually idempotent
Ran the live smoke test (T017) that had been wrongly marked deferred
for "no llama-server reachable" — llama-server was installed via
Homebrew the whole time and a router-mode server was launched against
a real local GGUF model to verify.

This caught a real gap the mocked suite couldn't: router mode's
/models/load and /models/unload are NOT idempotent at the wire level.
Calling load on an already-loaded model returns HTTP 400 "model is
already running" (and unload/"model is not running" symmetrically),
not the {"success": true} the conformance contract assumed without
live verification. LlamaCppProvider now absorbs exactly those two
error messages as the desired end-state already reached; any other
error still propagates. Contract doc corrected to describe the real
behavior; regression tests added (mocked, run in CI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 16:10:51 +01:00
James VeitchandClaude Sonnet 5 ac248e1e99 chore(beacon): regenerate ROADMAP.md rollup
Follow-up to the llamacpp-integration epic status edit — beacon epic
refresh regenerates this file automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:49:27 +01:00
James VeitchandClaude Sonnet 5 ea66ceee9d docs(beacon): flip llamacpp-integration epic Planning->Active
Spec 008-llamacpp-integration is complete; epic-finish bookkeeping
(archive) follows in a separate chore PR once this merges, matching
the llm-provider-abstraction epic's pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:48:03 +01:00
James VeitchandClaude Sonnet 5 cf3f7fc174 fix(llamacpp): surface a clear error for non-router-mode servers (FR-006)
LlamaCppProvider.list_models() caught every exception and returned [],
indistinguishable from "no models installed". Fixed _get_json to raise
on HTTP error status (matching _post_json's existing behavior — its
docstring already claimed this), and list_models() to distinguish
OSError (genuinely unreachable, degrades to []) from RuntimeError
(server responded with an error — surfaced with a router-mode hint).

Found by beacon-reviewer ahead of PR open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:43:12 +01:00
James VeitchandClaude Sonnet 5 5af502d07e feat(llamacpp): add LlamaCppProvider + LlamaCppClient node (spec 008)
Second LLMProvider (ADR-007), backed by llama-server's router mode. Only
one new ComfyUI node — LlamaCppClient, emitting the same LLM_CLIENT socket
Ollama Client does. ChatCompletion, LLMModelSelector, LLMLoadModel,
LLMUnloadModel work unmodified once wired to it — the actual proof the
provider abstraction generalizes, not just Ollama-shaped in practice.

API shape verified live against ggml-org/llama.cpp's tools/server/README.md
(research.md) rather than assumed: model identifier field is "id" (not
"name"), status is a nested {"value": "..."} object. list_models() needs
no normalization — llama.cpp's status vocabulary is exactly ModelStatus's
full set, unlike Ollama's narrower one.

38 new tests (test_llamacpp_provider.py, test_llamacpp.py), including a
dedicated US4 test proving no generic node branches on provider type —
the same call sequence succeeds against an Ollama-shaped or llama.cpp-
shaped fake provider.

README/docs updated for the new node this time, not left stale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:27:29 +01:00
James VeitchandClaude Sonnet 5 d3ee35bfda docs(beacon): DESIGN artifacts for llama.cpp Model Integration (spec 008)
Full spec/plan/tasks/BDD scaffold for the llamacpp-integration epic, now
that its dependency (llm-provider-abstraction, PR #17) is satisfied.
Researched llama-server's router-mode API shape live (postdates training
data) rather than assuming it — two details that would have been wrong by
assumption: the model identifier field is "id" not "name" (differs from
Ollama's /api/tags), and "status" is a nested object ({"value": "..."}) not
a flat string.

Unlike the prerequisite epic, this decomposition genuinely holds as 4
independent user stories — LlamaCppClient is a new node, not a changed one,
so there's no shared "output type" migration forcing an atomic cutover.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:17:54 +01:00
James VeitchandClaude Sonnet 5 27916f4fb0 chore(beacon): regenerate ROADMAP.md gantt/table for llamacpp-integration Active
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:11:25 +01:00
James VeitchandClaude Sonnet 5 689aa2a391 chore(beacon): finish llm-provider-abstraction epic
All owned specs shipped (PR #17), so beacon epic finish moves it to
archive/ and flips status to Done. Marks llamacpp-integration Active — its
dependency is now satisfied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 15:10:57 +01:00
James Veitch 9ca65bc29a Merge pull request #17 from darth-veitcher/007-llm-provider-abstraction
LLM Provider Abstraction: generic adapter pattern for Ollama (ADR-007)
2026-07-11 15:08:14 +01:00
James VeitchandClaude Sonnet 5 c32b6a7e42 docs(beacon): note PR #17 open on the llm-provider-abstraction epic
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:42:59 +01:00
James VeitchandClaude Sonnet 5 3df38485d2 docs: update README/docs for the generic node rename, add migration table (FR-009)
README.md and docs/index.md still named OllamaChatCompletion/OllamaModelSelector/
OllamaLoadModel/OllamaUnloadModel and the OLLAMA_CLIENT socket — stale after
the atomic cutover. Updated both (they're content-duplicates) and added the
old->new migration table FR-009 requires, which previously only existed as
a code comment (comfydv.ollama.MIGRATION_MAP), not surfaced to users.

Found via a dedicated docs-freshness pass, not caught by the beacon-reviewer
correctness review (which is scoped to code, not prose).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:42:19 +01:00
James VeitchandClaude Sonnet 5 8e227790a9 fix(llm): forward options through to structured output (beacon-reviewer finding)
chat_structured() silently dropped the options dict (temperature, seed,
num_predict, repeat_penalty, etc. — set via the OllamaOption* nodes) in
structured_output mode: OllamaProvider.chat_structured() only used it for
the cache key, and the shared comfydv._llm.chat.chat_structured() helper
had no options parameter at all. On main, the hand-rolled implementation
sent options verbatim in the request body; this migration silently lost it,
contradicting FR-008 (structured-output behavior must be unchanged).

Fixed by forwarding options as pydantic-ai's model_settings.extra_body,
matching the original payload shape exactly rather than lossily remapping
onto ModelSettings' own standardized field names (which don't cover
Ollama-native params like num_predict/repeat_penalty/top_k anyway).

Verified against the live local Ollama server: structured output with
temperature=0.0/seed=42 succeeds on first attempt.

Caught by an independent beacon-reviewer pass on the diff before opening a
PR — exactly what that review step is for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:35:51 +01:00
James VeitchandClaude Sonnet 5 6c2c906c4a docs(beacon): mark tasks.md Phase 7 subsumed by T-CUT-10/12
Same lint/typecheck/doctor/live-validation work, done together with the
cutover rather than as a separate pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:29:20 +01:00
James VeitchandClaude Sonnet 5 1879ca2265 test(llm): complete atomic node cutover — test_ollama.py rewrite (T-CUT-08/10/12)
Rewrote tests/test_ollama.py against a _FakeProvider double per
atomic-cutover-plan.md's D4/D5 test-layer split: node-contract/delegation
tests only, no aiohttp/_post_json mocking (that lives in
test_ollama_provider.py now). 98 unit tests, collapsed from the original
~125 largely mechanical monkeypatch-per-scenario tests where the retry/
validation coverage they duplicated already lives in
test_llm_chat_structured.py.

Fixed a real gap the rename surfaced along the way: comfy-manager-entry.json
(ComfyUI Manager registry listing) still had the old Ollama-specific
display names — updated it and its matching test expectation.

Full validation (T-CUT-10): 218 tests green (only the pre-existing,
unrelated Dockerfile-python-version test fails); ruff/ty clean (confirmed
via git stash comparison that the remaining ty diagnostics pre-date this
work); beacon doctor --strict shows only already-disclosed items.

Manual smoke test (T-CUT-12): Ollama was reachable in this environment, so
ran the real integration suite rather than a walkthrough. 6/8 passed
including the critical paths (real load/unload, real structured-output
retry-then-raise, real error handling). 2 failures traced via direct curl
to Ollama's /api/chat (bypassing this codebase) to a pre-existing,
ADR-006-documented unreliability in the specific test model, not a
regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:28:34 +01:00
James VeitchandClaude Sonnet 5 d90ae98fed feat(llm): atomic node cutover — generic ChatCompletion/LLM* nodes (T-CUT-01/04-07/11)
OllamaClient now emits an OllamaProvider via the generic LLM_CLIENT socket
(was OllamaClientType/OLLAMA_CLIENT). OllamaChatCompletion, OllamaModelSelector,
OllamaLoadModel, OllamaUnloadModel renamed to ChatCompletion, LLMModelSelector,
LLMLoadModel, LLMUnloadModel — all delegate to client.* (LLMProvider protocol)
instead of building Ollama URLs/payloads inline. Structured output now goes
through client.chat_structured() (pydantic-ai), not hand-rolled tool-calling.

ollama.py's own duplicate HTTP/cache infra (_post_json, _TTLLRUCache,
_MODEL_LIST_CACHE, _CHAT_RESPONSE_CACHE) is gone — single source of truth in
comfydv._llm.ollama_provider (plan D1); the combo-widget path
(_load_default_models, /dv/ollama/models) imports _fetch_models/_run_async
from there instead of duplicating them. _client_headers deleted (dead code
once nodes delegate to client.*).

Added MIGRATION_MAP (old->new node/socket names, FR-009). NODE_CLASS_MAPPINGS
keys changed to match (breaking rename, confirmed acceptable per ADR-007).

conftest.py's autouse cache-clearing fixture repointed to the single cache
source and ChatCompletion (plan D8).

tests/test_ollama.py itself is NOT yet updated (T-CUT-08, next commit) — it
will fail to collect until then; this is the expected atomic-cutover
intermediate state, not a regression to fix independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:19:07 +01:00
James VeitchandClaude Sonnet 5 fb111c0142 feat(llm): implement OllamaProvider method bodies (T-CUT-02/03)
list_models()/load_model()/unload_model()/chat()/chat_structured() —
behavior-preserving ports of the existing inline ollama.py logic, plus one
genuinely new capability: list_models() also queries /api/ps to report
real loaded/unloaded status (the old OllamaModelSelector only ever showed
names via /api/tags, never live status).

15 new tests in tests/test_ollama_provider.py, mocking at the module's own
_post_json/_get_json seam per atomic-cutover-plan.md's D4 test-layer split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:14:51 +01:00
James VeitchandClaude Sonnet 5 22dbddc8eb docs(beacon): fix tasks.md checkbox hygiene, add trackable T-CUT phase
Struck-through historical tasks (- [ ] ~~...~~) still read as open
checkboxes to beacon's tooling — bullet status was picking one as "active"
instead of the real next step. Marked all superseded Phase 3/5/T014/T019-21
entries [-] (deferred, per the project's own convention), and added a real
Phase 8 with T-CUT-01..12 as trackable checkboxes matching
atomic-cutover-plan.md, so tooling and humans see the same source of truth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 14:12:02 +01:00
James VeitchandClaude Sonnet 5 ef2464aebb docs(beacon): properly spec the atomic node cutover (issue #16)
Full line-by-line inventory of every reference in src/comfydv/ollama.py
and tests/test_ollama.py (1820 lines, read in full — not estimated)
affected by the OllamaClient/OllamaChatCompletion/OllamaModelSelector/
OllamaLoadModel/OllamaUnloadModel rename, plus the design decisions it
surfaced that "just rename it" was hiding:

- Single source of truth for HTTP/cache infra (comfydv._llm.ollama_provider)
- client == "<host string>" equality is removed, not preserved (OllamaProvider
  isn't a str subclass)
- Bare-string client backward compatibility is removed (undocumented side
  effect of the old str-subclass trick, never an intended feature)
- Test-layer split: node-contract/delegation tests stay in test_ollama.py
  using a _FakeProvider double; Ollama-wire-protocol tests move to a new
  test_ollama_provider.py — this is what actually resolves the 35 relocated
  _post_json monkeypatches, rather than patching them 1:1 at a seam that no
  longer exists
- Structured-output retry tests are not ported 1:1 — already covered by
  tests/test_llm_chat_structured.py's existing 6 tests

Produces a 12-step sequenced task list (T-CUT-01..12) in
atomic-cutover-plan.md that supersedes tasks.md's original US1/US3
decomposition (struck through, kept for history). This is now the
authoritative plan for the next BUILD session to execute.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 13:55:39 +01:00
James VeitchandClaude Sonnet 5 736ffd1e77 feat(llm): shared chat_structured() via pydantic-ai (T011-T013)
Provider-layer only — src/comfydv/_llm/chat.py doesn't touch ollama.py, so
it's safe to land ahead of the deferred atomic node cutover (issue #16).
Ports ADR-006's exact retry/validation contract: bounded retries (0-5,
clamped), RuntimeError naming model/attempt-count/truncated-last-response
on exhaustion. Agent's own internal retries disabled so the error contract
is comfydv's, not pydantic-ai's.

Required an API-discovery spike (Agent/OpenAIProvider/OpenAIChatModel
constructors and exception surface aren't reliably knowable from training
data for a fast-moving library) before tests could be written meaningfully
— tests and implementation were validated together rather than strictly
red-first; noted in tasks.md rather than presented as pure TDD.

Also fixes a pre-existing gap in test_packaging.py's requirements.txt/
pyproject.toml consistency check: it stripped `[extras]` on one side of the
comparison but not the other, which this change's pydantic-ai-slim[openai]
entry exposed as the first dependency with an extras marker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 13:37:16 +01:00
James VeitchandClaude Sonnet 5 1b1c7830d5 docs(beacon): correct tasks.md — US1/US3 cutover is atomic, not incremental
Discovered mid-implementation: OllamaClient is a single shared producer for
every downstream Ollama node, so changing its output type per ADR-007
breaks all consumers simultaneously, and the class rename breaks
tests/test_ollama.py's 125 references atomically, not per-story. Confirmed
by independent product + engineering review (agent-trio deliberation,
aligned). Re-scoped the node-layer cutover as its own follow-up session,
tracked in issue #16. ADR-007's decision itself is unaffected — only
delivery sequencing changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 13:31:21 +01:00
James VeitchandClaude Sonnet 5 9d90f39854 feat(llm): scaffold LLMProvider protocol and OllamaProvider (T001-T006)
Adds pydantic-ai-slim[openai] per ADR-007, the Message/ModelStatus/
ModelInfo/LLMProvider protocol, and an OllamaProvider skeleton carrying
host/headers as instance state — ported _post_json/_fetch_models/_run_async/
_TTLLRUCache infra from comfydv.ollama unchanged. Method bodies land per
user story in specs/007-llm-provider-abstraction/tasks.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 13:25:47 +01:00
James VeitchandClaude Sonnet 5 fb0556fff3 docs(beacon): DESIGN artifacts for LLM provider abstraction (ADR-007)
Adopt a shared LLMProvider protocol (list/load/unload/chat/structured-chat)
as the adapter pattern answer to GitHub issue #15 — Ollama and llama.cpp
share the chat/structured-output mechanism via pydantic-ai while model
lifecycle stays backend-specific per provider. Supersedes ADR-006, narrows
ADR-004's scope to non-chat REST calls.

Adds epic 007 (llm-provider-abstraction, prerequisite) and the
llamacpp-integration epic it unblocks, plus the full spec/plan/tasks/BDD
scaffold for the prerequisite epic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132ojafeazQ3ephcBejEWFj
2026-07-11 13:25:40 +01:00
James Veitch b3374c88c4 add loop 2026-07-11 13:07:58 +01:00
James Veitch 50076d7c6b add manager to local harness 2026-07-10 16:40:51 +01:00
James Veitch aa7ff8dae3 Merge pull request #14 from darth-veitcher/feature/ollama-structured-output
feat(ollama): opt-in structured output via tool-calling + pydantic validation
2026-07-09 17:51:08 +01:00
James VeitchandClaude Sonnet 5 253569d483 feat(ollama): live dynamic output sockets for structured_output
Adds a POST /dv/ollama/update_structured_outputs route and a matching JS
extension so editing structured_output/output_schema on
OllamaChatCompletion updates the node's output sockets immediately in the
UI — one per schema property — mirroring FormatString's existing live
dynamic-output pattern. No need to run the graph first to see the extracted
fields appear. Invalid/incomplete JSON while typing falls back to the base
3-output shape rather than erroring the request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 17:37:41 +01:00
James VeitchandClaude Sonnet 5 40e851588d feat(ollama): opt-in structured output via tool-calling + pydantic validation
Adds structured_output/output_schema/max_retries inputs to
OllamaChatCompletion, fixing three real reliability problems: models
prepending commentary, wrapping responses in code fences, and occasionally
returning blank output. When enabled, requests go through Ollama's
OpenAI-compatible tool-calling endpoint (a single forced tool call matching
output_schema) instead of native /api/chat, and each response is validated
against a dynamically-built pydantic model — invalid/blank responses retry
with fresh network calls, exhausting into a clear RuntimeError rather than
silently passing bad data downstream. One additional ComfyUI output socket
is exposed per schema property, mirroring FormatString's existing
dynamic-output pattern. Fully backward compatible: structured_output
defaults off and non-structured behavior is untouched.

Ollama's native /api/chat "format" field (JSON-Schema-constrained decoding)
was tried first but proved unreliable against a real local model during
implementation — see ADR-006 for the full investigation and why
tool-calling was chosen over both native format and pydantic-ai (the latter
would reintroduce httpx/openai-sdk, reversing ADR-004).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:50:54 +01:00
James Veitch 23f8f55493 Merge pull request #13 from darth-veitcher/chore/finish-ollama-integration-epic
chore(beacon): finish ollama-integration epic
2026-07-04 14:09:52 +01:00
James VeitchandClaude Sonnet 5 d38cd5e794 chore(beacon): finish ollama-integration epic
All owned specs shipped, so beacon epic finish moves it to archive/ and
flips status to Done. Clears the epic-spec-lifecycle and epic-gates
warnings beacon doctor --strict was raising.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 14:09:15 +01:00
James Veitch e42913d587 Merge pull request #12 from darth-veitcher/chore/dynamic-versioning-from-vcs
chore: derive package version from git tags via hatch-vcs
2026-07-04 14:03:15 +01:00
James VeitchandClaude Sonnet 5 335ff5e31a chore: derive package version from git tags via hatch-vcs
Replaces the static project.version string with dynamic = ["version"],
sourced from the nearest git tag (hatch-vcs). Fixes docs.yml, which read
pyproject.toml's version field directly and would have broken once that
field stopped being a plain string; it now reads the resolved version via
importlib.metadata after `uv sync` installs the package.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 13:51:55 +01:00
James Veitch 4f69405af6 Merge pull request #11 from darth-veitcher/feature/ollama-auth-headers-and-format-string-inline-display
feat: Ollama auth headers + response cache, FormatString inline display
v0.1.0
2026-07-04 13:45:07 +01:00
James VeitchandClaude Sonnet 5 eef0f1ff59 feat(format-string): inline read-only display of formatted_string
FormatString is now OUTPUT_NODE=True and returns {"ui": {"text": [...]},
"result": (...)} instead of a bare tuple, so the rendered output shows
directly on the node — no separate Show Text node needed to inspect it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 13:43:37 +01:00
James VeitchandClaude Sonnet 5 fcb95fadfa feat(ollama): per-client auth headers and local response caching
Adds an OLLAMA_HEADERS socket type (Basic Auth, Bearer Token, Custom) that
composes onto OllamaClient, so requests to a proxied/authenticated Ollama
server carry the right Authorization header through model discovery,
load/unload, and chat.

Also adds a bounded local cache: a short-TTL LRU for model discovery
(avoids duplicate /api/tags round-trips from repeated node creation/refresh)
and an unbounded-TTL LRU for chat responses, keyed on the full request
signature — needed because OllamaChatCompletion is OUTPUT_NODE=True and so
is re-executed by ComfyUI on every queue run even when nothing changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 13:43:28 +01:00
James VeitchandClaude Sonnet 4.6 fbf73a7d74 fix(ci): use --group docs instead of --extra docs for uv sync
dependency-groups in pyproject.toml are not optional-dependencies extras;
uv sync uses --group <name> to include them.

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