* feat: v2.1.0 — fal platform utilities (async fan-out, cost tools, recovery) Six new nodes under FAL/Platform, built on fal's queue/request-id/pricing primitives: - Fal Submit + Fal Collect: queue jobs without blocking and collect results later — N generations run in parallel inside one graph - Fal Result by Request ID: re-fetch any past generation from fal's queue without re-generating or re-billing - Fal Cost Estimator: per-run cost report parsed from registry pricing before anything is queued - Fal Session Costs: in-memory ledger of every fal call this session (endpoint, duration, request id, est. cost) with running total - Fal Save Media from URL: persist expiring CDN results into the ComfyUI output directory Core: utils/pricing.py (pricing-text parser + registry lookup), utils/ledger.py (thread-safe session ledger), api.py gains submit_only, result_from_request_id, request-id capture via on_enqueue, and automatic ledger recording; any_endpoint helpers extracted for reuse. 16 new tests (61 total). * feat: v2.2.0 — persistent result cache, spend guard, URL passthrough Three fal-platform features on top of the 2.1 platform pack: - Persistent result cache (sqlite, [cache] config): identical fal calls are served free from disk across ComfyUI restarts; uploads are deduplicated by content hash so identical inputs upload once. Bypass per node via force_rerun. Cache hits are never blocked by the spend guard and never recorded as spend. - Spend Guard + billing ([spend_guard] config): preflight refuses to submit once the session's estimated spend reaches session_budget_usd or the account balance (fal Billing API) falls below min_balance_usd. New Fal Account Balance node; ledger reconciliation against the aggregated usage API (fal exposes no per-request billed amounts). - fal-CDN URL passthrough: every dynamic node's media inputs gain optional *_direct_url twins (URL sent directly, no download/re-upload; 1,405 twins across 992 nodes) and image-kind nodes output image_urls alongside IMAGE — fal→fal chains with zero local I/O. Review fixes: path traversal blocked in FalSaveMediaURL (confined to the output dir, atomic O_EXCL filename claim), FalAnyEndpoint force_rerun now bypasses the cache, cache lookup ordered before spend-guard preflight, result recovery no longer counts as new session spend. 14 new tests (76 total).
57 lines
1.7 KiB
Python
57 lines
1.7 KiB
Python
"""Unit tests for the registry pricing parser."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
from conftest import PKG, _load_package
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def pricing_mod():
|
|
_load_package()
|
|
return importlib.import_module(f"{PKG}.nodes.utils.pricing")
|
|
|
|
|
|
def test_run_ratio_wins_over_per_unit(pricing_mod):
|
|
text = (
|
|
"Your request will cost $0.08 per image. For $1.00, you can run "
|
|
"this model 12 times."
|
|
)
|
|
parsed = pricing_mod.PricingUtils.parse(text)
|
|
assert parsed["per_run"] == pytest.approx(1.0 / 12.0)
|
|
|
|
|
|
def test_per_unit_only(pricing_mod):
|
|
parsed = pricing_mod.PricingUtils.parse(
|
|
"Your request will cost $0.05 per second of video."
|
|
)
|
|
assert parsed["per_run"] is None
|
|
assert parsed["per_unit"] == pytest.approx(0.05)
|
|
assert "second" in parsed["unit"]
|
|
|
|
|
|
def test_per_image_implies_per_run(pricing_mod):
|
|
parsed = pricing_mod.PricingUtils.parse("Your request will cost $0.04 per image.")
|
|
assert parsed["per_run"] == pytest.approx(0.04)
|
|
|
|
|
|
def test_junk_never_raises(pricing_mod):
|
|
for junk in ("", "free during preview!!", "$", "per per per", None or ""):
|
|
parsed = pricing_mod.PricingUtils.parse(junk)
|
|
assert parsed["raw"] == junk
|
|
|
|
|
|
def test_estimate_against_real_registry(pricing_mod):
|
|
est = pricing_mod.PricingUtils.estimate("fal-ai/nano-banana-2/edit", 10)
|
|
assert est["runs"] == 10
|
|
report = pricing_mod.PricingUtils.format_report(est)
|
|
assert "fal-ai/nano-banana-2/edit" in report
|
|
|
|
|
|
def test_unknown_endpoint_safe(pricing_mod):
|
|
est = pricing_mod.PricingUtils.estimate("fal-ai/does-not-exist", 3)
|
|
report = pricing_mod.PricingUtils.format_report(est)
|
|
assert isinstance(report, str) and report
|