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
Tests
This directory contains comprehensive pytest tests for the comfydv package.
Running Tests
# Run all tests
uv run pytest
# Run with verbose output
uv run pytest -v
# Run with coverage report
uv run pytest --cov=src/comfydv --cov-report=html
# Run specific test file
uv run pytest tests/test_format_string.py
# Run specific test class
uv run pytest tests/test_format_string.py::TestVariableExtraction
# Run specific test
uv run pytest tests/test_format_string.py::TestVariableExtraction::test_extract_simple_single_variable
# Run tests matching a pattern
uv run pytest -k "jinja2"
Coverage reports are available in htmlcov/index.html after running with --cov-report=html.
Test Coverage
Current: 75% (203 statements total, 51 missed)
format_string.py: 78% coverage__init__.py: 100% coveragecircuit_breaker.py: 68% coveragerandom_choice.py: 60% coverageutils.py: 75% coverage
Test Structure
conftest.py
Contains pytest configuration, fixtures, and mocks for ComfyUI dependencies:
- Mock ComfyUI modules (
comfy,server,folder_paths,aiohttp) - Uses
pytest_configurehook to install mocks before test collection - Provides
format_string_classfixture usingimportlibto directly load module - Fixtures for test data and class instances
- Pytest hooks for early mock installation
test_format_string.py
Comprehensive test suite for the FormatString node with 47 tests organized into classes:
- TestVariableExtraction (12 tests): Variable extraction from templates
- TestSimpleFormatting (4 tests): Python format string rendering
- TestJinja2Formatting (5 tests): Jinja2 template rendering
- TestDynamicOutputs (6 tests): Dynamic output configuration
- TestOutputConsistency (3 tests): Outputs match RETURN_TYPES/RETURN_NAMES
- TestInputTypes (4 tests): INPUT_TYPES method validation
- TestIsChanged (5 tests): Cache invalidation logic
- TestStatePersistence (2 tests): State saving/loading
- TestEdgeCases (4 tests): Error handling and edge cases
- TestTimeNowFunction (2 tests): time_now utility function
Mocking Strategy
The tests use importlib.util to directly load the format_string.py module, bypassing the package __init__.py which has ComfyUI dependencies. This allows testing without ComfyUI installation while maintaining the root __init__.py for ComfyUI extension discovery.
Writing Tests
Example test
def test_new_feature(self, format_string_class, sample_data):
"""Test that new feature works correctly."""
result = format_string_class.some_method(sample_data["name"])
assert result == expected_value
Available fixtures
format_string_class: Fresh FormatString class with reset statesample_templates: Dictionary of sample template stringssample_data: Dictionary of sample data for templates
Test naming convention
Use descriptive names: test_<what>_<condition>_<expected>
Troubleshooting
Debugging failed tests
# Run with verbose output
uv run pytest -vv --tb=long
# Run with pdb debugger
uv run pytest --pdb
Test isolation
Each test is independent. The format_string_class fixture provides a fresh instance with reset state.
Future Improvements
- Add integration tests with actual ComfyUI installation
- Add JavaScript tests for frontend functionality
- Increase coverage for circuit_breaker and random_choice nodes
- Add property-based testing with Hypothesis