> built 2026-10-14 12:58 UTC from 14eff54 (main) · falaw 0.0.62. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# falaw

Agent-friendly Python facade over fal.ai for generating and managing AI media (images, video, audio).

```python
from falaw import generate_image, list_models, journal

r = generate_image("a tiger eye, macro, 35mm", quality="fast")
r.first.download(to="./tiger.png")

[m.id for m in list_models(category="image_to_video")]
journal.note("schnell at quality='fast' defaults to 1024x1024")
```

## Why

`fal-client` already gives you 100+ models behind a uniform call. What
agents (and humans) still struggle with is *which* model to use, *what*
parameters it takes, and *what to do with* the URL it returns. `falaw`
adds:

- Task-level verbs (`generate_image`, `text_to_speech`, …) with smart model selection by quality tier.
- A queryable model registry — no more grepping docs for IDs.
- `Result` / `Asset` objects that download, name, and organize outputs.
- A journal so each session leaves notes for the next one.
- A Claude skill, plus stub bridges for MCP and HTTP services — all derived from the same tool registry.

## Install

```bash
pip install 'falaw[fal]'   # to actually call fal.ai (pulls in fal-client)
export FAL_KEY="your-fal-api-key"
```

`pip install falaw` alone installs **no fal.ai client**. That is enough to build, price, hash, serialize and cache `Plan`s (and to read cache hits), which is all a caller needs if it never makes a paid call. The first real network call raises `falaw.FalClientNotInstalled` (an `ImportError`) naming `pip install 'falaw[fal]'`. Anything that executes plans declares `falaw[fal]` as its requirement.

### From the browser

The planning half of this package ships to npm as **`falaw-client`** (the `ts/`
directory): the catalogue with its prices, the cost rules, `CallPlan` and
`plan_hash` are generated from and pinned to this package, and execution goes
through a server relay because fal.ai forbids browser-held keys.

```ts
import { planGenerateImage, makePlan, totalCostUsd, execute, queueTransport } from 'falaw-client';
const plan = makePlan([planGenerateImage({ prompt: 'a tiger eye', quality: 'fast' })]);
totalCostUsd(plan);  // before any network
await execute(plan, { transport: queueTransport({ proxyUrl: '/api/fal/proxy', key }) });
```

See [`ts/README.md`](). After changing a dataclass, the catalogue,
a cost rule, `parse_response` or the canonical byte-form, run
`python -m falaw export-schema`, then `cd ts && npm run codegen`, and commit
both — `tests/test_schema_export.py` and `ts/src/codegen.test.ts` fail until
you do.

## Core surface

| Function                                                           | Purpose                                                                                            |
|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
| `generate_image(prompt, *, quality, image_size, model_id, extra)`  | Text-to-image, picks FLUX by quality tier.                                                         |
| `text_to_speech(text, *, quality, voice, model_id, extra)`         | TTS, picks a voice model by tier.                                                                  |
| `list_models(*, category, quality_tier)`                           | Browse the catalog.                                                                                |
| `pick_model(*, category, quality_tier)`                            | Pick a sensible default.                                                                           |
| `call_fal(application, arguments, *, on_event)`                    | Escape hatch to any fal model. Emits `ProgressEvent`s + auto-journals on error.                    |
| `cached_call_fal(...)`                                             | Same, plus content-addressed cache; emits `cache_hit` events on reuse.                             |
| `execute_plan(plan, *, concurrency=N)`                             | Run a `Plan` → `list[Artifact]`. Raises on the first failure, unwrapped.                           |
| `execute_plan_isolated(plan, *, concurrency=N)`                    | Run a `Plan` → `ExecutionReport`: one outcome per call, so one bad call does not discard the rest. |
| `plan_dependencies(plan)`                                          | The Plan’s `"<from N>"` dependency DAG, and its structural validator.                              |
| `render_scene(scene, *, concurrency=N)` / `iter_render_scene(...)` | Render every shot+beat; thread-pooled, with yield-as-done iterator.                                |
| `estimate_scene_cost(scene)`                                       | Walk a Scene, return a `CostRollup` with per-line USD breakdown.                                   |
| `subscribe(callback)`                                              | Attach a global subscriber to the `ProgressEvent` bus.                                             |
| `journal.note / issue / improvement(...)`                          | Leave a trace for future sessions.                                                                 |
| `Session(output_dir=...)`                                          | Optional stateful controller.                                                                      |

### Structured progress events

`call_fal` and `cached_call_fal` emit `ProgressEvent`s at every
lifecycle transition (`queued`, `progress`, `log`, `done`, `error`,
`cache_hit`). Subscribe per-call (`on_event=`) or globally
(`falaw.subscribe(...)`); the legacy `on_log=print` is still honored
for backward compatibility.

```python
from falaw import subscribe, generate_image

subscribe(lambda ev: print(f"[{ev.kind}] {ev.application} {ev.elapsed_s:.2f}s"))
generate_image("a tiger eye", quality="fast")
```

### Cost estimation

`ModelRecord.cost_estimate: CostEstimate | None` carries a structured
`{kind, amount, currency}` price (kinds: `per_call | per_image | per_second | per_token | per_megapixel`). `estimate_scene_cost(scene)`
sums per-call costs and returns a `CostRollup` with per-line
breakdown. Models without a populated `cost_estimate` appear in the
rollup’s `skipped` list so audits surface drift.

### Pricing a direct Anthropic / OpenAI call

falaw prices calls made straight to the model vendor too, from one dated table (`falaw/data/first_party_llm_prices.json`), so an app does not keep its own copy:

```python
from falaw import llm_call_cost_usd

usd, known = llm_call_cost_usd("anthropic", "claude-opus-4-7", response.usage)
```

`usage` is the vendor’s own (a dict or the SDK object; OpenAI’s Responses and Chat Completions shapes both work, cached tokens included). A model the table does not list is priced at the provider’s dearest listed rates with `known=False`: never a free call, but not a ceiling either (an older model can cost more), so a caller that must never under-count refuses when `known` is false.

### Re-pricing a saved plan

A `CallPlan`’s `estimated_cost_usd` is frozen at plan time — right for a quote, wrong for a gate, because rate tables move (0.0.46 re-quoted every premium LLM call tenfold *upward*). Every `plan_*` therefore records **how** it priced the call in `CallPlan.cost_basis`: which pricer, which rate table and version, and the quantity hints (`duration_s`, token bounds) that price the call but deliberately never enter `arguments`. `reprice_plan(plan)` reads that back and re-quotes at today’s rates — pure data, no network, no cache peek:

```python
from falaw import plan_from_dict, reprice_plan

out = reprice_plan(plan_from_dict(saved_row))
out.plan  # same calls, same plan_hash, today's costs
out.known_delta_usd  # how much the priced part moved
out.changed  # per-call diffs: index, status, old, new, basis_changed
out.unpriced  # calls today's rates cannot price — refuse the gate
```

Per-call status is `unchanged`, `changed`, `unknown` (a basis today’s tables cannot price) or `no_basis` (a plan saved before this existed). The last two **clear the call’s cost to `None`** and light up `Plan.has_unknown_costs`: a stale figure re-presented as a current quote is the failure this fixes, so an unknown basis is reported, never trusted. `cost_basis` is descriptive only — it is omitted from the serialized dict when unset, and never enters `plan_hash` or the cache key, so existing plans and cassettes are unmoved.

A basis is only re-quoted by a pricer reading the **same table** it names; a mismatch is `unknown`, never a re-quote. Two tables are two sets of books, and re-pricing a caller’s reconciled $0.50 row at falaw’s published $0.01 is a 50x under-quote wearing the clothes of a price drop. So a call you quoted with your own `llm_rates=` needs your own `Pricer` to re-quote it: `reprice_plan(plan, pricers={**DFLT_PRICERS, "llm_rates": mine})`, where `mine.table` is the identity the basis carries.

### Fan-out: partial results, bounded concurrency, per-call isolation

A `Plan` is a fan-out — 200 panels is 200 `CallPlan`s in one Plan — so
`execute_plan`’s `list[Artifact]` has no room to say  *“call 7 failed, here are
the other 199”*. `execute_plan_isolated` does:

```python
from falaw import execute_plan_isolated

report = execute_plan_isolated(plan, concurrency=8)

for outcome in report.outcomes:  # always one per call, in plan order
    if outcome.ok:
        save(outcome.artifact)
    elif outcome.status == "failed":
        retry_later(outcome.call, outcome.error)  # retry this call verbatim
    else:
        replan(outcome.call, outcome.reason)  # blocked: its input does not exist

report.estimated_spend_usd, report.has_unknown_costs, report.cache_hit_savings_usd
```

Three states, not two. **Failed** means the call raised and can be retried as
it is. **Blocked** means it never ran — a `"<from N>"` placeholder whose
producer did not succeed — so it has to be re-planned, not retried. A caller
that cannot tell them apart retries a call whose input does not exist.

`concurrency` bounds how many calls are in flight; independent calls run in
parallel, and a call is never started beside the producer it consumes. It
defaults to `1` (sequential) everywhere, here and in `render_scene`, because
every call is a paid request: parallelism is opt-in. Weigh the vendor’s rate
limit and memory — materializing a media result peaks at roughly twice the
asset’s size, and `concurrency` multiplies that.

`execute_plan` keeps the plain `list[Artifact]` contract and re-raises the
first failure **unwrapped**, so falaw’s typed error hierarchy still reaches the
caller. It is `execute_plan_isolated(...).artifacts_or_raise()` — one engine,
two policies.

`render_scene(..., concurrency=4)` runs shots and beats in parallel through the
same kind of thread pool (fal calls are HTTP-bound). Use
`iter_render_scene(...)` to yield `(kind, result)` pairs as each unit completes
— handy for live UI updates.

## Three cache modes, because a re-run is not a cache bypass

`execute_plan` reads the cache and writes to it. Those are separate decisions, and it takes two flags rather than one so a caller can ask for either half.

|                                    | cache read   | cache write   | ask for it with                       |
|------------------------------------|--------------|---------------|---------------------------------------|
| **reuse, then keep** — the default | yes          | yes           | `execute_plan(plan)`                  |
| **re-run, but keep**               | no           | yes           | `execute_plan(plan, refresh=True)`    |
| **do not touch the cache**         | no           | no            | `execute_plan(plan, use_cache=False)` |

`refresh=True` is what belongs behind a `force` / “re-verify this” switch. `use_cache=False` re-runs *and discards*, so the next consumer of the same plan pays for the result the forced run already bought — a guaranteed double charge, noise on an LLM plan and a real charge for nothing on an image or video one. Reach for `use_cache=False` only when you genuinely want no cache interaction at all.

`use_cache=False, refresh=True` raises: there is no key to write under when the cache is off.

## Testing code that uses falaw

falaw content-addresses every media result, so `execute_plan` **reads the
bytes** behind each result URL. A suite that stubs the fal *response* but not
the *asset transport* is resolving its made-up URLs for real — and it passes
while doing so, because a failed fetch degrades to a URL-only artifact with a
`UserWarning` rather than raising.

falaw ships the fake, so nobody has to write one. Three autouse fixtures, each
re-exportable into a `conftest.py` in one line:

```python
# conftest.py
from falaw.testing import (  # noqa: F401
    fake_assets,  # url -> bytes, served from memory
    isolated_falaw_cache,  # cache / content store / url-index under tmp_path
    no_outbound_network,  # refuses AND records any non-loopback connection
)
```

```python
def test_two_urls_one_content_address(fake_assets):
    shared = fake_assets.serve("http://cdn/a.png", b"identical bytes")
    fake_assets.serve("http://cdn/b.png", shared)  # pin explicit bytes
    fake_assets.fail("http://cdn/gone.png")  # 404, as an expired asset does
    ...
    assert fake_assets.fetched == [...]  # assert on the record
```

`fake_assets` installs itself through `falaw.content.using_url_fetcher`, the
public transport seam, so it covers `execute_plan`, `execute_plan_isolated`,
`materialize_asset` and `content_ref_for_url` at once — including calls your
own public API makes, which have no `asset_fetcher=` argument to pass. An
explicitly passed `fetcher=` still wins. `file://` URLs are *not* faked (they
are not the network); pin one explicitly if you want it served from memory.

`using_url_fetcher` is not test-only: it is also how you give falaw an
authenticated, retrying, or mirrored transport in production, in one place
instead of at every call site.

Two rules that are easy to get wrong:

- **Don’t reach for `FALAW_FETCH_ARTIFACT_BYTES=0` to make a suite offline.**
  It works by turning content addressing *off* — the suite goes hermetic and
  stops testing the feature.
- **Assert on the record, not on an exception.** falaw absorbs a failed fetch
  by design, so a guard that only raises is invisible. That is why both the
  fake and the network guard *record* every attempt, and why
  `no_outbound_network` fails the test at **teardown**.

Marker `live_api` opts a test back into the real transport and no network
guard. A suite with different marker names builds its own fixtures from
`make_fake_assets_fixture` / `make_no_outbound_network_fixture`.

## Architecture

Single source of truth: a `ToolSpec` dataclass per tool. From it we derive every external surface:

```default
falaw.registry  ──► bridges/skill.py    ──►  .claude/skills/falaw/SKILL.md
                ──► bridges/mcp.py      ──►  MCP server          (planned)
                ──► bridges/service.py  ──►  qh HTTP service     (planned)
                ──► (UI)                                          (planned)
```

Adding a new surface is a new bridge module, never a re-implementation of the operations.

## Self-improvement loop

Every session can read and write the agent journal at `~/.config/falaw/journal/`. The Claude skill instructs Claude to:

1. Read recent entries before novel work.
2. Write a note / issue / improvement when something surprises it.

`call_fal` auto-journals failures with the application id and arguments, so the next session recognizes the trap.

## Layout

```default
falaw/
  base.py            ToolSpec, ModelRecord
  core.py            call_fal: subscribe + auto-journal
  registry.py        register_tool, list/get/pick model
  reprice.py         reprice_plan: re-quote a saved Plan at today's rates
  results.py         Asset, Result, parse_response
  session.py         Session
  journal.py         file-backed journal
  operations/
    images.py        generate_image
    audio.py         text_to_speech
  bridges/
    skill.py         render Claude SKILL.md from registry
    mcp.py           (stub)
    service.py       (stub)
  data/
    models.json      seed catalog
    skills/falaw/    generated skill files (shipped with package)
misc/
  docs/              aggregated fal.ai docs (3MB md, llms.txt, llms-full.txt)
  regenerate_skill.py
tests/
```

## Regenerate the skill after adding a tool

```bash
python misc/regenerate_skill.py
```

Writes `falaw/data/skills/falaw/SKILL.md` and `.claude/skills/falaw/SKILL.md`.

## Status

v0 — functional core, real Claude skill, stubs for MCP and HTTP service. The bridges share the same registry, so filling in the stubs is additive.

## Roadmap

See [`misc/docs/roadmap.md`]() for the ordered work and the standing
constraints. In short, four tracks:

1. **Content addressing** — *landed* (falaw#14). `Artifact.asset_id` is the SHA-256 of the
   artifact’s bytes and the per-call cache key holds upstream *content* hashes, not fal CDN URLs.
   fal states that every upload gets a unique URL and that expired files are permanently deleted,
   so URL-keying meant a byte-identical regeneration missed the cache and a stored response decayed
   into a dead link. Bytes now route through `lacing.ArtifactStore.put_blob_stream`. Still open:
   the plan-time cache peek keys on *unresolved* arguments (D2), which lands with track 2.
2. **Backend-parametric `Plan`** — `CallPlan.backend` + registry dispatch in `execute_plan`, so
   a second execution backend is still priced, cached and dry-runnable.
3. **Licence-and-terms ledger** — per `(model, backend)`, queried at plan time, `unknown` means
   refuse.
4. **Cost data from the vendor** — read fal’s pricing API instead of hand-maintaining
   `data/models.json` (19 of 40 records currently carry no structured price at all).

<p class="epythet-aggregates">This documentation as a single file: <a href="falaw.md">falaw.md</a> (Markdown, for agents).</p>


# _autosummary/falaw.account.html.md

# falaw.account

Account health probe.

`fal_client` swallows the JSON body of its 403s, so a locked-account error
looks identical to “wrong API key” or “model is busy.” [`health_check()`](_autosummary/falaw.account.html.md#falaw.account.health_check)
makes a tiny, idempotent request to the fal storage endpoint and returns a
typed [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus) so the caller can distinguish:

- account is locked / unverified  → [`AccountStatus.locked`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus.locked) is True
- bad credentials                 → [`AccountStatus.unauthorized`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus.unauthorized) is True
- everything looks fine            → [`AccountStatus.ok`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus.ok) is True
- network / unknown error          → all flags False, `error` populated

Use this *before* a long render to fail fast with a useful message:

```python
from falaw.account import health_check
status = health_check()
if not status.ok:
    raise SystemExit(status.message_for_user())
```

### Functions

| [`health_check`](_autosummary/falaw.account.html.md#falaw.account.health_check)(\*[, probe_url, timeout_s, api_key])   | Probe the fal storage endpoint and return a typed [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus).   |
|------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|

### Classes

| [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus)(ok, locked, unauthorized, ...)   | Outcome of [`health_check()`](_autosummary/falaw.account.html.md#falaw.account.health_check).   |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|

### *class* falaw.account.AccountStatus(ok, locked, unauthorized, status_code, detail, url, error)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of [`health_check()`](_autosummary/falaw.account.html.md#falaw.account.health_check).

#### detail *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Server-supplied detail string (best-effort), or ‘’ if unavailable.

#### error *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`repr` of the underlying exception when `ok` is False
and the failure isn’t a recognized lock/unauth (e.g. network error).

#### locked *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff the response indicated a locked / unverified account.

#### message_for_user()

Single human-readable line explaining the status. Stable for logging.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff the account responded without an auth/lock error.

#### status_code *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)*

HTTP status from the probe (None if no HTTP exchange happened).

#### unauthorized *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff credentials are missing or invalid.

#### url *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

If actionable (e.g. billing dashboard), URL the user should visit.

### falaw.account.health_check(, probe_url='https://rest.fal.ai/storage/auth/token?storage_type=fal-cdn-v3', timeout_s=10.0, api_key=None)

Probe the fal storage endpoint and return a typed [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus).

* **Parameters:**
  * **probe_url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Endpoint to probe. Default is the storage-auth-token endpoint
    `fal_client` uses internally; small, idempotent, requires auth.
  * **timeout_s** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – Network timeout.
  * **api_key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Optional override of `FAL_KEY`. Falls back to a key bound with
    [`falaw.using_fal_credentials()`](_autosummary/falaw.html.md#falaw.using_fal_credentials), then the env var.
* **Return type:**
  [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus)
* **Returns:**
  [`AccountStatus`](_autosummary/falaw.account.html.md#falaw.account.AccountStatus) with `ok=True` when the probe succeeded.


# _autosummary/falaw.backends.html.md

# falaw.backends

Execution backend registry — where `CallPlan.backend` dispatches (falaw#15).

Until this module existed, “make the vendor call” meant one hardcoded path:
`falaw.core.call_fal`. That is a budget bypass waiting to happen — the
video_gen research programme’s decisions of record (§3.2) put it plainly:
 *“Do not write a ComfyUI Transform before this lands. A flavor without a
priceable Plan is a budget bypass.”* A backend reachable only by overriding
`Transform.execute` wholesale returns `Plan(calls=())`, so
`Plan.total_cost_usd` reads $0.00 on a real spend and every downstream
consumer of `Plan` — the cache, the dry-run converter, the cost gate — never
sees the call happen.

The fix is the smallest one that closes that gap: `CallPlan` gains a
`backend: str` field (default [`falaw.canonical.DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND), i.e.
`"fal"`), and `execute_isolated` resolves it through this module’s `backends`
registry instead of calling `call_fal` directly. Everything else about a
`CallPlan` — its cost estimate, its cache key, its artifact conversion —
stays exactly what it already is, because the executor’s contract is
narrow: turn `(application, arguments)` into the vendor’s *raw* response,
in the same shape [`falaw.core.call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) already returns. Converting
that raw response into a `lacing.Artifact` remains `execute`’s job, uniform
across backends.

Nothing above `falaw.Plan` may know a specific backend exists. A module that
imports a backend type, references a vendor node name, or assumes a
backend’s execution semantics is an architecture bug regardless of whether
it works — this registry is the one seam allowed to know backends exist at
all.

Register a second backend with `backends.register(name, executor)`, then
build `CallPlan(..., backend=name)`. See
`docs/comfyui_integration_plan.md` (video_gen research programme) for the
two-tier ruling this registry is the executor half of: a `nw.Transform`
comfy *flavor* is the graph **template** and never opens a socket; a
`GraphExecutionBackend` registered here is the **executor**.

### Module Attributes

| [`BackendExecutor`](_autosummary/falaw.backends.html.md#falaw.backends.BackendExecutor)   | `executor(application, arguments, *, on_event=None) -> dict` — the same shape [`falaw.core.call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) returns: the vendor's raw response, ready for the existing artifact converter.   |
|--------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`backends`](_autosummary/falaw.backends.html.md#falaw.backends.backends)          | Public registry of execution backends, keyed by `CallPlan.backend`.                                                                                                                                                                                       |

### Functions

| [`get_backend_executor`](_autosummary/falaw.backends.html.md#falaw.backends.get_backend_executor)(name)   | Look up a backend executor by name; raises with the known names.   |
|-------------------------------------------------------------------------------|--------------------------------------------------------------------|

### falaw.backends.BackendExecutor

`executor(application, arguments, *, on_event=None) -> dict` — the same
shape [`falaw.core.call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) returns: the vendor’s raw response, ready
for the existing artifact converter. An executor’s only job is \*making the
call\*; it must not attempt caching (the registry sits below the cache) or
artifact conversion (`execute` owns that, uniformly across backends).

alias of `Callable`[[…], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.backends.backends *: Registry* *= <Registry falaw.backends>*

Public registry of execution backends, keyed by `CallPlan.backend`.
`on_conflict="error"` so a misconfigured plugin fails loudly instead of
silently shadowing the built-in fal executor.

### falaw.backends.get_backend_executor(name)

Look up a backend executor by name; raises with the known names.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]


# _autosummary/falaw.base.html.md

# falaw.base

Core types: ToolSpec and ModelRecord.

These dataclasses are the single source of truth from which every external
surface (Claude skill, MCP server, HTTP service, UI) is derived.

### Module Attributes

| [`TABLE_VERSION_CHARS`](_autosummary/falaw.base.html.md#falaw.base.TABLE_VERSION_CHARS)   | Hex characters of the digest [`data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version) returns.   |
|------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|

### Functions

| [`data_table_version`](_autosummary/falaw.base.html.md#falaw.base.data_table_version)(path)   | Short content digest naming the version of a committed data table.   |
|-----------------------------------------------------------------------------|----------------------------------------------------------------------|

### Classes

| [`CostEstimate`](_autosummary/falaw.base.html.md#falaw.base.CostEstimate)(\*, kind, amount[, currency, ...])   | Quantitative cost of one fal call against this model.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------|
| [`ModelRecord`](_autosummary/falaw.base.html.md#falaw.base.ModelRecord)(\*, id, category[, description, ...]) | One entry in the fal model catalog.                     |
| [`ToolSpec`](_autosummary/falaw.base.html.md#falaw.base.ToolSpec)(\*, name, description, func[, ...])      | Single source of truth for a tool exposed by falaw.     |

### *class* falaw.base.CostEstimate(, kind, amount, currency='USD', notes='', source='approximate')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Quantitative cost of one fal call against this model.

#### kind

How the price scales. `per_call` is the simplest:
constant per invocation regardless of size. `per_second`
applies to video / TTS where output length matters.
`per_image` covers batch image gen. `per_megapixel`
covers high-res image generation. `per_token` covers
LLM-style endpoints.

#### amount

USD (or `currency`) per unit defined by `kind`.

#### currency

ISO 4217 code. `"USD"` is the only supported value
today; the field exists so we can extend without a schema
migration.

#### notes

Free-form caveats — “rounded up to next second”, etc.

#### source

How the estimate was obtained — `"docs"`,
`"empirical"`, `"approximate"`. Lets us flag stale or
unverified entries in audits.

### *class* falaw.base.ModelRecord(\*, id, category, description='', aliases=(), quality_tier='', cost_hint='', cost_estimate=None, docs_url='', max_clip_seconds=None, single_character_recommended=False, supported_resolutions=(), default_negative_prompt='', max_prompt_chars=None, size_param='image_size', param_specs=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One entry in the fal model catalog.

#### default_negative_prompt *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Quality/realism negatives worth appending by default (e.g. to avoid the
plastic-skin look). Empty when none.

#### max_clip_seconds *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Practical max length of a single generated clip, in seconds (e.g. ~10
for Seedance). Drives the “this shot is too long, split it” warning.

#### max_prompt_chars *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Hard cap on `prompt` length, from the model’s OpenAPI schema. Calls
over it fail at fal with an HTTP 422 after queueing; falaw raises first.

#### param_specs *: [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

Discoverable per-model parameters that matter for choosing well, as
`{name: {"enum": [...], "default": ...}}` — e.g. Recraft’s `style`,
whose photoreal default silently overrides a woodcut prompt. See
[`falaw.model_params()`](_autosummary/falaw.html.md#falaw.model_params).

#### single_character_recommended *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the model handles a single character per shot far better than
multiple interacting ones — drives the “two characters, consider
shot/reverse-shot” warning.

#### size_param *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"image_size"` (fal’s named
presets, the default) or `"aspect_ratio"` (+ `resolution`).

* **Type:**
  Which argument carries the output size

#### supported_resolutions *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Resolutions the model offers, cheap→expensive (e.g. (“720p”, “1080p”)).

### falaw.base.TABLE_VERSION_CHARS *= 12*

Hex characters of the digest [`data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version) returns.

### *class* falaw.base.ToolSpec(\*, name, description, func, input_schema=<factory>, output_schema=<factory>, tags=(), examples=(), version='0.0.1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Single source of truth for a tool exposed by falaw.

A ToolSpec is what the registry stores. Bridges read it to produce
Claude-skill instructions, MCP tool descriptors, HTTP endpoints, etc.

### falaw.base.data_table_version(path)

Short content digest naming the version of a committed data table.

The one definition for every table falaw ships (`models.json`,
`llm_rates.json`), so a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) stamped by one is
comparable with a basis stamped by another.

Deliberately the *bytes*, not a declared `version` field: 0.0.46 moved
every premium LLM row’s price tenfold without touching one, and that drift
is exactly what a persisted quote must be able to detect. A digest cannot
forget to be bumped.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/falaw.bridges.html.md

# falaw.bridges

Bridges: derive auxiliary surfaces (skill, MCP, HTTP service) from the tool registry.

Each bridge is a thin adapter over `falaw.registry.list_tools()`. The point
is that adding a new surface (e.g. a CLI, a Slack bot, a TUI) is a new
bridge module — never a re-implementation of the operations themselves.

### Modules

| [`mcp`](_autosummary/falaw.bridges.mcp.html.md#module-falaw.bridges.mcp)         | MCP bridge: expose every falaw tool as an MCP tool.                     |
|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`service`](_autosummary/falaw.bridges.service.html.md#module-falaw.bridges.service) | HTTP service bridge (planned): expose falaw tools as a `qh` app.        |
| [`skill`](_autosummary/falaw.bridges.skill.html.md#module-falaw.bridges.skill)     | Render a Claude SKILL.md (and references) from the falaw tool registry. |


# _autosummary/falaw.bridges.mcp.html.md

# falaw.bridges.mcp

MCP bridge: expose every falaw tool as an MCP tool.

Like `bridges/skill.py`, this is a thin adapter over
`falaw.registry.list_tools()` — adding a new falaw tool automatically adds
an MCP tool, no edit here. Built on `fastmcp` (the federation’s MCP library;
see `lookbook/mcp.py` for the canonical pattern).

Two entry points:

- [`register_tools()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.register_tools) — add falaw’s tools to an *existing* `FastMCP`
  server. This is what an **aggregating** server (reelee’s MCP server, which
  composes falaw + nw + artful + lacing) calls.
- [`build_mcp_server()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.build_mcp_server) — build a *standalone* `FastMCP` server exposing
  just falaw. [`serve()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.serve) runs it over stdio.

The server’s `instructions` reach the model before any tool call, so they are
behaviour-driving text and are guarded like code:
[`unresolved_tool_references()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.unresolved_tool_references) reports every tool name or `name_*` family
a piece of prose names that the registry does not hold. An aggregating server
should run it over its own composed preamble too.

`fastmcp` is an optional dependency: `pip install falaw[mcp]`.

### Module Attributes

| [`FREE_TAG`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.FREE_TAG)     | Tag marking a tool that makes **no** vendor call — docs/registry upkeep only.                                   |
|---------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| [`COSTED_TOOLS`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.COSTED_TOOLS) | Snapshot of [`costed_tools()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.costed_tools) for consumers that want a constant. |

### Functions

| [`build_mcp_server`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.build_mcp_server)(\*[, name, tools])                | Build a standalone `FastMCP` server exposing every falaw tool.      |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`costed_tools`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.costed_tools)()                                     | Names of the falaw tools that spend money — the metering SSOT.      |
| [`register_tools`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.register_tools)(server, \*[, tools, prefix])        | Register every falaw `ToolSpec` on `server` (a `FastMCP`).          |
| [`serve`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.serve)(\*[, transport])                             | Run the standalone falaw MCP server.                                |
| [`unresolved_tool_references`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.unresolved_tool_references)(text, \*[, tool_names]) | Tool names and `name_*` families named in `text` that do not exist. |

### falaw.bridges.mcp.COSTED_TOOLS *= ('animate_face', 'apply_note_to_beat', 'apply_note_to_scene', 'cast_character', 'cast_voice', 'composite_character_in_environment', 'edit_image', 'establish_environment', 'generate_audio', 'generate_image', 'generate_image_with_refs', 'image_to_video', 'lipsync', 'llm_complete', 'parse_screenplay', 'remove_background', 'render_beat', 'render_scene', 'render_shot', 'storyboard_shot', 'talking_avatar_from_text', 'text_to_speech', 'text_to_video', 'upscale_image', 'voice_clone')*

Snapshot of [`costed_tools()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.costed_tools) for consumers that want a constant. Prefer
the function when tools may be registered after import.

### falaw.bridges.mcp.FREE_TAG *= 'maintenance'*

Tag marking a tool that makes **no** vendor call — docs/registry upkeep only.
Everything else reaches fal.ai or an LLM and therefore spends money.

### falaw.bridges.mcp.build_mcp_server(, name='falaw', tools=None)

Build a standalone `FastMCP` server exposing every falaw tool.

Raises `ImportError` with an install hint if `fastmcp` is missing.

### falaw.bridges.mcp.costed_tools()

Names of the falaw tools that spend money — the metering SSOT.

Every genre package in the federation exposes this so an aggregating
connector can gate spend from one source of truth per package
(`braidio.mcp.COSTED_TOOLS`, `muvid.mcp.COSTED_TOOLS`). falaw had
none, so its 24 money-spending tools — the *raw* fal.ai operations,
including `text_to_video` and `render_scene` — reached an aggregating
connector completely unmetered (thorwhalen/reelee#265).

**Derived from the registry, not hand-listed**, matching this module’s
existing contract that adding a falaw tool needs no edit here. The
consequence is the safe one: a newly-registered tool is \*\*costed by
default\*\* and must be explicitly tagged [`FREE_TAG`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.FREE_TAG) to be exempt.
Forgetting the tag over-meters a free tool (annoying); the inverse would
let a new money-spender through a credit cap (a billing hole).

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### falaw.bridges.mcp.register_tools(server, , tools=None, prefix='')

Register every falaw `ToolSpec` on `server` (a `FastMCP`).

* **Parameters:**
  * **server** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – A `fastmcp.FastMCP` instance (or anything with a compatible
    `.tool(fn, *, name, description, tags)` method).
  * **tools** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`ToolSpec`](_autosummary/falaw.base.html.md#falaw.base.ToolSpec)]]) – The tools to register; defaults to the whole falaw registry.
  * **prefix** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Optional name prefix (e.g. `"falaw_"`) so an aggregating
    server can namespace falaw’s tools away from another package’s.
* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  The MCP tool names registered (in registry order).

### falaw.bridges.mcp.serve(, transport='stdio', \*\*kwargs)

Run the standalone falaw MCP server.

stdio is the default transport — what Claude Desktop and the Anthropic SDK
expect when launching an MCP server as a subprocess.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.bridges.mcp.unresolved_tool_references(text, , tool_names=None)

Tool names and `name_*` families named in `text` that do not exist.

Behaviour-driving prose — the server `instructions`, a tool description,
an aggregating connector’s composed preamble — names tools, and a call-site
sweep cannot see prose. Nothing derives one from the other, so nothing
failed when `_INSTRUCTIONS` spent months telling every model to
“use the `plan_*` tools to inspect cost before spending” while the
registry held no such tool (thorwhalen/falaw#47). This is the check that
now fails instead.

* **Parameters:**
  * **text** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The prose to scan.
  * **tool_names** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – The names that count as existing; defaults to the falaw
    registry. An aggregating server that registered falaw under a
    prefix should pass what [`register_tools()`](_autosummary/falaw.bridges.mcp.html.md#falaw.bridges.mcp.register_tools) returned, so
    `falaw_generate_image` resolves.
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]
* **Returns:**
  The unresolved references, deduplicated, in order of first appearance.

```pycon
>>> unresolved_tool_references("Use the refresh_* tools.")
()
>>> unresolved_tool_references("Use the plan_* tools to inspect cost.")
('plan_*',)
```


# _autosummary/falaw.bridges.service.html.md

# falaw.bridges.service

HTTP service bridge (planned): expose falaw tools as a `qh` app.

The plan: feed `falaw.registry.list_tools()` callables to `qh.mk_app` so
each tool becomes an HTTP endpoint. Same SSOT, new surface — no rewrite
of the operations themselves.

### Functions

| `build_qh_app`(\*args, \*\*kwargs)   |    |
|--------------------------------------|----|


# _autosummary/falaw.bridges.skill.html.md

# falaw.bridges.skill

Render a Claude SKILL.md (and references) from the falaw tool registry.

The skill is the entry point that teaches Claude Code how to use falaw. It
is generated — never hand-edited at the install location — so adding a
new tool automatically updates the skill the next time we run
`write_skill_files()`.

### Functions

| [`build_skill_md`](_autosummary/falaw.bridges.skill.html.md#falaw.bridges.skill.build_skill_md)([tools])       | Render the SKILL.md content from the tool registry.                 |
|--------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`write_skill_files`](_autosummary/falaw.bridges.skill.html.md#falaw.bridges.skill.write_skill_files)(target_dir) | Write SKILL.md (and a small references/ folder) under `target_dir`. |

### falaw.bridges.skill.build_skill_md(tools=None)

Render the SKILL.md content from the tool registry.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.bridges.skill.write_skill_files(target_dir)

Write SKILL.md (and a small references/ folder) under `target_dir`.

Returns the path written.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/falaw.cache.html.md

# falaw.cache

Content-addressed cache for fal calls.

Why this exists: the directorial workflow (“a single edit, the scene
re-renders around it”) only works if unchanged beats *don’t* re-render.
A cache hit is the difference between a 30-second edit-and-preview
loop and a 5-minute one.

Design:

* Key = SHA256 of (model_id, sorted(arguments_json)). Two structurally
  identical fal calls collapse to the same key.
* Value = the raw fal response, plus optional locally-downloaded asset
  paths. We persist both the JSON manifest and the binary downloads.
* Cache root: `$FALAW_CACHE_DIR` or `$FALAW_DATA_DIR/cache` or
  `~/.config/falaw/cache`.
* The cache is *per-process* aware via lru_cache for hot lookups;
  on-disk for persistence across runs.

**The key arguments are not always the wire arguments.** A chained plan
(`generate_image` → `image_to_video`) sends fal a *URL* for the upstream
image, but a URL is minted fresh per upload — so keying on it guarantees a
miss the moment the upstream is genuinely re-executed, and the miss re-bills
the expensive downstream call for unchanged work. [`cached_call_fal()`](_autosummary/falaw.cache.html.md#falaw.cache.cached_call_fal)
therefore takes an optional `key_arguments`: the same call with upstream
references resolved to **content hashes** (`sha256:<hex>`) instead of URLs.
See [`falaw.content`](_autosummary/falaw.content.html.md#module-falaw.content) for how those hashes are produced.

Usage:

> from falaw.cache import cached_call_fal
> raw = cached_call_fal(“fal-ai/flux/dev”, {“prompt”: “…”})

The wrapped variant has the same signature as `core.call_fal` but skips
the network when the (model_id, arguments) tuple was seen before.

### Module Attributes

| [`ASSETS_DIRNAME`](_autosummary/falaw.cache.html.md#falaw.cache.ASSETS_DIRNAME)   | Sub-directory of the falaw cache holding [`materialize_asset()`](_autosummary/falaw.cache.html.md#falaw.cache.materialize_asset) copies.   |
|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`cache_get`](_autosummary/falaw.cache.html.md#falaw.cache.cache_get)(application, arguments, \*[, ...])       | Return the raw fal response if cached, else None.                       |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`cache_put`](_autosummary/falaw.cache.html.md#falaw.cache.cache_put)(application, arguments, raw, \*[, ...])  | Persist a fal response.                                                 |
| [`cache_stats`](_autosummary/falaw.cache.html.md#falaw.cache.cache_stats)()                                      | Quick summary of the cache: entry count, disk usage, and where it went. |
| [`cached_call_fal`](_autosummary/falaw.cache.html.md#falaw.cache.cached_call_fal)(application, arguments, \*[, ...]) | Call a fal model, but reuse the cached response when present.           |
| [`drop_cache_entry`](_autosummary/falaw.cache.html.md#falaw.cache.drop_cache_entry)(application, arguments, \*)       | Delete the cache entry for `(application, arguments)`.                  |
| [`emit_cache_hit`](_autosummary/falaw.cache.html.md#falaw.cache.emit_cache_hit)(application[, on_event])            | Emit the synthetic `cache_hit` progress event for `application`.        |
| [`materialize_asset`](_autosummary/falaw.cache.html.md#falaw.cache.materialize_asset)(url, \*[, key_hint, store, ...]) | Download a remote asset to the cache and return the local path.         |

### falaw.cache.ASSETS_DIRNAME *= 'assets'*

Sub-directory of the falaw cache holding [`materialize_asset()`](_autosummary/falaw.cache.html.md#falaw.cache.materialize_asset) copies.

Named here rather than in [`falaw.prune`](_autosummary/falaw.prune.html.md#module-falaw.prune) because `_asset_path()` is
what decides the layout; the prune side reads this so the two cannot drift.

### falaw.cache.cache_get(application, arguments, , backend='fal', key_extra=None)

Return the raw fal response if cached, else None.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.cache.cache_put(application, arguments, raw, , note='', wire_arguments=None, backend='fal', key_extra=None)

Persist a fal response. Returns the entry directory path.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id.
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – the arguments the entry is **keyed** on.
  * **raw** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – the fal response to store.
  * **note** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – free-form label recorded in the manifest.
  * **wire_arguments** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – the arguments actually sent to fal, when they differ
    from the key arguments (chained calls send URLs but are keyed on
    content hashes). Recorded for debugging only — it never affects
    the key.
  * **backend** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – which execution backend produced `raw` (falaw#15). Joins
    the cache key only when non-default, same rule as
    [`falaw.canonical.cache_key_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload); recorded in the
    manifest under the same condition, for debugging.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

The manifest is written to a temporary file and moved into place with
[`os.replace()`](https://docs.python.org/3/library/os.html#os.replace), so a reader never sees a half-written entry. That is not
hypothetical since `execute_plan(concurrency=N)`: two calls of one Plan
that are structurally identical land on the same key, and an interleaved
`json.dump` would leave a permanently unparseable entry — a cache that
poisons itself under exactly the fan-out it exists to make cheap.

### falaw.cache.cache_stats()

Quick summary of the cache: entry count, disk usage, and where it went.

`areas` is the part worth reading. Since falaw#14 the cache holds the
*bytes* of every generated asset, so a total on its own cannot distinguish
gigabytes of irreplaceable blobs from gigabytes of `assets/` copies that
cost nothing to regenerate — and only the second is safe to reclaim without
thinking. Each area’s economics, and the primitives that reclaim it, are in
[`falaw.prune`](_autosummary/falaw.prune.html.md#module-falaw.prune).

`size_bytes` is every byte under the cache root, unchanged in meaning from
before the breakdown existed: the `other` area absorbs whatever the named
areas do not claim, so the areas always sum to the whole.

(`+SKIP`ed — it reads the caller’s real cache, a multi-gigabyte walk on
the production box. `tests.test_prune` pins this against a throwaway
cache instead.)

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> stats = cache_stats()
>>> sorted(stats["areas"])
['assets', 'content', 'manifests', 'other', 'scenes', 'url_index']
```

### falaw.cache.cached_call_fal(application, arguments, , key_arguments=None, refresh=False, on_event=None, backend='fal', key_extra=None)

Call a fal model, but reuse the cached response when present.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id.
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – model input dict — what is sent **on the wire**.
  * **key_arguments** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – what the cache entry is **keyed** on, when that differs
    from what goes on the wire. Defaults to `arguments`. The split
    exists because a chained call must send fal an expiring URL for its
    upstream input while being keyed on that input’s *content hash*, so
    a byte-identical upstream regeneration hits instead of re-billing.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – if True, bypass the cache and overwrite it with a fresh result.
  * **on_event** – Per-call subscriber for [`falaw.events.ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent).
    On a cache hit, a synthetic `cache_hit` event is emitted so
    UIs can show “skipped” instead of “running”.
  * **backend** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – which execution backend serves this call (falaw#15) —
    resolved via [`falaw.backends`](_autosummary/falaw.backends.html.md#module-falaw.backends). Also joins the cache key
    (non-default only), so two backends never share an entry. Despite
    the name, this function is no longer fal-specific; the name is
    kept because “fal” is still the only backend and every existing
    call site already spells it this way.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  Raw response (whether from cache or network).

### falaw.cache.drop_cache_entry(application, arguments, , backend='fal', key_extra=None)

Delete the cache entry for `(application, arguments)`. Returns whether one existed.

The counterpart to [`cache_put()`](_autosummary/falaw.cache.html.md#falaw.cache.cache_put), and the mechanism that keeps a cache
from becoming a *trap*. An entry whose response can no longer be turned
into a usable artifact — fal deleted the URL and the bytes are not in the
content store — must be a **miss**, not a permanent failure: without this,
escaping one dead call means re-running the whole plan.
[`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) calls it.

The plan-level escapes are `execute_plan(plan, refresh=True)`, which
re-runs every call and **keeps** what it pays for, and
`execute_plan(plan, use_cache=False)`, which re-runs and keeps nothing —
so the second one bills again on the next run (falaw#49). Neither is a
substitute for this function: both re-bill *every* call in the plan, and
this drops the one entry that actually went bad.

Only the manifest is removed. Blobs in the content store are shared by
content hash across entries and are never dropped from here.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### falaw.cache.emit_cache_hit(application, on_event=None)

Emit the synthetic `cache_hit` progress event for `application`.

Shared by [`cached_call_fal()`](_autosummary/falaw.cache.html.md#falaw.cache.cached_call_fal) and [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) (which
drives the cache directly, because it must decide whether a hit is
*usable* before committing to it) so a UI sees one event shape either way.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.cache.materialize_asset(url, , key_hint='', store=None, fetcher=None, refresh=False)

Download a remote asset to the cache and return the local path.

The local filename is content-addressed by the asset’s **bytes**, so two
URLs serving identical bytes resolve to one file. The extension is a
presentational hint for ffmpeg/PIL; the SHA-256 is the address.

Repeat calls are cheap, in three widening circles — this is what makes it
safe to call from a loop over 200 shots:

1. the file is already on disk here (no store lookup, no network) —
   **immutable URLs only**, see below;
2. the bytes are in the content store (no network, or one validating
   round-trip) — so it still works after fal has expired the URL;
3. otherwise, one download.

Circle 1 matters on its own: the content store is prunable, so an asset
can survive as a materialized file after its blob is gone.

**Circle 1 is taken only when the URL cannot change** — that is,
[`falaw.content.is_immutable_url()`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url) — because reaching it requires
trusting the `url -> hash` index to name the *current* bytes, and for an
arbitrary caller-supplied URL it does not (thorwhalen/falaw#23). A mutable
URL goes to circle 2, where [`falaw.content.content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url)
revalidates before reusing anything; when the bytes really are unchanged
that costs one conditional request and still no download, and when they
have changed you get the new file instead of silently getting the old one.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – the remote asset URL. `file://` is supported and is used
    deliberately by downstream packages for locally-rendered media.
  * **key_hint** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – optional human-readable filename prefix.
  * **store** – injected `lacing.ArtifactStore`; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store).
  * **fetcher** – injected byte source (`url -> Iterable[bytes]`); defaults to
    the `urllib`-based one. The seam for custom transport (auth
    headers, retries) and for a hermetic test suite.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – re-fetch unconditionally, skipping every circle. Rarely needed
    now: a mutable URL revalidates on its own (falaw#23), so this is for
    an origin that lies about its validators.
* **Raises:**
  [**FalAssetFetchError**](_autosummary/falaw.errors.html.md#falaw.errors.FalAssetFetchError) – the bytes could not be retrieved.
      Unlike a generated-media artifact (which degrades to URL-only —
      see [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute)), there is nothing to degrade to
      here: the caller asked for a local file.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/falaw.canonical.html.md

# falaw.canonical

One canonical byte-form for everything falaw hashes (falaw#17).

falaw derives money-bearing identities from JSON: the per-call cache key
(`falaw.cache._key()`), the whole-plan idempotency handle
([`falaw.plan.plan_hash()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_hash)), and the dry-run artifact id
(`falaw.plan._synthetic_artifact`). Before this module each site called
`json.dumps(..., sort_keys=True, default=str)` — and `default=str` is a
**give-up-quietly branch inside a key-composition function**, which fails in
both directions at once:

* two structurally different values whose `str()` coincide collapse to one
  key — `Decimal("0.5")` vs `"0.5"`, `Path("/a/b")` vs `"/a/b"` — a
  silent *wrong hit*: the caller is handed someone else’s artifact and billed
  it as a saving;
* an object without a stable `str()` (the default `<Ref object at 0x...>`
  repr) mints a fresh key per instance — a *permanent miss* plus unbounded
  cache growth.

JSON’s silent key coercion has the same collision property from the other
side: `{1: "x"}` and `{"1": "x"}` dump to identical bytes, so two
different argument dicts share a key. (`lacing` rejects the same case in
its digests, for the same reason.)

The rule this module enforces: \*\*a key-composition function must never have a
“give up quietly” branch.\*\* [`canonical_blob()`](_autosummary/falaw.canonical.html.md#falaw.canonical.canonical_blob) serializes with
`sort_keys=True`, **no** `default`, `allow_nan=False`, and string-only
mapping keys — anything else raises [`falaw.errors.FalNonCanonicalArgument`](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument)
naming the offending path and type, so the failure is a diagnosable refusal at
plan time rather than a wrong artifact at collect time.

Two payload projections live here **side by side, on purpose**:

* [`cache_key_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload) — `{app, args}`, the per-call content-addressed
  cache key;
* [`plan_identity_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload) — `{app, args, tool}`, the structural
  idempotency form shared by `plan_hash` and the dry-run artifact id.

The field sets differ deliberately (`tool` is a falaw-side label; the
underlying fal call is the same whatever the tool was called), but a field
that changes *what the call produces* must go into **both** — in this module,
in one commit. `backend` (falaw#15’s `CallPlan.backend`) is exactly that
field: both projections accept it, and it lands in the hashed payload
whenever it is not [`DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND) — omitted, not merely equal, when it
is. Two consequences, both load-bearing:

* a backend added to `plan_hash` but not the cache key would let one
  backend return another backend’s artifact as a wrong hit — the reason the
  two projections take the parameter together rather than one growing it
  first;
* omitting the field for the default backend means every call falaw has ever
  planned or cached — all of them `"fal"` today — keeps its **exact**
  pre-#15 digest. A future non-default backend hashes under a genuinely
  different key (correctly — it *is* a different call), without silently
  invalidating the deployed fal cache the day this ships.

Byte-compatibility: for JSON-native payloads the output of
[`canonical_blob()`](_autosummary/falaw.canonical.html.md#falaw.canonical.canonical_blob) is identical to the old form, so every cache entry
whose arguments were already JSON-native keeps its key. Invalidated —
deliberately, because their keys were unreliable or unportable — are entries
whose arguments needed any of JSON’s laxities: the `default=str`
stringification, a bare `NaN`/`Infinity` (previously serialized natively),
or silently-coerced non-string mapping keys. `tests/test_canonical.py` pins
the stability half with literal digests.

### Module Attributes

| [`DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND)   | The only execution backend falaw ships until a second one lands (falaw#15).   |
|-----------------------------------------------------------------|-------------------------------------------------------------------------------|

### Functions

| [`canonical_blob`](_autosummary/falaw.canonical.html.md#falaw.canonical.canonical_blob)(payload)                        | The one byte-form falaw hashes: sorted keys, no fallback, no NaN.                         |
|-------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`ensure_canonical`](_autosummary/falaw.canonical.html.md#falaw.canonical.ensure_canonical)(payload, \*[, context])       | Raise `FalNonCanonicalArgument` unless `payload` is canonical JSON.                       |
| [`cache_key_payload`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload)(application, arguments, \*)  | `{app, args}` — what the per-call content-addressed cache keys on.                        |
| [`plan_identity_payload`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload)(application, ...[, ...]) | `{app, args, tool}` — the structural form behind `plan_hash` and the dry-run artifact id. |

### falaw.canonical.DFLT_BACKEND *= 'fal'*

The only execution backend falaw ships until a second one lands
(falaw#15). The single source for two defaults that must agree:
`CallPlan.backend`’s field default, and the value [`cache_key_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload)
/ [`plan_identity_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload) omit from the hashed payload.

### falaw.canonical.cache_key_payload(application, arguments, , backend='fal', key_extra=None)

`{app, args}` — what the per-call content-addressed cache keys on.

No `tool`: two CallPlans differing only in falaw-side labelling make the
same fal call and *should* share a cache entry. `backend` joins the
payload only when it is not [`DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND) — see the module
docstring for why that asymmetry, not unconditional inclusion, is the
safe choice. A field that changes what the call **produces** must be
added here AND in [`plan_identity_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload).

`key_extra` joins under the same omit-if-empty rule: identity a caller
declares beyond the wire arguments (nw#27’s Transform `impl_version` is
the first customer — “same interface, changed behaviour” must miss the
cache without renaming anything). Empty means absent, so every key ever
issued without one is unchanged.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.canonical.canonical_blob(payload)

The one byte-form falaw hashes: sorted keys, no fallback, no NaN.

Raises `FalNonCanonicalArgument` for anything JSON cannot represent
faithfully — there is deliberately no `default=` escape hatch, because a
hashing function that guesses is a hashing function that collides.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### falaw.canonical.ensure_canonical(payload, , context='arguments')

Raise `FalNonCanonicalArgument` unless `payload` is canonical JSON.

Canonical means: `str` / `bool` / `int` / finite `float` / `None`
scalars, `list` / `tuple` sequences, and mappings with **string** keys,
recursively. Call this at a boundary (`make_call_plan`,
`cached_call_fal`) so the refusal happens while it is still free —
before any network call, not on the way to one.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.canonical.plan_identity_payload(application, arguments, , tool, backend='fal', key_extra=None)

`{app, args, tool}` — the structural form behind `plan_hash` and
the dry-run artifact id.

Includes `tool` so a re-plan of the same request is recognizable as the
same *plan* even though the cache would treat the calls identically.
`backend` and `key_extra` join the payload under the same
omit-if-default rule as [`cache_key_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload); keep the two functions
adjacent so adding a field to one is an explicit decision about the other.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.content.html.md

# falaw.content

Content addressing for fal-produced media — identify a file by *what it is*.

fal serves generated media from a CDN URL, and fal’s own documentation is
explicit that a URL carries neither identity nor durability:

> “Each upload produces a unique URL with no shared namespace.”

> “Expired files are permanently deleted and cannot be recovered.”

So a URL is the wrong handle for the two jobs falaw needs done:

1. **Naming an artifact.** `lacing.Artifact.asset_id` is contractually the
   SHA-256 of the artifact’s *bytes* — “two artifacts with the same
   `asset_id` are byte-identical regardless of where they live”. Hashing the
   URL instead produces a different id for two byte-identical renders and
   silently breaks every consumer that trusts that contract.
2. **Keying a cache.** A byte-identical upstream regeneration must produce a
   downstream cache **hit**. With a URL in the key it produces a miss, and the
   miss re-bills a $0.35–$1.50 clip for work that did not change.

This module is the single place falaw turns a URL into a content hash. It

* streams the bytes into a `lacing.ArtifactStore` blob store — which is
  content-addressed, `dol`-backed and swappable (in-memory / directory /
  object store) — rather than growing a second blob store of its own;
* remembers `url -> (content_hash, bytes_size, validators)` in a small
  on-disk index, so re-executing an already-cached plan does **not** re-download
  anything. The index is a *hint*, never an identity, and it is only \*\*sound for
  immutable URLs\*\* — so falaw decides, per URL, whether it may be trusted
  (thorwhalen/falaw#23):
  - a **fal-served** URL is trusted outright, because fal guarantees a URL is
    minted per upload and therefore never re-points at different bytes;
  - **anything else** — an arbitrary caller-supplied `https://…/reference.png`
    reached through [`falaw.cache.materialize_asset()`](_autosummary/falaw.cache.html.md#falaw.cache.materialize_asset), or a `file://` clip
    that gets re-rendered to the same path — is **revalidated** before reuse: a
    conditional request replaying the recorded `ETag` / `Last-Modified`, or
    a `(mtime, size)` check for a local file. `304` costs a round-trip and
    no payload; a changed asset yields a changed content hash, as it must.

  When falaw *cannot* check — no validators recorded, or an injected transport
  with no conditional-request support — it re-fetches rather than trusting. An
  unverifiable hint is not evidence. [`is_immutable_url()`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url) is the predicate,
  and `IMMUTABLE_URL_HOSTS` is where you add a host you mint yourself;
* serves the bytes from the store after the URL has expired, so a months-old
  cache hit still yields a usable artifact instead of a dead link.

The blob store is **injected**, never constructed inline by callers:
[`default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store) is the falaw-cache-rooted default, and every
public function here takes a `store` keyword so a caller can point falaw at
an S3-backed store without touching any other code.

So is the **transport**. Per call it is the `fetcher=` / `asset_fetcher=`
argument; for a whole process it is [`using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher), which covers
[`content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url), [`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset),
[`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan) and [`falaw.execute_plan_isolated()`](_autosummary/falaw.html.md#falaw.execute_plan_isolated) in one
place. That matters most to *downstream* test suites: falaw content-addresses
every media result, so a suite that stubs the fal response but not the
transport resolves its made-up URLs for real — and stays green while doing it,
because a failed fetch degrades to a URL-only artifact with a warning rather
than raising. [`falaw.testing`](_autosummary/falaw.testing.html.md#module-falaw.testing) ships the fake so no consumer has to write
one.

### Examples

```pycon
>>> from lacing import ArtifactStore
>>> store = ArtifactStore.in_memory()
>>> ref = content_ref_for_url(
...     "https://fal.media/files/one.png",
...     store=store,
...     fetcher=lambda url: [b"pretend-", b"png-bytes"],
... )
>>> ref.bytes_size
17
>>> ref.content_hash == __import__("hashlib").sha256(b"pretend-png-bytes").hexdigest()
True
```

The same bytes served at a *different* URL yield the same reference — which is
the whole point:

```pycon
>>> other = content_ref_for_url(
...     "https://fal.media/files/two.png",
...     store=store,
...     fetcher=lambda url: [b"pretend-png-bytes"],
... )
>>> other.content_hash == ref.content_hash
True
```

### Module Attributes

| [`UrlFetcher`](_autosummary/falaw.content.html.md#falaw.content.UrlFetcher)   | A callable that yields the bytes of a URL in chunks.   |
|---------------------------------------------------------------|--------------------------------------------------------|

### Functions

| [`content_ref_for_url`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url)(url, \*[, store, ...])        | Materialize `url`'s bytes into `store` and return their content hash.                                  |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| [`default_content_store`](_autosummary/falaw.content.html.md#falaw.content.default_content_store)()                           | The falaw-cache-rooted `lacing.ArtifactStore`.                                                         |
| [`default_url_fetcher`](_autosummary/falaw.content.html.md#falaw.content.default_url_fetcher)()                             | The transport used when no `fetcher=` argument is given.                                               |
| [`is_immutable_url`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url)(url)                             | Whether `url` is guaranteed never to serve different bytes later.                                      |
| [`remembered_ref`](_autosummary/falaw.content.html.md#falaw.content.remembered_ref)(url)                               | The [`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef) previously recorded for `url`, if any. |
| [`using_url_fetcher`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher)(fetcher)                        | Make `fetcher` falaw's default asset transport for the duration.                                       |
| [`write_blob_to_file`](_autosummary/falaw.content.html.md#falaw.content.write_blob_to_file)(content_hash, path, \*[, ...]) | Materialize the blob for `content_hash` as a **copy** at `path`.                                       |

### Classes

| [`ConditionalOutcome`](_autosummary/falaw.content.html.md#falaw.content.ConditionalOutcome)(not_modified[, chunks, ...])   | The result of asking an origin whether a URL's bytes changed.           |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef)(content_hash, bytes_size)              | A content-addressed handle on some bytes falaw has materialized.        |
| [`Validators`](_autosummary/falaw.content.html.md#falaw.content.Validators)([etag, last_modified])                 | What an origin gave us to ask "are these bytes still current?" cheaply. |

### *class* falaw.content.ConditionalOutcome(not_modified, chunks=None, validators=Validators(etag='', last_modified=''))

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The result of asking an origin whether a URL’s bytes changed.

Either `not_modified` (the remembered content hash still stands, at the
cost of one round-trip and no payload), or the **new** bytes — carried here
rather than re-requested, because a caller that discovers staleness has
already paid for the response body.

### *class* falaw.content.ContentRef(content_hash, bytes_size)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A content-addressed handle on some bytes falaw has materialized.

`content_hash` is the SHA-256 hex digest of the bytes — the value
`lacing.Artifact.asset_id` is contractually required to hold, and the
value that goes into a downstream cache key in place of a URL.

### falaw.content.UrlFetcher

A callable that yields the bytes of a URL in chunks.

The injection seam for tests and for callers that need custom transport
(auth headers, retries, a local mirror). [`default_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.default_url_fetcher) resolves
the one in force; [`using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher) installs another.

alias of `Callable`[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]

### *class* falaw.content.Validators(etag='', last_modified='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What an origin gave us to ask “are these bytes still current?” cheaply.

`etag` and `last_modified` are the HTTP response headers of the same
names, replayed as `If-None-Match` / `If-Modified-Since` on the next
read. A `file://` URL has neither, so falaw synthesises an `etag` from
the file’s `(mtime_ns, size)` — see `_file_validators()`. Falsy when
the origin offered nothing, which is the case falaw must treat as
“cannot revalidate”, never as “unchanged”.

### falaw.content.content_ref_for_url(url, , store=None, fetcher=None, refresh=False, assume_immutable=None)

Materialize `url`’s bytes into `store` and return their content hash.

Idempotent and cheap on repeat, but *how* cheap depends on whether the URL
can change behind falaw’s back — and falaw decides that rather than asking
the caller to know (thorwhalen/falaw#23):

1. **Immutable URL** (fal’s own — see [`is_immutable_url()`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url)) with a
   remembered hash whose blob is present: returned immediately, \*\*no
   network\*\*. This is what makes re-executing an already-cached plan free
   rather than re-downloading every clip.
2. **Mutable URL** with a remembered hash: falaw **revalidates** — a
   conditional `GET` replaying the recorded `ETag` / `Last-Modified`
   (for `file://`, a `(mtime, size)` comparison). A `304` costs one
   round-trip and no payload; a `200` means the bytes really changed and
   the new ones are stored, so the content hash changes with them.
3. **No usable answer** — nothing remembered, no validators recorded, or a
   transport that cannot make conditional requests: a plain fetch.

Step 3 is the important default. A transport that cannot revalidate makes
falaw **re-fetch**, never trust: an unverifiable hint is not evidence.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The asset URL. `file://` is supported and treated as mutable.
  * **store** – Injected `lacing.ArtifactStore`; defaults to
    [`default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store).
  * **fetcher** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]]) – Injected byte source; defaults to [`default_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.default_url_fetcher)
    (the built-in `urllib` transport unless [`using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher)
    has installed another). To support revalidation, a custom transport
    exposes a `conditional_fetch(url, validators) -> ConditionalOutcome`
    attribute; without one it is simply never asked.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip every shortcut and re-fetch unconditionally. Rarely needed
    now that mutable URLs revalidate on their own — keep it for a URL
    whose origin lies about its validators.
  * **assume_immutable** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool)]) – Override the host-based decision. `True` restores
    the old unconditional trust (use for a host you mint yourself, and
    prefer adding it to `IMMUTABLE_URL_HOSTS`); `False` forces
    revalidation even for fal.
* **Raises:**
  [**FalAssetFetchError**](_autosummary/falaw.html.md#falaw.FalAssetFetchError) – the bytes could not be retrieved, or the response
      was empty. Never returns a reference it could not back with bytes —
      a silent zero-byte “artifact” is the failure mode this guards.
* **Return type:**
  [`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef)

### falaw.content.default_content_store()

The falaw-cache-rooted `lacing.ArtifactStore`.

Rooted at `<falaw cache dir>/content`, so it moves with
`$FALAW_CACHE_DIR` / `$FALAW_DATA_DIR` like every other piece of falaw
state. Constructed per call (the constructor only ensures directories
exist) so a test that re-points the cache dir gets a fresh store.

**Deleting a blob removes it from disk** rather than moving it to the OS
trash. `dol.Files` — the blob backend `from_directory` lays down —
trashes by default, including on Linux, which is a kind default for a
user’s own files and the wrong one here: every blob is derived data, and
the only thing in falaw that deletes one is [`falaw.prune.prune_content()`](_autosummary/falaw.prune.html.md#falaw.prune.prune_content),
whose whole job is to give the volume its space back. A trashed blob frees
nothing (the trash usually lives on the same volume) while the prune report
says it did (thorwhalen/falaw#66). The deletion is still an explicit,
`dry_run`-by-default operator act, so the trash was never the safety net.

### falaw.content.default_url_fetcher()

The transport used when no `fetcher=` argument is given.

[`using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher)’s installed fetcher if there is one, else the
built-in `urllib`-based one. Resolved at call time, so every falaw entry
point that reads asset bytes — [`content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url),
[`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset), [`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan) and
[`falaw.execute_plan_isolated()`](_autosummary/falaw.html.md#falaw.execute_plan_isolated) — honours an override installed after
they were imported.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]

### falaw.content.is_immutable_url(url)

Whether `url` is guaranteed never to serve different bytes later.

True only for hosts falaw knows mint a fresh URL per upload —
`IMMUTABLE_URL_HOSTS`, which is fal’s own CDN out of the box, and any
**subdomain** of one. **Everything else is mutable**, including `file://`:
a locally-rendered clip re-rendered to the same path is the textbook case of
one URL serving two different things.

The default is the safe one on purpose. Guessing “probably immutable” for
an unknown host is what thorwhalen/falaw#23 was: a changed input producing
an unchanged content address, silently, with the stale hash flowing into
`Artifact.asset_id` and every downstream cache key.

Two rules make this a *security* boundary rather than a string comparison,
and both were added after a review reproduced falaw#23 against the first
cut of this function:

1. **The host must be a plain DNS name.** `urlsplit().hostname` is not
   what an HTTP client resolves. Python accepts a backslash (and `%2F`)
   inside a netloc, while WHATWG-conformant clients — including
   `requests`/`urllib3` — treat them as *delimiters*. So
   `http://127.0.0.1:8000\@fal.media/x` parses here with hostname
   `fal.media` and is fetched from `127.0.0.1`. Anything outside
   `[a-z0-9.-]` is therefore refused outright.
2. **Matching is label-wise, never by raw suffix.** `endswith(".fal.media")`
   is true of `evil.com\.fal.media`; `host == h or host.endswith("." + h)`
   is not.

```pycon
>>> is_immutable_url("https://v3b.fal.media/files/x.png")
True
>>> is_immutable_url("https://fal.media/files/x.png")
True
>>> is_immutable_url("https://example.com/reference.png")
False
>>> is_immutable_url("file:///tmp/render.mp4")
False
```

A host that merely *contains* a trusted name is not that host, however it
is spelled:

```pycon
>>> is_immutable_url("https://fal.media@evil.example/x.png")
False
>>> is_immutable_url("https://notfal.media/x.png")
False
>>> is_immutable_url("http://127.0.0.1:8000\\@fal.media/x.png")
False
```

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### falaw.content.remembered_ref(url)

The [`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef) previously recorded for `url`, if any.

A *hint*, not a guarantee: it says “the last time falaw fetched this URL,
the bytes hashed to this” and says nothing about whether those bytes are
still in any store. Callers must verify (`store.has_blob(...)`, or an
already-materialized file on disk) before trusting it.

Public because it is the cheap pre-check that lets a caller answer “do I
already have this?” without a network round-trip — see
[`falaw.cache.materialize_asset()`](_autosummary/falaw.cache.html.md#falaw.cache.materialize_asset).

**It says nothing about whether the URL still serves those bytes.** For a
mutable URL that question needs [`content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url), which
revalidates; this is the raw recorded value, and trusting it for a
non-[`is_immutable_url()`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url) URL is thorwhalen/falaw#23.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef)]

### falaw.content.using_url_fetcher(fetcher)

Make `fetcher` falaw’s default asset transport for the duration.

The **public seam** for replacing falaw’s network transport wholesale —
with an authenticated client, a retrying one, a local mirror, or (the
common case) an in-memory fake in a test suite.

Prefer this to reaching for the module’s private default: it covers every
entry point in one place, it needs no `monkeypatch`, and it cannot be
invalidated by an internal rename. It is also the only mechanism that
reaches a call falaw makes from a thread you did not create — see
`_DEFAULT_FETCHER` for why that matters.

An explicitly passed `fetcher=` / `asset_fetcher=` still wins: this
changes the *default*, never an explicit choice.

Nests and restores, so an inner block cannot leak over an outer one:

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]]

```pycon
>>> from lacing import ArtifactStore
>>> store = ArtifactStore.in_memory()
>>> before = default_url_fetcher()
>>> with using_url_fetcher(lambda url: [b"faked"]):
...     content_ref_for_url("https://fal.media/x.png", store=store).bytes_size
5
>>> default_url_fetcher() is before
True
```

For a ready-made fake with pinned bytes and 404s, see [`falaw.testing`](_autosummary/falaw.testing.html.md#module-falaw.testing).

### falaw.content.write_blob_to_file(content_hash, path, , store=None)

Materialize the blob for `content_hash` as a **copy** at `path`.

Writes via a temporary file and an atomic rename, so a concurrent reader
never sees a partial file.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

## Why a copy and not a hard link

Hard-linking the store’s own blob file would be free, and an earlier cut of
this function did exactly that — but a hard link makes the returned path
and the content-store blob **the same inode**. Any consumer that writes
through the returned path (ffmpeg writing in place, an accidental `open(p,
"ab")`) then leaves a blob whose SHA-256 no longer matches its own name,
while `has_blob` keeps answering `True` — the store starts serving
*wrong bytes under a correct content address*, which is precisely the
failure class content addressing exists to eliminate. Making the link
read-only instead is not an option either: the store’s backend writes blobs
with `open(path, "wb")`, so a read-only blob breaks re-putting identical
bytes.

A copy costs disk (and, on APFS/btrfs/XFS, `shutil.copyfile` gets
copy-on-write for free). Immutability of the content store is worth it.

Returns `path`.

* **raises KeyError:**
  the store holds no blob for `content_hash`.


# _autosummary/falaw.core.html.md

# falaw.core

Wrapper over `fal_client.subscribe` with progress events and auto-journaling.

Operations should call `call_fal` rather than `fal_client.subscribe` directly,
so we get one place to:

* publish structured progress events to subscribers (UI, telemetry),
* auto-journal failures (so the next agent learns from them),
* layer retries / backoff later without changing call sites.

See [`falaw.events`](_autosummary/falaw.events.html.md#module-falaw.events) for the event model. `on_log` (a
string-only callback) still works for backward compatibility — when
both `on_log` and `on_event` are given, both fire.

### Module Attributes

| [`FAL_PROVIDER`](_autosummary/falaw.core.html.md#falaw.core.FAL_PROVIDER)    | The provider id a fal key is bound under, in the fleet's facade kit (`ocracy.kit.using_credentials()`).   |
|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|
| [`FAL_KEY_ENVVARS`](_autosummary/falaw.core.html.md#falaw.core.FAL_KEY_ENVVARS) | The env vars falaw itself reads for a fal key, in order (the pricing and account probes).                 |

### Functions

| [`call_fal`](_autosummary/falaw.core.html.md#falaw.core.call_fal)(application, arguments, \*[, ...])   | Call a fal model via fal_client.subscribe.                                                                              |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| [`current_fal_key`](_autosummary/falaw.core.html.md#falaw.core.current_fal_key)()                             | The fal API key bound for the current context, or `None`.                                                               |
| [`using_fal_credentials`](_autosummary/falaw.core.html.md#falaw.core.using_fal_credentials)(key)                    | Bind `key` as the fal credential for every [`call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) in this context. |

### falaw.core.FAL_KEY_ENVVARS *= ('FAL_KEY', 'FAL_API_KEY')*

The env vars falaw itself reads for a fal key, in order (the pricing and account
probes). [`call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) reads none: it leaves the env lookup to the fal SDK.

### falaw.core.FAL_PROVIDER *= 'fal'*

The provider id a fal key is bound under, in the fleet’s facade kit
(`ocracy.kit.using_credentials()`). The binding is a
[`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar) (rather than an `os.environ` mutation), so it is
scoped to the entering context, the asyncio tasks it spawns and work run in a copy
of it (`contextvars.copy_context().run`; a bare thread pool does not copy it):
concurrent requests are
unaffected and no process-wide lock is needed. Because the kit is shared, a server
that binds `fal` once reaches every facade that calls fal, not only falaw. This
is the seam reelee’s HTTP server uses to forward a BYO key into server-side fal
calls (reelee#159) without threading a credential argument through every
intermediate signature (plan → execute → cached_call_fal → call_fal).
Where `ocracy` is not installed, `falaw._credentials` falls back to a local
binding with the same semantics (not shared with other facades) and warns once.

### falaw.core.call_fal(application, arguments, , on_log=None, on_event=None, with_logs=True, journal_errors=True, api_key=None)

Call a fal model via fal_client.subscribe.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id (e.g. `"fal-ai/flux/dev"`).
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Input arguments. Keys depend on the model.
  * **on_log** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Legacy log callback — receives raw log lines as strings.
    Defaults to no-op (use `on_event` for structured access).
  * **on_event** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Per-call subscriber for `ProgressEvent`s. Fires
    in addition to the global subscribers registered via
    :func:`falaw.events.subscribe`.
  * **with_logs** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pass through to fal_client; when True the model streams logs.
  * **journal_errors** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True, exceptions are recorded as journal issues
    before being re-raised. The journal entry includes the application
    id and arguments so future agents can recognize the same trap.
  * **api_key** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Explicit fal key for this call. When `None` (default) the
    key bound via [`using_fal_credentials()`](_autosummary/falaw.core.html.md#falaw.core.using_fal_credentials) is used; when that is
    also unset, the fal SDK’s own `FAL_KEY` env-var lookup applies
    (the historical behaviour). A resolved key is used per-call via a
    dedicated `fal_client.SyncClient` — it is never written to a
    global or an env var.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The raw response dict from the model.

### falaw.core.current_fal_key()

The fal API key bound for the current context, or `None`.

Resolution order callers should mirror: an explicit `api_key` argument
to [`call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) wins over this context value, which in turn wins over
the fal SDK’s own `FAL_KEY` env-var lookup.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.core.using_fal_credentials(key)

Bind `key` as the fal credential for every [`call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) in this context.

Intended for server-side bring-your-own-key flows: wrap a unit of work
that will make one or more fal calls, and they all authenticate with
`key` instead of the server’s `FAL_KEY` env var — without any
intermediate function needing a credential parameter.

A falsy `key` is a deliberate no-op (the context is left untouched), so
a caller can pass an optional header value straight through without
special-casing “no BYO key — fall back to the server/env key”.

Thread/async safe: this is `ocracy.kit.using_credentials(fal=key)`, backed
by a [`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar), so the binding is visible only within
the entering context (and asyncio tasks or copied contexts it spawns),
never to concurrent
requests.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]


# _autosummary/falaw.corpus.html.md

# falaw.corpus

Extract model records from `misc/docs/llms-full.txt`.

The fal.ai llms-full.txt has a regular structure:

> ## <Section heading>
> <paragraph>

> ### <Model name>

> - **Model**: `fal-ai/<id>`
> - **Purpose**: <one-liner>
> - **Features**: <details>

We map the section to a category, infer a quality tier from the model
name, and emit ModelRecord-shaped dicts the registry can consume. The
`refresh_models_from_corpus` tool merges these into `data/models.json`,
adding new models without overwriting hand-curated entries.

### Functions

| [`extract_models_from_corpus`](_autosummary/falaw.corpus.html.md#falaw.corpus.extract_models_from_corpus)(path)              | Yield ModelRecord-shaped dicts parsed from llms-full.txt.   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
| [`refresh_models_from_corpus`](_autosummary/falaw.corpus.html.md#falaw.corpus.refresh_models_from_corpus)(\*[, path, write]) | Merge corpus-discovered models into models.json (additive). |

### falaw.corpus.extract_models_from_corpus(path)

Yield ModelRecord-shaped dicts parsed from llms-full.txt.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.corpus.refresh_models_from_corpus(, path=None, write=False)

Merge corpus-discovered models into models.json (additive).

Returns `{added, total, from_corpus, write}` summary. Setting
`write=False` (the default) reports what *would* change without
touching the file.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.cost.html.md

# falaw.cost

Cost estimation: ModelRecord.cost_estimate + Scene rollups.

Two surfaces here:

- `estimate_call_cost(record, *, count=1, seconds=None, megapixels=None,
  tokens=None)()` — turn one model’s `CostEstimate` into a USD figure.
- `estimate_scene_cost(scene, *, ...)()` — walk every Shot + Beat in a
  [`falaw.Scene`](_autosummary/falaw.html.md#falaw.Scene) and sum the per-call costs that
  [`falaw.render_scene()`](_autosummary/falaw.html.md#falaw.render_scene) would incur. Returns a structured rollup
  > (per-line + totals) so UIs can show “this render would cost ~$0.42”.

A scene-level estimate is *advisory*. It’s based on the same
`pick_model(category, quality_tier=...)` lookup that the renderer
uses, multiplied by per-line duration / pixel budgets. Real costs vary
with model fluctuations and provider billing rules, but this is good
enough to gate on (e.g. `--budget=1.00`) and to surface in
`muvid status`.

### Module Attributes

| [`DFLT_MEGAPIXELS`](_autosummary/falaw.cost.html.md#falaw.cost.DFLT_MEGAPIXELS)   | Pixel budget assumed for a `per_megapixel` call when the caller gives none: a 16:9 canvas 1024 wide (≈0.59 MP), enough for a useful upper-bound estimate.   |
|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| [`estimate_call_cost`](_autosummary/falaw.cost.html.md#falaw.cost.estimate_call_cost)(record, \*[, count, ...])       | Cost of one fal call against `record`.                       |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
| [`estimate_scene_cost`](_autosummary/falaw.cost.html.md#falaw.cost.estimate_scene_cost)(scene, \*[, tts_quality, ...]) | Estimate the USD cost of a full `render_scene()` invocation. |

### Classes

| [`CostLine`](_autosummary/falaw.cost.html.md#falaw.cost.CostLine)(\*, kind, item_id, model_id, amount, ...)   | One line item in a scene rollup.                                                  |
|-------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`CostRollup`](_autosummary/falaw.cost.html.md#falaw.cost.CostRollup)(\*, total_amount[, currency, ...])        | Result of [`estimate_scene_cost()`](_autosummary/falaw.cost.html.md#falaw.cost.estimate_scene_cost). |

### *class* falaw.cost.CostLine(, kind, item_id, model_id, amount, currency, note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One line item in a scene rollup.

### *class* falaw.cost.CostRollup(, total_amount, currency='USD', lines=(), skipped=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of [`estimate_scene_cost()`](_autosummary/falaw.cost.html.md#falaw.cost.estimate_scene_cost).

#### by_kind()

Sum per `kind` for quick inspection.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### falaw.cost.DFLT_MEGAPIXELS *= 0.6*

Pixel budget assumed for a `per_megapixel` call when the caller gives none:
a 16:9 canvas 1024 wide (≈0.59 MP), enough for a useful upper-bound estimate.
Exported to the TypeScript twin, which must assume the same.

### falaw.cost.estimate_call_cost(record, , count=1, seconds=None, megapixels=None, tokens=None)

Cost of one fal call against `record`.

Returns `None` when the cost is **unknown**, so callers can
distinguish “free” from “we can’t say”. That happens when the record
carries no `cost_estimate` at all, or when its pricing is
quantity-based (`per_second` / `per_token`) and the caller did
not supply the quantity — an unpriceable call, not a free one.

Returning `0.0` for a missing quantity would be actively dangerous:
a `per_second` clip is the single most expensive thing fal bills
for, and a caller gating a budget on the answer would read
“$0.00” and spend real money without a prompt. `None` instead
propagates to `CallPlan.estimated_cost_usd` and lights up
`Plan.has_unknown_costs`, which exists for exactly this case.
Callers that know the quantity should pass it; `per_second` callers
can fall back to `record.max_clip_seconds` for an upper bound.

`per_megapixel` is deliberately different: an image’s pixel budget
has a sane house default (below), so it stays priceable.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### falaw.cost.estimate_scene_cost(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, shot_seconds=None)

Estimate the USD cost of a full `render_scene()` invocation.

Walks every shot + beat with the same `pick_model` semantics
the renderer uses, then sums per-call costs. Returns a structured
[`CostRollup`](_autosummary/falaw.cost.html.md#falaw.cost.CostRollup) with per-line breakdowns, plus a list of
“skipped” entries the caller should surface (typically: a model
with no `cost_estimate` populated).

`shot_seconds` is the assumed clip length used to price
`shots_as_video`. A `Shot` carries no duration of its own —
screen time comes from the renderer’s per-shot run — and
image-to-video models bill **per second**, so without this the video
lines are genuinely unpriceable and are reported in
`CostRollup.skipped` rather than silently priced at $0.00.
Pass the length you expect (the beat path has the same knob as
`estimated_seconds`).

* **Return type:**
  [`CostRollup`](_autosummary/falaw.cost.html.md#falaw.cost.CostRollup)


# _autosummary/falaw.degrade.html.md

# falaw.degrade

Where falaw sends “I carried on, but you should know” — one collection point.

falaw deliberately degrades rather than failing in several places: a failed
asset fetch becomes a URL-only artifact instead of discarding a render fal has
already billed, and an unreadable origin falls back to bytes already in the
store instead of losing the artifact. Every one of those is a decision the
caller would want to hear about, and none of them should stop the run.

`warnings.warn` alone is the wrong channel for two reasons, both learned:

* **It deduplicates.** Python’s default filter shows a given message once per
  location per process, so on a long-lived server the second and every
  subsequent stale serve of the same URL is *silent*.
* **It cannot be collected.** Under `execute_plan(concurrency=N)` a
  speculative probe’s complaint interleaves with real ones, and
  `warnings.catch_warnings` is documented as not thread-safe — it mutates
  process-global filter state, so one call suppressing its own noise would
  suppress another call’s genuine warning at random.

Hence a [`ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar) sink. Set inside a unit of work it is
visible only to that unit (each pool task runs in its own context copy), and
when nothing has set one the message falls through to `warnings.warn`.

This lives in its own module, below both [`falaw.plan`](_autosummary/falaw.plan.html.md#module-falaw.plan) and
[`falaw.content`](_autosummary/falaw.content.html.md#module-falaw.content), because **both** produce degradations and neither may
import the other at module scope.

### Functions

| [`deferred_degrade_warnings`](_autosummary/falaw.degrade.html.md#falaw.degrade.deferred_degrade_warnings)()                 | Collect degradation messages into a list instead of warning.   |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`emit_degradation`](_autosummary/falaw.degrade.html.md#falaw.degrade.emit_degradation)(message, \*[, stacklevel]) | Report that falaw degraded rather than failed.                 |

### falaw.degrade.deferred_degrade_warnings()

Collect degradation messages into a list instead of warning.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]

### falaw.degrade.emit_degradation(message, , stacklevel=3)

Report that falaw degraded rather than failed.

Into the active sink if there is one, else straight to `warnings.warn`.
Every degradation in falaw goes through here so that none of them can be
the one that escapes collection — the more severe the degradation, the
worse it is for it to be the escapee.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/falaw.errors.html.md

# falaw.errors

Typed exceptions for falaw operations.

The bare `fal_client` exposes `FalClientError`, `FalClientHTTPError`, and
`FalClientTimeoutError`. These are correct but coarse — every 403, 429,
402, and timeout looks the same to a caller. This module wraps fal_client
errors in a more specific hierarchy so:

- An agent can catch [`FalAccountLocked`](_autosummary/falaw.errors.html.md#falaw.errors.FalAccountLocked) and surface a “go fix billing”
  message instead of debugging a generic 403.
- A retry layer can catch [`FalRateLimited`](_autosummary/falaw.errors.html.md#falaw.errors.FalRateLimited) and back off, but let
  [`FalAccountLocked`](_autosummary/falaw.errors.html.md#falaw.errors.FalAccountLocked) propagate (no amount of retry will fix it).
- Scripts can catch [`FalDurationOutOfRange`](_autosummary/falaw.errors.html.md#falaw.errors.FalDurationOutOfRange) to fall back to splitting
  a shot, instead of silently truncating to whatever the model produced.

Use [`translate()`](_autosummary/falaw.errors.html.md#falaw.errors.translate) to convert a fal_client exception to the appropriate
typed subclass — [`falaw.core.call_fal()`](_autosummary/falaw.core.html.md#falaw.core.call_fal) does this automatically.

### Examples

```pycon
>>> err = FalAccountLocked(
...     "account locked",
...     status_code=403,
...     detail="Verify your email at fal.ai/settings",
...     url="https://fal.ai/dashboard/billing",
... )
>>> err.url
'https://fal.ai/dashboard/billing'
>>> isinstance(err, FalError)
True
```

### Functions

| [`import_fal_client`](_autosummary/falaw.errors.html.md#falaw.errors.import_fal_client)()               | Import and return `fal_client`, or raise [`FalClientNotInstalled`](_autosummary/falaw.errors.html.md#falaw.errors.FalClientNotInstalled).                      |
|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|
| [`import_httpx`](_autosummary/falaw.errors.html.md#falaw.errors.import_httpx)([what])              | Import and return `httpx` (part of the `fal` extra), or raise [`FalClientNotInstalled`](_autosummary/falaw.errors.html.md#falaw.errors.FalClientNotInstalled). |
| [`translate`](_autosummary/falaw.errors.html.md#falaw.errors.translate)(exc, \*[, application]) | Convert an underlying fal_client / network exception to a typed falaw exception.                                                      |

### Exceptions

| [`FalAccountLocked`](_autosummary/falaw.errors.html.md#falaw.errors.FalAccountLocked)(message, \*, status_code[, ...])   | fal account is locked / suspended / awaiting verification.                   |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`FalAssetFetchError`](_autosummary/falaw.errors.html.md#falaw.errors.FalAssetFetchError)(message, \*, url[, cause])       | The bytes behind a fal-served asset URL could not be retrieved.              |
| [`FalBadRequest`](_autosummary/falaw.errors.html.md#falaw.errors.FalBadRequest)(message, \*, status_code[, ...])      | Server rejected the request payload — typical 400 / 422.                     |
| [`FalClientNotInstalled`](_autosummary/falaw.errors.html.md#falaw.errors.FalClientNotInstalled)([what, missing])              | A real network call was attempted but the fal.ai client is not installed.    |
| [`FalDurationOutOfRange`](_autosummary/falaw.errors.html.md#falaw.errors.FalDurationOutOfRange)(message, \*, model_id, ...)   | The requested duration is outside what the model can produce.                |
| [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)                                            | Base for all falaw-raised exceptions.                                        |
| [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)(message, \*, status_code[, ...])       | Wraps an HTTP error from fal.ai with the original status, body, and headers. |
| [`FalInsufficientFunds`](_autosummary/falaw.errors.html.md#falaw.errors.FalInsufficientFunds)(message, \*, status_code)      | Account balance is insufficient — typical 402.                               |
| [`FalModelHung`](_autosummary/falaw.errors.html.md#falaw.errors.FalModelHung)(message, \*, model_id, elapsed_s)      | A model was queued but never returned — distinct from a network timeout.     |
| [`FalNonCanonicalArgument`](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument)(message, \*, path)          | An argument cannot be canonicalised into falaw's hashed JSON form.           |
| [`FalPromptTooLong`](_autosummary/falaw.errors.html.md#falaw.errors.FalPromptTooLong)(message, \*, model, limit, ...)    | The prompt exceeds the model's hard length cap (falaw#76).                   |
| [`FalRateLimited`](_autosummary/falaw.errors.html.md#falaw.errors.FalRateLimited)(message, \*[, retry_after_s])        | fal is throttling requests — typical 429.                                    |
| [`FalServerError`](_autosummary/falaw.errors.html.md#falaw.errors.FalServerError)(message, \*, status_code[, ...])     | fal-side server error — typical 5xx.                                         |
| [`FalTimeout`](_autosummary/falaw.errors.html.md#falaw.errors.FalTimeout)(message, \*, elapsed_s[, application])   | The fal call timed out before producing a result.                            |
| [`FalUnauthorized`](_autosummary/falaw.errors.html.md#falaw.errors.FalUnauthorized)(message, \*, status_code[, ...])    | Missing or invalid API credentials — typical 401.                            |

### *exception* falaw.errors.FalAccountLocked(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal account is locked / suspended / awaiting verification.

Typical 403 with a body indicating the account is not in good standing.
No amount of retrying or model-switching will fix this — the user has
to act (verify email, top up billing, contact support).

### *exception* falaw.errors.FalAssetFetchError(message, , url, cause=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The bytes behind a fal-served asset URL could not be retrieved.

Raised by [`falaw.content`](_autosummary/falaw.content.html.md#module-falaw.content) when content-addressing an artifact fails —
the URL 404s (fal deletes expired files permanently), the transfer errors,
or the response is empty. It is deliberately **loud**: returning an
Artifact whose `asset_id` is not the SHA-256 of any bytes would break
`lacing.Artifact`’s content-hash contract and poison every downstream
cache key derived from it.

### *exception* falaw.errors.FalBadRequest(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Server rejected the request payload — typical 400 / 422.

### *exception* falaw.errors.FalClientNotInstalled(what='calling fal.ai', , missing='fal_client')

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

A real network call was attempted but the fal.ai client is not installed.

`pip install falaw` carries no fal.ai client, so that callers that only
build, price, hash and cache plans install none. Anything that actually
reaches fal.ai needs the `fal` extra: `pip install 'falaw[fal]'`.
It is an [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError), so `except ImportError` still matches.

### *exception* falaw.errors.FalDurationOutOfRange(message, , model_id, requested, valid_range)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The requested duration is outside what the model can produce.

Raised by Plan/Execute when the caller asks for `duration_s` that the
model’s declared `expected_duration_range` cannot satisfy. Callers can
catch this to split the shot, repeat it, or pick a different model.

### *exception* falaw.errors.FalError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Base for all falaw-raised exceptions.

### *exception* falaw.errors.FalHTTPError(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

Wraps an HTTP error from fal.ai with the original status, body, and headers.

Subclasses pick out specific status codes / patterns. Use this base when
you want to catch any HTTP failure (e.g. for retry).

### *exception* falaw.errors.FalInsufficientFunds(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Account balance is insufficient — typical 402.

### *exception* falaw.errors.FalModelHung(message, , model_id, elapsed_s)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

A model was queued but never returned — distinct from a network timeout.

Raised by higher-level orchestration that sets a per-call wall-clock
budget (e.g. “give up on this lipsync after 5 minutes and pick another model”).

### *exception* falaw.errors.FalNonCanonicalArgument(message, , path)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

An argument cannot be canonicalised into falaw’s hashed JSON form.

Raised by [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical) when a value reaches a key-composition
site (the per-call cache key, `plan_hash`, the dry-run artifact id) that
JSON cannot represent faithfully: a non-JSON object, a non-finite float,
or a non-string mapping key. Deliberately **loud**: the old behaviour —
`json.dumps(..., default=str)` — silently collided structurally
different calls into one cache key, handing back the *wrong artifact* as
a supposed saving (falaw#17).

`path` names the offending node, e.g. `arguments.extra.ref`.

### *exception* falaw.errors.FalPromptTooLong(message, , model, limit, length)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError), [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

The prompt exceeds the model’s hard length cap (falaw#76).

Raised before any call is queued: fal answers an over-long prompt with an
HTTP 422 only after the request round-trips. `limit`/`length` are in
characters.

### *exception* falaw.errors.FalRateLimited(message, , retry_after_s=None, \*\*kwargs)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal is throttling requests — typical 429.

`retry_after_s` is parsed from the `Retry-After` header if present,
else `None` (caller decides backoff).

### *exception* falaw.errors.FalServerError(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal-side server error — typical 5xx. Generally retryable.

### *exception* falaw.errors.FalTimeout(message, , elapsed_s, application=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The fal call timed out before producing a result.

### *exception* falaw.errors.FalUnauthorized(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Missing or invalid API credentials — typical 401.

### falaw.errors.import_fal_client()

Import and return `fal_client`, or raise [`FalClientNotInstalled`](_autosummary/falaw.errors.html.md#falaw.errors.FalClientNotInstalled).

### falaw.errors.import_httpx(what='fetching from fal.ai')

Import and return `httpx` (part of the `fal` extra), or raise
[`FalClientNotInstalled`](_autosummary/falaw.errors.html.md#falaw.errors.FalClientNotInstalled).

### falaw.errors.translate(exc, , application=None)

Convert an underlying fal_client / network exception to a typed falaw exception.

Returns the original exception unchanged if no specific subclass applies
(so callers always get *something* — never silently swallowed).

Logic:

- `FalClientHTTPError` with 403 + lock-pattern body → [`FalAccountLocked`](_autosummary/falaw.errors.html.md#falaw.errors.FalAccountLocked)
- `FalClientHTTPError` with 401 → [`FalUnauthorized`](_autosummary/falaw.errors.html.md#falaw.errors.FalUnauthorized)
- `FalClientHTTPError` with 402 → [`FalInsufficientFunds`](_autosummary/falaw.errors.html.md#falaw.errors.FalInsufficientFunds)
- `FalClientHTTPError` with 429 → [`FalRateLimited`](_autosummary/falaw.errors.html.md#falaw.errors.FalRateLimited) (parses Retry-After)
- `FalClientHTTPError` with 4xx → [`FalBadRequest`](_autosummary/falaw.errors.html.md#falaw.errors.FalBadRequest)
- `FalClientHTTPError` with 5xx → [`FalServerError`](_autosummary/falaw.errors.html.md#falaw.errors.FalServerError)
- `FalClientTimeoutError` → [`FalTimeout`](_autosummary/falaw.errors.html.md#falaw.errors.FalTimeout)
- anything else → returned unchanged

* **Return type:**
  [`BaseException`](https://docs.python.org/3/builtins/exceptions.html#BaseException)


# _autosummary/falaw.events.html.md

# falaw.events

Structured progress events for fal calls.

Replaces the implicit “stream raw log strings to stdout” model with a
small, machine-readable event stream that orchestrators can subscribe
to (UI progress bars, cost telemetry, billing dashboards, etc.).

A `ProgressEvent` is emitted at every major lifecycle transition of
a single `call_fal` invocation:

- `queued`      — call submitted to fal
- `progress`    — fal pushed an InProgress update with no message body
- `log`         — fal pushed an InProgress update with a log line
- `done`        — call returned a result
- `error`       — call raised
- `cache_hit`   — [`falaw.cached_call_fal()`](_autosummary/falaw.html.md#falaw.cached_call_fal) found a hit and skipped the network

Subscribers can be registered globally via [`subscribe()`](_autosummary/falaw.events.html.md#falaw.events.subscribe) or
per-call via the `on_event=` argument on `call_fal()` /
`cached_call_fal()`. The `on_log` parameter still works
(legacy: a string-only stream); see `call_fal()` for compatibility
notes.

### Functions

| [`clear_subscribers`](_autosummary/falaw.events.html.md#falaw.events.clear_subscribers)()     | Drop all registered subscribers.                                                             |
|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| [`emit`](_autosummary/falaw.events.html.md#falaw.events.emit)(event, \*[, also]) | Send `event` to every registered subscriber + the `also` list.                               |
| [`subscribe`](_autosummary/falaw.events.html.md#falaw.events.subscribe)(callback)     | Register `callback` to receive every emitted ProgressEvent.                                  |
| [`unsubscribe`](_autosummary/falaw.events.html.md#falaw.events.unsubscribe)(callback)   | Remove a previously [`subscribe()`](_autosummary/falaw.events.html.md#falaw.events.subscribe)'d callback. |

### Classes

| [`ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent)(\*, kind, application, call_id)   | One step in the lifecycle of a fal call.   |
|--------------------------------------------------------------------------------------------------|--------------------------------------------|

### *class* falaw.events.ProgressEvent(, kind, application, call_id, message='', pct=None, elapsed_s=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One step in the lifecycle of a fal call.

#### kind

Lifecycle stage. See `EventKind`.

#### application

fal model id (e.g. `"fal-ai/flux/dev"`).

#### call_id

A short hex string that uniquely identifies the call.
All events for one `call_fal` invocation share the same
`call_id`.

#### message

Free-form text. For `"log"` events this is the log
line; for `"error"` it’s `repr(exc)`; otherwise empty.

#### pct

Optional progress percentage in [0.0, 100.0]. fal’s
current API doesn’t surface this; included for forward
compatibility.

#### elapsed_s

Seconds since the call started.

### falaw.events.clear_subscribers()

Drop all registered subscribers. Mostly for tests.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.events.emit(event, , also=())

Send `event` to every registered subscriber + the `also` list.

Subscriber exceptions are swallowed (with a printed warning) so a
misbehaving UI hook can’t bring the rendering pipeline down.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.events.subscribe(callback)

Register `callback` to receive every emitted ProgressEvent.

Returns the callback unchanged so it can be used as a decorator:

```default
@subscribe
def log_to_file(ev: ProgressEvent) -> None:
    ...
```

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### falaw.events.unsubscribe(callback)

Remove a previously [`subscribe()`](_autosummary/falaw.events.html.md#falaw.events.subscribe)’d callback. No-op if absent.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/falaw.html.md

# falaw

falaw: agent-friendly Python facade over fal.ai for AI media generation.

Quick start (single-shot generation):

```pycon
>>> from falaw import generate_image
>>> r = generate_image("a tiger eye, macro, 35mm", quality="fast")
>>> r.first.download(to="./tiger.png")
```

Directorial workflow (Scene IR + caching):

```pycon
>>> from falaw import Scene, cast_character, render_scene
>>> sarah = cast_character("Sarah", "mid-30s, dark curly hair")
>>> # ... build a Scene with characters/beats/shots ...
>>> manifest = render_scene(scene)            # caches per-beat
>>> # edit one beat, re-render: only that beat re-fires.
```

Leave notes for future sessions:

```pycon
>>> from falaw import journal
>>> journal.note("Sarah's voice clone needs ~10s reference for stability")
```

### Functions

| [`get_llm_price`](_autosummary/falaw.html.md#falaw.get_llm_price)(provider, model, \*[, prices])       | The row for `model` (exact id), or `None` when the table does not list it.                                                                                       |
|-----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`list_llm_prices`](_autosummary/falaw.html.md#falaw.list_llm_prices)([provider, prices])                | Every row, or the rows of one `provider`.                                                                                                                        |
| [`llm_call_cost_usd`](_autosummary/falaw.html.md#falaw.llm_call_cost_usd)(provider, model, usage, \*)      | `(USD, known)` for one first-party call from the vendor's reported `usage`.                                                                                      |
| [`llm_prices_table_version`](_autosummary/falaw.html.md#falaw.llm_prices_table_version)()                         | The table's version, a digest of its bytes (see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version)). |
| [`call_plan_from_dict`](_autosummary/falaw.html.md#falaw.call_plan_from_dict)(d)                             | Rebuild a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) from a [`call_plan_to_dict()`](_autosummary/falaw.html.md#falaw.call_plan_to_dict) dict.            |
| [`call_plan_to_dict`](_autosummary/falaw.html.md#falaw.call_plan_to_dict)(call)                            | Convert a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) to a plain JSON-serializable dict.                                                           |
| [`plan_dependencies`](_autosummary/falaw.html.md#falaw.plan_dependencies)(plan)                            | Per-call set of the call indices it references via `"<from N>"`.                                                                                                 |
| [`plan_from_dict`](_autosummary/falaw.html.md#falaw.plan_from_dict)(d)                                  | Rebuild a [`Plan`](_autosummary/falaw.html.md#falaw.Plan) from a [`plan_to_dict()`](_autosummary/falaw.html.md#falaw.plan_to_dict) dict.                     |
| [`plan_hash`](_autosummary/falaw.html.md#falaw.plan_hash)(plan)                                    | Stable, plan-scoped **structural idempotency key** for a whole [`Plan`](_autosummary/falaw.html.md#falaw.Plan).                                            |
| [`plan_to_dict`](_autosummary/falaw.html.md#falaw.plan_to_dict)(plan)                                 | Convert a [`Plan`](_autosummary/falaw.html.md#falaw.Plan) to a plain JSON-serializable dict.                                                               |
| [`catalogue_cost_basis`](_autosummary/falaw.html.md#falaw.catalogue_cost_basis)(model_id, \*\*quantities)     | A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a call priced from `models.json`.                                                         |
| [`cost_basis_from_dict`](_autosummary/falaw.html.md#falaw.cost_basis_from_dict)(d)                            | Rebuild a [`CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) from a [`cost_basis_to_dict()`](_autosummary/falaw.html.md#falaw.cost_basis_to_dict) dict.          |
| [`cost_basis_to_dict`](_autosummary/falaw.html.md#falaw.cost_basis_to_dict)(basis)                          | Convert a [`CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) to a plain JSON-serializable dict.                                                          |
| [`llm_cost_basis`](_autosummary/falaw.html.md#falaw.llm_cost_basis)(routed_model, \*[, ...])            | A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a routed LLM call priced from the rate table.                                             |
| [`reprice_plan`](_autosummary/falaw.html.md#falaw.reprice_plan)(plan, \*[, pricers])                  | Re-quote every call in `plan` against today's rate tables.                                                                                                       |
| [`clear_subscribers`](_autosummary/falaw.html.md#falaw.clear_subscribers)()                                | Drop all registered subscribers.                                                                                                                                 |
| [`estimate_call_cost`](_autosummary/falaw.html.md#falaw.estimate_call_cost)(record, \*[, count, ...])       | Cost of one fal call against `record`.                                                                                                                           |
| [`estimate_scene_cost`](_autosummary/falaw.html.md#falaw.estimate_scene_cost)(scene, \*[, tts_quality, ...]) | Estimate the USD cost of a full [`render_scene()`](_autosummary/falaw.html.md#falaw.render_scene) invocation.                                                      |
| [`get_llm_rate`](_autosummary/falaw.html.md#falaw.get_llm_rate)(model, \*[, rates])                   | The row for `model`, or `None` when the table does not price it.                                                                                                 |
| [`list_llm_rates`](_autosummary/falaw.html.md#falaw.list_llm_rates)(\*[, rates])                        | Every row in the table, in file order — the catalogue view.                                                                                                      |
| [`llm_ceiling_usd`](_autosummary/falaw.html.md#falaw.llm_ceiling_usd)(model, \*[, input_tokens, ...])    | Ceiling cost in USD of `count` calls routing `model`, or `None`.                                                                                                 |
| [`subscribe`](_autosummary/falaw.html.md#falaw.subscribe)(callback)                                | Register `callback` to receive every emitted ProgressEvent.                                                                                                      |
| [`unsubscribe`](_autosummary/falaw.html.md#falaw.unsubscribe)(callback)                              | Remove a previously [`subscribe()`](_autosummary/falaw.html.md#falaw.subscribe)'d callback.                                                                     |
| [`animate_face`](_autosummary/falaw.html.md#falaw.animate_face)(image_url, audio_url, \*[, ...])      | Animate a still face from audio.                                                                                                                                 |
| [`apply_note_to_beat`](_autosummary/falaw.html.md#falaw.apply_note_to_beat)(beat, note, \*[, model])        | Use the LLM to apply a directorial note to a Beat.                                                                                                               |
| [`apply_note_to_scene`](_autosummary/falaw.html.md#falaw.apply_note_to_scene)(scene, note, \*[, model])      | Apply a cross-cutting note: LLM proposes per-beat edits, we apply them.                                                                                          |
| [`beat_content_hash`](_autosummary/falaw.html.md#falaw.beat_content_hash)(beat, \*[, character])           | Hash everything that affects how the beat *renders*.                                                                                                             |
| [`cache_get`](_autosummary/falaw.html.md#falaw.cache_get)(application, arguments, \*[, ...])       | Return the raw fal response if cached, else None.                                                                                                                |
| [`cache_put`](_autosummary/falaw.html.md#falaw.cache_put)(application, arguments, raw, \*[, ...])  | Persist a fal response.                                                                                                                                          |
| [`cache_stats`](_autosummary/falaw.html.md#falaw.cache_stats)()                                      | Quick summary of the cache: entry count, disk usage, and where it went.                                                                                          |
| [`cache_usage`](_autosummary/falaw.html.md#falaw.cache_usage)()                                      | Disk usage of the falaw cache, broken down by area.                                                                                                              |
| [`prune_assets`](_autosummary/falaw.html.md#falaw.prune_assets)(\*[, older_than, max_bytes, ...])     | Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22).                                                                                   |
| [`prune_content`](_autosummary/falaw.html.md#falaw.prune_content)(\*[, older_than, max_bytes, ...])    | Reclaim content-addressed blobs — the gigabytes (falaw#22).                                                                                                      |
| [`prune_manifests`](_autosummary/falaw.html.md#falaw.prune_manifests)(\*[, older_than, max_bytes, ...])  | Reclaim cache entries — the fal responses themselves (falaw#22).                                                                                                 |
| [`drop_cache_entry`](_autosummary/falaw.html.md#falaw.drop_cache_entry)(application, arguments, \*)       | Delete the cache entry for `(application, arguments)`.                                                                                                           |
| [`cached_call_fal`](_autosummary/falaw.html.md#falaw.cached_call_fal)(application, arguments, \*[, ...]) | Call a fal model, but reuse the cached response when present.                                                                                                    |
| [`content_ref_for_url`](_autosummary/falaw.html.md#falaw.content_ref_for_url)(url, \*[, store, ...])         | Materialize `url`'s bytes into `store` and return their content hash.                                                                                            |
| [`default_content_store`](_autosummary/falaw.html.md#falaw.default_content_store)()                            | The falaw-cache-rooted `lacing.ArtifactStore`.                                                                                                                   |
| [`default_url_fetcher`](_autosummary/falaw.html.md#falaw.default_url_fetcher)()                              | The transport used when no `fetcher=` argument is given.                                                                                                         |
| [`using_url_fetcher`](_autosummary/falaw.html.md#falaw.using_url_fetcher)(fetcher)                         | Make `fetcher` falaw's default asset transport for the duration.                                                                                                 |
| [`call_fal`](_autosummary/falaw.html.md#falaw.call_fal)(application, arguments, \*[, ...])        | Call a fal model via fal_client.subscribe.                                                                                                                       |
| [`cast_character`](_autosummary/falaw.html.md#falaw.cast_character)(name, description, \*[, ...])       | Create a Character with a canonical face (and optional voice).                                                                                                   |
| [`current_fal_key`](_autosummary/falaw.html.md#falaw.current_fal_key)()                                  | The fal API key bound for the current context, or `None`.                                                                                                        |
| [`cast_voice`](_autosummary/falaw.html.md#falaw.cast_voice)(character, \*[, voice_id, ...])         | Attach or update the Voice on a Character.                                                                                                                       |
| [`composite_character_in_environment`](_autosummary/falaw.html.md#falaw.composite_character_in_environment)(...[, ...])     | Place a character into an environment as one composited still.                                                                                                   |
| [`edit_image`](_autosummary/falaw.html.md#falaw.edit_image)(image_url, prompt, \*[, quality, ...])  | Edit an image with a natural-language instruction.                                                                                                               |
| [`establish_environment`](_autosummary/falaw.html.md#falaw.establish_environment)(name, description, \*)       | Create an Environment with a canonical establishing image.                                                                                                       |
| [`execute_plan`](_autosummary/falaw.html.md#falaw.execute_plan)(plan, \*[, on_event, dry_run, ...])   | Execute a Plan, returning a list of materialized :class:<br/><br/>```<br/>`<br/>```<br/><br/>lacing.Artifact\`s.                                                 |
| [`execute_plan_isolated`](_autosummary/falaw.html.md#falaw.execute_plan_isolated)(plan, \*[, on_event, ...])   | Execute a Plan with **per-call failure isolation**, returning a report.                                                                                          |
| [`extract_models_from_corpus`](_autosummary/falaw.html.md#falaw.extract_models_from_corpus)(path)                   | Yield ModelRecord-shaped dicts parsed from llms-full.txt.                                                                                                        |
| [`generate_image`](_autosummary/falaw.html.md#falaw.generate_image)(prompt, \*[, quality, ...])         | Generate an image from a text prompt.                                                                                                                            |
| [`generate_image_with_refs`](_autosummary/falaw.html.md#falaw.generate_image_with_refs)(prompt, ...[, ...])       | Generate a new image conditioned on one or more reference images.                                                                                                |
| `get_model`(id)                                                                                     |                                                                                                                                                                  |
| `get_tool`(name)                                                                                    |                                                                                                                                                                  |
| [`image_to_video`](_autosummary/falaw.html.md#falaw.image_to_video)(image_url[, prompt, quality, ...])  | Animate a still image into a video.                                                                                                                              |
| [`iter_render_scene`](_autosummary/falaw.html.md#falaw.iter_render_scene)(scene, \*[, tts_quality, ...])   | Yield `(kind, result)` pairs as each shot/beat finishes.                                                                                                         |
| [`lipsync`](_autosummary/falaw.html.md#falaw.lipsync)(video_url, audio_url, \*[, quality, ...])  | Re-sync mouth motion in an existing video to a new audio track.                                                                                                  |
| `list_models`(\*[, category, quality_tier])                                                         |                                                                                                                                                                  |
| `list_tools`(\*[, tag])                                                                             |                                                                                                                                                                  |
| [`llm_complete`](_autosummary/falaw.html.md#falaw.llm_complete)(prompt, \*[, system, model, ...])     | Single-shot LLM completion.                                                                                                                                      |
| [`llm_complete_with_receipt`](_autosummary/falaw.html.md#falaw.llm_complete_with_receipt)(prompt, \*[, ...])       | Single-shot LLM completion that also reports what it cost.                                                                                                       |
| `load_scene`(path)                                                                                  |                                                                                                                                                                  |
| `make_beat`(speaker[, line, action, emotion, ...])                                                  |                                                                                                                                                                  |
| [`make_call_plan`](_autosummary/falaw.html.md#falaw.make_call_plan)(\*, tool, application, ...[, ...])  | Build a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) and (optionally) check the cache.                                                              |
| `make_shot`(description, \*[, framing, ...])                                                        |                                                                                                                                                                  |
| [`materialize_asset`](_autosummary/falaw.html.md#falaw.materialize_asset)(url, \*[, key_hint, store, ...]) | Download a remote asset to the cache and return the local path.                                                                                                  |
| [`parse_response`](_autosummary/falaw.html.md#falaw.parse_response)(raw, \*, application, arguments)    | Best-effort parser over the common fal response shapes.                                                                                                          |
| [`parse_screenplay`](_autosummary/falaw.html.md#falaw.parse_screenplay)(text, \*[, title, style, model])  | Convert prose screenplay text into a Scene IR via an LLM call.                                                                                                   |
| [`pick_model`](_autosummary/falaw.html.md#falaw.pick_model)(\*, category[, quality_tier])           | Pick a sensible fal model for a (category, quality) request.                                                                                                     |
| [`model_constraints`](_autosummary/falaw.html.md#falaw.model_constraints)(id)                              | The capability/limit fields for a model — the "static reminder of limitations" a shot-list builder surfaces.                                                     |
| [`model_params`](_autosummary/falaw.html.md#falaw.model_params)(id)                                   | Per-model parameters worth knowing before a call, `{name: spec}`.                                                                                                |
| [`video_model_constraints`](_autosummary/falaw.html.md#falaw.video_model_constraints)()                          | `model_constraints` for every video model in the catalog — the data a shot-list builder shows as its model-limits reference.                                     |
| [`plan_animate_face`](_autosummary/falaw.html.md#falaw.plan_animate_face)(image_url, audio_url, \*[, ...]) | Plan a [`falaw.animate_face()`](_autosummary/falaw.html.md#falaw.animate_face) call (image + audio → talking video).                                               |
| [`plan_composite_character_in_environment`](_autosummary/falaw.html.md#falaw.plan_composite_character_in_environment)(...)       | Plan a [`falaw.composite_character_in_environment()`](_autosummary/falaw.html.md#falaw.composite_character_in_environment) call.                                                         |
| [`plan_edit_image`](_autosummary/falaw.html.md#falaw.plan_edit_image)(image_url, prompt, \*[, ...])      | Plan a [`falaw.edit_image()`](_autosummary/falaw.html.md#falaw.edit_image) call (Flux Kontext / SeedEdit / OmniGen).                                             |
| [`plan_generate_image`](_autosummary/falaw.html.md#falaw.plan_generate_image)(prompt, \*[, quality, ...])    | Plan a [`falaw.generate_image()`](_autosummary/falaw.html.md#falaw.generate_image) call without executing it.                                                        |
| [`plan_generate_image_with_refs`](_autosummary/falaw.html.md#falaw.plan_generate_image_with_refs)(prompt, ...[, ...])  | Plan a [`falaw.generate_image_with_refs()`](_autosummary/falaw.html.md#falaw.generate_image_with_refs) call.                                                                   |
| [`plan_image_to_video`](_autosummary/falaw.html.md#falaw.plan_image_to_video)(image_url[, prompt, ...])      | Plan a [`falaw.image_to_video()`](_autosummary/falaw.html.md#falaw.image_to_video) call.                                                                             |
| [`plan_lipsync`](_autosummary/falaw.html.md#falaw.plan_lipsync)(video_url, audio_url, \*[, ...])      | Plan a [`falaw.lipsync()`](_autosummary/falaw.html.md#falaw.lipsync) call (existing video + new audio → re-synced video).                                     |
| [`plan_llm_complete`](_autosummary/falaw.html.md#falaw.plan_llm_complete)(prompt, \*[, system, ...])       | Plan a [`falaw.llm_complete()`](_autosummary/falaw.html.md#falaw.llm_complete) call without executing it.                                                          |
| [`plan_generate_audio`](_autosummary/falaw.html.md#falaw.plan_generate_audio)(prompt, \*[, kind, ...])       | Plan a [`falaw.generate_audio()`](_autosummary/falaw.html.md#falaw.generate_audio) call (prompt → ambient/SFX/music).                                                |
| [`plan_text_to_speech`](_autosummary/falaw.html.md#falaw.plan_text_to_speech)(text, \*[, quality, ...])      | Plan a [`falaw.text_to_speech()`](_autosummary/falaw.html.md#falaw.text_to_speech) call (text → audio Artifact).                                                     |
| [`refresh_full_docs`](_autosummary/falaw.html.md#falaw.refresh_full_docs)(\*[, docs_dir, ...])             | Re-crawl per-page docs and rebuild `fal_ai_docs_full.md`.                                                                                                        |
| [`refresh_llms`](_autosummary/falaw.html.md#falaw.refresh_llms)(\*[, docs_dir, journal])              | Refresh `llms.txt` and `llms-full.txt`; return a summary dict.                                                                                                   |
| [`refresh_models_from_corpus`](_autosummary/falaw.html.md#falaw.refresh_models_from_corpus)(\*[, path, write])      | Merge corpus-discovered models into models.json (additive).                                                                                                      |
| [`refresh_model_prices`](_autosummary/falaw.html.md#falaw.refresh_model_prices)(\*[, write, api_key, ...])    | Refresh `models.json` cost estimates from fal's pricing API.                                                                                                     |
| [`check_model_liveness`](_autosummary/falaw.html.md#falaw.check_model_liveness)([ids, status_of])             | `{endpoint_id: is_live}` for `ids` (default: every catalogued model).                                                                                            |
| [`dead_models`](_autosummary/falaw.html.md#falaw.dead_models)([ids, status_of])                      | The ids from [`check_model_liveness()`](_autosummary/falaw.html.md#falaw.check_model_liveness) that are no longer deployed.                                                |
| [`fetch_model_prices`](_autosummary/falaw.html.md#falaw.fetch_model_prices)(endpoint_ids, \*[, ...])        | `{endpoint_id: {"unit_price", "unit", "currency"}}` from fal's API.                                                                                              |
| [`refresh_llm_rates`](_autosummary/falaw.html.md#falaw.refresh_llm_rates)(\*[, write, ...])                | Fetch both sources, propose a fresh table, diff it — never overwrite.                                                                                            |
| [`refresh_state`](_autosummary/falaw.html.md#falaw.refresh_state)()                                    | Return the saved per-source refresh state (etags, last fetch times).                                                                                             |
| [`register_tool`](_autosummary/falaw.html.md#falaw.register_tool)(\*\*spec_kwargs)                     | Decorator: register the wrapped function as a falaw tool.                                                                                                        |
| [`remove_background`](_autosummary/falaw.html.md#falaw.remove_background)(image_url, \*[, quality, ...])   | Remove the background from an image.                                                                                                                             |
| [`render_beat`](_autosummary/falaw.html.md#falaw.render_beat)(beat, character, \*[, ...])            | Render one Beat to a lipsynced video.                                                                                                                            |
| [`render_scene`](_autosummary/falaw.html.md#falaw.render_scene)(scene, \*[, tts_quality, ...])        | Render every shot and beat.                                                                                                                                      |
| [`render_shot`](_autosummary/falaw.html.md#falaw.render_shot)(shot, \*[, environment, ...])          | Render a Shot as a still (default) or a short clip.                                                                                                              |
| `save_scene`(scene, path)                                                                           |                                                                                                                                                                  |
| [`scene_from_dict`](_autosummary/falaw.html.md#falaw.scene_from_dict)(d)                                 | Inverse of asdict: reconstruct a Scene from a plain dict.                                                                                                        |
| `scene_to_dict`(scene)                                                                              |                                                                                                                                                                  |
| `shot_content_hash`(shot, \*[, environment])                                                        |                                                                                                                                                                  |
| [`storyboard_shot`](_autosummary/falaw.html.md#falaw.storyboard_shot)(shot, \*[, environment, ...])      | Render a storyboard still for a Shot.                                                                                                                            |
| [`talking_avatar_from_text`](_autosummary/falaw.html.md#falaw.talking_avatar_from_text)(text, image_url, \*)      | text + face image → talking video.                                                                                                                               |
| [`generate_audio`](_autosummary/falaw.html.md#falaw.generate_audio)(prompt, \*[, kind, ...])            | Generate ambient/SFX/music from a prompt.                                                                                                                        |
| [`text_to_speech`](_autosummary/falaw.html.md#falaw.text_to_speech)(text, \*[, quality, voice, ...])    | Synthesize speech.                                                                                                                                               |
| [`text_to_video`](_autosummary/falaw.html.md#falaw.text_to_video)(prompt, \*[, quality, ...])          | Generate a video from a text prompt.                                                                                                                             |
| [`upscale_image`](_autosummary/falaw.html.md#falaw.upscale_image)(image_url, \*[, scale, ...])         | Upscale an image.                                                                                                                                                |
| [`using_fal_credentials`](_autosummary/falaw.html.md#falaw.using_fal_credentials)(key)                         | Bind `key` as the fal credential for every [`call_fal()`](_autosummary/falaw.html.md#falaw.call_fal) in this context.                                          |
| [`voice_clone`](_autosummary/falaw.html.md#falaw.voice_clone)(reference_audio_url, text, \*[, ...])  | Generate speech in a cloned voice.                                                                                                                               |

### Classes

| [`LlmPrice`](_autosummary/falaw.html.md#falaw.LlmPrice)(\*, provider, model, rates[, ...])        | One row of the first-party table: a model's rates and where they came from.                                        |
|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`AccountStatus`](_autosummary/falaw.html.md#falaw.AccountStatus)(ok, locked, unauthorized, ...)       | Outcome of `health_check()`.                                                                                       |
| [`Asset`](_autosummary/falaw.html.md#falaw.Asset)(url, kind[, content_type, width, ...])       | A single piece of generated media.                                                                                 |
| [`Beat`](_autosummary/falaw.html.md#falaw.Beat)(\*, id[, speaker, line, action, ...])         | The atomic unit of a scene: who, says what, with what intent.                                                      |
| [`CallOutcome`](_autosummary/falaw.html.md#falaw.CallOutcome)(\*, index, call, status[, ...])        | What happened to one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan), at its position in the Plan.       |
| [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan)(\*, tool, application[, backend, ...])    | A single planned fal call.                                                                                         |
| [`Character`](_autosummary/falaw.html.md#falaw.Character)(\*, name[, description, ...])            | A reusable character: stable face, stable voice, stable style.                                                     |
| [`ContentRef`](_autosummary/falaw.html.md#falaw.ContentRef)(content_hash, bytes_size)               | A content-addressed handle on some bytes falaw has materialized.                                                   |
| [`Environment`](_autosummary/falaw.html.md#falaw.Environment)(\*, name[, description, ...])          | A reusable location/setting.                                                                                       |
| [`ExecutionReport`](_autosummary/falaw.html.md#falaw.ExecutionReport)([outcomes])                        | The result of running a Plan: one [`CallOutcome`](_autosummary/falaw.html.md#falaw.CallOutcome) per call, in order. |
| [`ModelRecord`](_autosummary/falaw.html.md#falaw.ModelRecord)(\*, id, category[, description, ...])  | One entry in the fal model catalog.                                                                                |
| [`Plan`](_autosummary/falaw.html.md#falaw.Plan)([calls])                                      | An ordered sequence of [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) — a render plan, in essence.      |
| [`CallRepricing`](_autosummary/falaw.html.md#falaw.CallRepricing)(\*, index, call, old_cost_usd, ...)  | The before/after for one call in a re-priced plan.                                                                 |
| [`CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis)(\*, pricer, priced[, quantities, ...])   | How a [`CallPlan.estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) was arrived at (falaw#60).      |
| [`Pricer`](_autosummary/falaw.html.md#falaw.Pricer)(\*, quote, table[, version])                | One pricing rule, and the table that backs it.                                                                     |
| [`RepricedPlan`](_autosummary/falaw.html.md#falaw.RepricedPlan)(\*, plan[, calls])                    | Result of [`reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) — the new Plan plus the per-call diff.   |
| [`Result`](_autosummary/falaw.html.md#falaw.Result)([assets, raw, application, arguments])      | A fal call result with parsed assets and the original raw response.                                                |
| [`CostEstimate`](_autosummary/falaw.html.md#falaw.CostEstimate)(\*, kind, amount[, currency, ...])    | Quantitative cost of one fal call against this model.                                                              |
| [`CostLine`](_autosummary/falaw.html.md#falaw.CostLine)(\*, kind, item_id, model_id, amount, ...) | One line item in a scene rollup.                                                                                   |
| [`CostRollup`](_autosummary/falaw.html.md#falaw.CostRollup)(\*, total_amount[, currency, ...])      | Result of [`estimate_scene_cost()`](_autosummary/falaw.html.md#falaw.estimate_scene_cost).                                  |
| [`ProgressEvent`](_autosummary/falaw.html.md#falaw.ProgressEvent)(\*, kind, application, call_id)      | One step in the lifecycle of a fal call.                                                                           |
| [`Scene`](_autosummary/falaw.html.md#falaw.Scene)(\*, title[, style, characters, ...])         | The whole editable structure: cast, locations, shots, beats.                                                       |
| [`Session`](_autosummary/falaw.html.md#falaw.Session)([output_dir, journal, history])            | A working session for a sequence of falaw operations.                                                              |
| [`Shot`](_autosummary/falaw.html.md#falaw.Shot)(\*, id[, description, framing, ...])          | A visual frame: framing + environment + characters in view.                                                        |
| [`ToolSpec`](_autosummary/falaw.html.md#falaw.ToolSpec)(\*, name, description, func[, ...])       | Single source of truth for a tool exposed by falaw.                                                                |
| [`Voice`](_autosummary/falaw.html.md#falaw.Voice)(\*, name[, voice_id, ...])                   | A character's voice spec.                                                                                          |
| [`LlmRate`](_autosummary/falaw.html.md#falaw.LlmRate)(\*, model, tier, per_call_usd[, ...])      | What one routed model costs, on every basis the table knows.                                                       |
| [`AreaUsage`](_autosummary/falaw.html.md#falaw.AreaUsage)(name, path, entries, bytes)              | Disk usage of one cache area.                                                                                      |
| [`CacheUsage`](_autosummary/falaw.html.md#falaw.CacheUsage)(root, areas)                            | Where the falaw cache's disk is going, by area.                                                                    |
| [`PruneCandidate`](_autosummary/falaw.html.md#falaw.PruneCandidate)(key, path, bytes[, last_modified])  | One thing a prune would delete (or did).                                                                           |
| [`PruneReport`](_autosummary/falaw.html.md#falaw.PruneReport)(area, dry_run[, candidates, ...])      | What a prune removed, or — with `dry_run=True` — would remove.                                                     |
| [`LlmReceipt`](_autosummary/falaw.html.md#falaw.LlmReceipt)(application, model, cache_hit, ...)     | What one eager LLM call cost — the record the eager path never kept.                                               |

### Exceptions

| [`FalAccountLocked`](_autosummary/falaw.html.md#falaw.FalAccountLocked)(message, \*, status_code[, ...])   | fal account is locked / suspended / awaiting verification.                   |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`FalAssetFetchError`](_autosummary/falaw.html.md#falaw.FalAssetFetchError)(message, \*, url[, cause])       | The bytes behind a fal-served asset URL could not be retrieved.              |
| [`FalClientNotInstalled`](_autosummary/falaw.html.md#falaw.FalClientNotInstalled)([what, missing])              | A real network call was attempted but the fal.ai client is not installed.    |
| [`FalBadRequest`](_autosummary/falaw.html.md#falaw.FalBadRequest)(message, \*, status_code[, ...])      | Server rejected the request payload — typical 400 / 422.                     |
| [`FalDurationOutOfRange`](_autosummary/falaw.html.md#falaw.FalDurationOutOfRange)(message, \*, model_id, ...)   | The requested duration is outside what the model can produce.                |
| [`FalError`](_autosummary/falaw.html.md#falaw.FalError)                                            | Base for all falaw-raised exceptions.                                        |
| [`FalHTTPError`](_autosummary/falaw.html.md#falaw.FalHTTPError)(message, \*, status_code[, ...])       | Wraps an HTTP error from fal.ai with the original status, body, and headers. |
| [`FalInsufficientFunds`](_autosummary/falaw.html.md#falaw.FalInsufficientFunds)(message, \*, status_code)      | Account balance is insufficient — typical 402.                               |
| [`FalModelHung`](_autosummary/falaw.html.md#falaw.FalModelHung)(message, \*, model_id, elapsed_s)      | A model was queued but never returned — distinct from a network timeout.     |
| [`FalNonCanonicalArgument`](_autosummary/falaw.html.md#falaw.FalNonCanonicalArgument)(message, \*, path)          | An argument cannot be canonicalised into falaw's hashed JSON form.           |
| [`FalPromptTooLong`](_autosummary/falaw.html.md#falaw.FalPromptTooLong)(message, \*, model, limit, ...)    | The prompt exceeds the model's hard length cap (falaw#76).                   |
| [`FalRateLimited`](_autosummary/falaw.html.md#falaw.FalRateLimited)(message, \*[, retry_after_s])        | fal is throttling requests — typical 429.                                    |
| [`FalServerError`](_autosummary/falaw.html.md#falaw.FalServerError)(message, \*, status_code[, ...])     | fal-side server error — typical 5xx.                                         |
| [`FalTimeout`](_autosummary/falaw.html.md#falaw.FalTimeout)(message, \*, elapsed_s[, application])   | The fal call timed out before producing a result.                            |
| [`FalUnauthorized`](_autosummary/falaw.html.md#falaw.FalUnauthorized)(message, \*, status_code[, ...])    | Missing or invalid API credentials — typical 401.                            |

### *class* falaw.AccountStatus(ok, locked, unauthorized, status_code, detail, url, error)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Outcome of `health_check()`.

#### detail *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Server-supplied detail string (best-effort), or ‘’ if unavailable.

#### error *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`repr` of the underlying exception when `ok` is False
and the failure isn’t a recognized lock/unauth (e.g. network error).

#### locked *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff the response indicated a locked / unverified account.

#### message_for_user()

Single human-readable line explaining the status. Stable for logging.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff the account responded without an auth/lock error.

#### status_code *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)*

HTTP status from the probe (None if no HTTP exchange happened).

#### unauthorized *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True iff credentials are missing or invalid.

#### url *: [str](https://docs.python.org/3/builtins/stdtypes.html#str) | [None](https://docs.python.org/3/builtins/constants.html#None)*

If actionable (e.g. billing dashboard), URL the user should visit.

### *class* falaw.AreaUsage(name, path, entries, bytes)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Disk usage of one cache area.

`entries` counts the area’s own unit — cache entries for `manifests`,
blobs for `content`, files for `assets` and `url_index` — not files
on disk, which is why it is reported next to `bytes` rather than derived
from it.

`path` is `None` when the area has no directory of its own. That is the
case for `manifests` (scattered across two-character shard directories at
the cache root) and for `other`. It is deliberately not the cache root:
a caller reaching for `rmtree(usage.area(...).path)` would then destroy
the content store along with it.

### *class* falaw.Asset(url, kind, content_type='', width=0, height=0, duration_s=0.0, metadata=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single piece of generated media.

Holds the URL plus minimal typed metadata. `download` materializes it.

#### download(, to=None, fix_suffix=False)

Download the asset to a file. Returns the local path.

The bytes are sniffed: some models return a different format than the
usual one (Recraft’s `vector_illustration/*` styles return SVG, not
PNG). If the suffix of `to` contradicts the content, a
`UserWarning` is issued; with `fix_suffix=True` the suffix is
corrected instead (so the returned path may differ from `to`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* falaw.Beat(, id, speaker='', line='', action='', emotion='', shot_id='', duration_s=None, notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The atomic unit of a scene: who, says what, with what intent.

A Beat that has only `action` (no `line`) is a non-verbal beat.

### *class* falaw.CacheUsage(root, areas)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where the falaw cache’s disk is going, by area.

The whole point of the breakdown: `size_bytes` alone cannot tell you
whether you are looking at gigabytes of irreplaceable blobs or gigabytes of
`assets/` copies that cost nothing to regenerate.

#### area(name)

The [`AreaUsage`](_autosummary/falaw.html.md#falaw.AreaUsage) named `name`.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no such area. The valid names are `AREA_NAMES`.
* **Return type:**
  [`AreaUsage`](_autosummary/falaw.prune.html.md#falaw.prune.AreaUsage)

#### summary()

One human-readable line per area, largest first.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### *property* total_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Every byte under the cache root.

Equal to a full walk of the root, because `other` absorbs whatever the
named areas do not claim. That identity is the point: a capacity report
whose total is “the sum of the areas I thought to enumerate” silently
under-reports the moment falaw grows a directory nobody added here, and
under-reporting is the one direction a capacity tool cannot afford.

### *class* falaw.CallOutcome(, index, call, status, artifact=None, error=None, cache_hit=False, blocked_by=(), reason='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What happened to one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan), at its position in the Plan.

`index` is the call’s position in `plan.calls` and is the identity a
caller retries or re-plans by — a report always carries exactly one outcome
per call, in plan order, so `index` is also the safe key for zipping a
Plan against anything the caller built alongside it.

#### artifact *: Artifact | [None](https://docs.python.org/3/builtins/constants.html#None)*

The materialized artifact. Set if and only if `status == "succeeded"`.

#### blocked_by *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]*

Indices of the calls whose non-success blocked this one. `()` when the
call was blocked for a run-level reason rather than a dependency.

#### cache_hit *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether the result came from the cache **as observed at run time**.

Not the same thing as [`falaw.CallPlan.cache_status`](_autosummary/falaw.html.md#falaw.CallPlan.cache_status), which is a
*prediction* made at plan time and can be wrong in both directions (a
concurrent run filled the entry; a hit turned out to be unusable and was
re-executed). Run-level cost accounting reads this one.

#### call *: [CallPlan](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)*

The call this outcome is about — enough to retry it verbatim.

#### error *: [BaseException](https://docs.python.org/3/builtins/exceptions.html#BaseException) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The exception the call raised. Set if and only if `status == "failed"`.

Kept as the exception object rather than a string so the caller can use
falaw’s typed hierarchy ([`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors)) to decide between backing off,
switching models, and giving up.

#### index *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Position in `plan.calls`. Stable, and unique within a report.

#### *property* ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Shorthand for `status == "succeeded"`.

#### reason *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Human-readable explanation. Required for `blocked`; free otherwise.

#### status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['succeeded', 'failed', 'blocked']*

`"succeeded"` / `"failed"` / `"blocked"`. See the module docstring.

### *class* falaw.CallPlan(\*, tool, application, backend='fal', arguments, output_kind, estimated_cost_usd=None, cache_status='unknown', expected_duration_s=None, metadata=<factory>, key_extra=<factory>, cost_basis=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single planned fal call. Pure data — no API contact yet.

`application` and `arguments` are the *exact* tuple
`cached_call_fal(application, arguments)` would take, so a Plan can be
cache-checked, executed, or replayed without ambiguity.

#### application *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The fal model id that will be invoked (e.g. `"fal-ai/flux/dev"`).
Backend-scoped: what it names depends on [`backend`](_autosummary/falaw.html.md#falaw.CallPlan.backend).

#### arguments *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Keyword arguments to pass to fal. Will be JSON-canonicalized for
cache key computation; should be JSON-serializable.

#### backend *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which execution backend [`execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan) dispatches this call to
(falaw#15) — see [`falaw.backends`](_autosummary/falaw.backends.html.md#module-falaw.backends). Defaults to
[`falaw.canonical.DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND) (`"fal"`), the only backend until a
second one lands. Enters the per-call cache key and `plan_hash` only
when it is **not** the default, so every call falaw has ever planned or
cached keeps its exact pre-#15 digest — see [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical).

#### *property* billable_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Cost that will actually be billed (0 on cache hit, estimate otherwise).

Returns `0.0` (not `None`) on cache hit or unknown estimate so
sums are well-defined; use [`estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) `is None` to
check unknown status explicitly.

#### cache_status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hit', 'miss', 'stale', 'unknown']*

Whether the cache will short-circuit this call. `"hit"` means
`execute` won’t bill, so [`Plan.total_cost_usd`](_autosummary/falaw.html.md#falaw.Plan.total_cost_usd) and
[`Plan.cache_hit_savings_usd`](_autosummary/falaw.html.md#falaw.Plan.cache_hit_savings_usd) reflect that.

#### cost_basis *: [CostBasis](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) | [None](https://docs.python.org/3/builtins/constants.html#None)*

How [`estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) was computed, or `None` when nothing
recorded it (falaw#60). Present, the quote can be re-run against today’s
rate table by [`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan); absent, that function reports the
call as `"no_basis"` and clears its cost to unknown rather than passing a
stale figure off as current. Purely descriptive — it stays out of
[`plan_hash()`](_autosummary/falaw.html.md#falaw.plan_hash) and the per-call cache key, and out of the serialized
dict entirely when unset, so every plan falaw has ever hashed or persisted
is unmoved.

#### estimated_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Predicted cost in USD. `None` when the model has no `cost_estimate`
populated (callers can distinguish “free” from “unknown”).

#### expected_duration_s *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`(min, max)` duration the model can produce, or `None` if no
duration contract is known. Plan-level validators can check that the
requested duration fits this range and raise [`FalDurationOutOfRange`](_autosummary/falaw.html.md#falaw.FalDurationOutOfRange)
*before* the call instead of letting it silently truncate.

#### key_extra *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Identity beyond the wire arguments — entries that change what the call
*produces* without appearing in `arguments`. Participates in the
per-call cache key AND `plan_hash`, under omit-if-empty (an empty dict
leaves every existing key byte-identical). Unlike `metadata`, which is
deliberately identity-free labelling, putting something here says “a
cached result minted without this value must not be reused”. First
customer: nw’s Transform `impl_version` (nw#27) — “same interface,
changed behaviour” must miss the cache without renaming anything.

#### metadata *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Free-form labels for downstream consumers. Conventional keys:
`shot_id`, `beat_id`, `character_name`, `strategy`.

#### output_kind *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['image', 'video', 'audio', 'json', 'text', 'binary']*

What kind of Artifact this call will produce.

#### tool *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

High-level tool name — `"generate_image"`, `"image_to_video"`, etc.
Distinct from `application` because one tool may dispatch to several
fal models depending on quality tier.

### *class* falaw.CallRepricing(, index, call, old_cost_usd, new_cost_usd, status, reason='', basis_changed=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The before/after for one call in a re-priced plan.

#### basis_changed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the rate table’s identity or version moved between the
persisted basis and today’s — the audit answer to “did the price change
because the *table* changed?”. Always `False` for `"no_basis"`, which
has nothing to compare against.

#### call *: [CallPlan](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)*

The re-priced call — the same call with today’s
`estimated_cost_usd` and a basis stamped with today’s table version.

#### *property* delta_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`new - old`, or `None` when either side is unknown.

`None` rather than `0.0`: a call that lost or gained its price did
not move by zero, and a caller summing deltas must not be handed a
number that says it did.

#### index *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Position in the original [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan).

#### new_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

What today’s rates say, `None` for unknown or no-basis.

#### old_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

What the persisted plan said, `None` if it said unknown.

#### reason *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Why, when the status is `"unknown"` or `"no_basis"`. Empty otherwise.

#### status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['unchanged', 'changed', 'unknown', 'no_basis']*

Which of the four cases this call fell into — see `RepriceStatus`.

### *class* falaw.Character(, name, description='', reference_image_url='', voice=None, style_notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A reusable character: stable face, stable voice, stable style.

### *class* falaw.ContentRef(content_hash, bytes_size)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A content-addressed handle on some bytes falaw has materialized.

`content_hash` is the SHA-256 hex digest of the bytes — the value
`lacing.Artifact.asset_id` is contractually required to hold, and the
value that goes into a downstream cache key in place of a URL.

### *class* falaw.CostBasis(\*, pricer, priced, quantities=<factory>, table='', table_version='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How a [`CallPlan.estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) was arrived at (falaw#60).

A frozen quote goes stale the moment the rate table moves — 0.0.46 moved
falaw’s LLM prices tenfold — and a persisted plan cannot be re-quoted from
`application` and `arguments` alone: the quantity hints that priced it
(the clip’s duration, the prompt’s token bound) are estimator-only and
never enter the wire arguments. This records them, plus *which* table
priced them, so a saved plan can be re-quoted faithfully
([`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan)) and audited after the fact.

Pure data. Nothing here reaches the network, and nothing here enters
[`plan_hash()`](_autosummary/falaw.html.md#falaw.plan_hash) or the per-call cache key: a basis says what a call
*cost*, never what it *produces*, so stamping one leaves every existing
digest byte-identical.

#### priced *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a `falaw.ModelRecord.id` for the
catalogue pricer, the routed `model` argument for the LLM pricer.
Distinct from [`CallPlan.application`](_autosummary/falaw.html.md#falaw.CallPlan.application), which for `fal-ai/any-llm`
names the router rather than the model that sets the price.

* **Type:**
  The entity that was priced

#### pricer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which pricing rule produced the figure — a key into
[`falaw.reprice.DFLT_PRICERS`](_autosummary/falaw.reprice.html.md#falaw.reprice.DFLT_PRICERS). `"model_catalogue"` for a record
priced out of `models.json`, `"llm_rates"` for a routed LLM priced out
of `llm_rates.json`. An unrecognized name re-prices as *unknown*, never
as the old number.

#### quantities *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

The estimator-only shape hints fed to the pricer, by keyword —
`{"seconds": 6.0}`, `{"input_tokens": 20000, "max_output_tokens": 4000}`.
Absent keys mean the hint was not supplied, which is what made a
quantity-priced call unpriceable in the first place. Omit-when-unset: a
hint that was `None` at plan time is left out rather than written as
`null`.

#### table *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Identity of the rate table consulted, e.g.
`"falaw/data/llm_rates.json"`. Free-form so a caller’s own reconciled
table can name itself.

#### table_version *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The version of that table at plan time — falaw stamps a short content
digest, so a table whose numbers moved gets a different value even when
its declared version did not. Empty means unversioned/unknown.

### *class* falaw.CostEstimate(, kind, amount, currency='USD', notes='', source='approximate')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Quantitative cost of one fal call against this model.

#### kind

How the price scales. `per_call` is the simplest:
constant per invocation regardless of size. `per_second`
applies to video / TTS where output length matters.
`per_image` covers batch image gen. `per_megapixel`
covers high-res image generation. `per_token` covers
LLM-style endpoints.

#### amount

USD (or `currency`) per unit defined by `kind`.

#### currency

ISO 4217 code. `"USD"` is the only supported value
today; the field exists so we can extend without a schema
migration.

#### notes

Free-form caveats — “rounded up to next second”, etc.

#### source

How the estimate was obtained — `"docs"`,
`"empirical"`, `"approximate"`. Lets us flag stale or
unverified entries in audits.

### *class* falaw.CostLine(, kind, item_id, model_id, amount, currency, note='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One line item in a scene rollup.

### *class* falaw.CostRollup(, total_amount, currency='USD', lines=(), skipped=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of [`estimate_scene_cost()`](_autosummary/falaw.html.md#falaw.estimate_scene_cost).

#### by_kind()

Sum per `kind` for quick inspection.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`float`](https://docs.python.org/3/builtins/functions.html#float)]

### *class* falaw.Environment(, name, description='', reference_image_url='', time_of_day='', lighting='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A reusable location/setting.

### *class* falaw.ExecutionReport(outcomes=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The result of running a Plan: one [`CallOutcome`](_autosummary/falaw.html.md#falaw.CallOutcome) per call, in order.

`len(report.outcomes) == len(plan.calls)` **always**, including on a run
where most calls failed. That invariant is the whole point: a consumer that
built something per call (nw builds one skeleton annotation per call) can
zip against [`outcomes`](_autosummary/falaw.outcomes.html.md#module-falaw.outcomes) and stay aligned. Zipping against a
*shorter* list of successes is the silent mis-pairing this type exists to
prevent.

#### artifacts_or_raise()

Every artifact in plan order, or re-raise the first failure’s exception.

The bridge back to the plain `list[Artifact]` contract of
[`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan). The exception raised is the **original** one
the call raised, unwrapped — falaw’s typed error hierarchy
([`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors)) is only useful to a caller if it survives the trip
through the executor.

A run with no failures but some blocked calls raises too: the list would
otherwise be short, and a short list is exactly the silent mis-pairing
this type exists to prevent.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[`Artifact`]

#### *property* blocked *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes whose call never ran, in plan order.

#### *property* cache_hit_savings_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Estimated USD *not* spent because a succeeded call was served from cache.

The run-time counterpart of [`falaw.Plan.cache_hit_savings_usd`](_autosummary/falaw.html.md#falaw.Plan.cache_hit_savings_usd)
(which is a plan-time prediction).

#### *property* estimated_spend_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

succeeded calls that were **not** cache hits.

Estimated, because it sums [`falaw.CallPlan.estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) —
falaw does not read fal’s invoice. Two deliberate exclusions:

- **Cache hits cost nothing**, and this reads the *observed*
  [`CallOutcome.cache_hit`](_autosummary/falaw.html.md#falaw.CallOutcome.cache_hit), not the plan-time prediction.
- **Failed calls are not counted.** The vendor may or may not have
  billed a call that raised, and falaw cannot know which; adding an
  estimate for it would be inventing a number. Read
  [`failed`](_autosummary/falaw.html.md#falaw.ExecutionReport.failed) to see how many calls are unaccounted for.

* **Type:**
  Estimated USD billed by this run

#### *property* failed *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes whose call raised, in plan order.

#### *property* has_unknown_costs *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when a call that actually billed has no price estimate.

The run-level twin of [`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs), and the
reason [`estimated_spend_usd`](_autosummary/falaw.html.md#falaw.ExecutionReport.estimated_spend_usd) must never be read on its own: an
unpriced call contributes `0.0` to the sum, so a report reading
`$0.00` means *either* “nothing was spent” *or* “we do not know what
was spent”. Those are not the same answer, and a budget gate that
cannot tell them apart approves the second one.

#### *property* is_complete *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when every call succeeded.

#### *property* produced *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[Artifact, ...]*

The artifacts that were made, in plan order.

**Shorter than the Plan when anything failed** — deliberately named so
it does not read like something to zip a per-call sequence against. Use
[`outcomes`](_autosummary/falaw.outcomes.html.md#module-falaw.outcomes) for anything positional.

#### *property* succeeded *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes that produced an artifact, in plan order.

#### summary()

A small JSON-able digest — counts, spend, and which indices failed.

For logs, telemetry and run records, where the artifacts and exception
objects themselves are not serializable.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### *exception* falaw.FalAccountLocked(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal account is locked / suspended / awaiting verification.

Typical 403 with a body indicating the account is not in good standing.
No amount of retrying or model-switching will fix this — the user has
to act (verify email, top up billing, contact support).

### *exception* falaw.FalAssetFetchError(message, , url, cause=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The bytes behind a fal-served asset URL could not be retrieved.

Raised by [`falaw.content`](_autosummary/falaw.content.html.md#module-falaw.content) when content-addressing an artifact fails —
the URL 404s (fal deletes expired files permanently), the transfer errors,
or the response is empty. It is deliberately **loud**: returning an
Artifact whose `asset_id` is not the SHA-256 of any bytes would break
`lacing.Artifact`’s content-hash contract and poison every downstream
cache key derived from it.

### *exception* falaw.FalBadRequest(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Server rejected the request payload — typical 400 / 422.

### *exception* falaw.FalClientNotInstalled(what='calling fal.ai', , missing='fal_client')

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError), [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError)

A real network call was attempted but the fal.ai client is not installed.

`pip install falaw` carries no fal.ai client, so that callers that only
build, price, hash and cache plans install none. Anything that actually
reaches fal.ai needs the `fal` extra: `pip install 'falaw[fal]'`.
It is an [`ImportError`](https://docs.python.org/3/builtins/exceptions.html#ImportError), so `except ImportError` still matches.

### *exception* falaw.FalDurationOutOfRange(message, , model_id, requested, valid_range)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The requested duration is outside what the model can produce.

Raised by Plan/Execute when the caller asks for `duration_s` that the
model’s declared `expected_duration_range` cannot satisfy. Callers can
catch this to split the shot, repeat it, or pick a different model.

### *exception* falaw.FalError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Base for all falaw-raised exceptions.

### *exception* falaw.FalHTTPError(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

Wraps an HTTP error from fal.ai with the original status, body, and headers.

Subclasses pick out specific status codes / patterns. Use this base when
you want to catch any HTTP failure (e.g. for retry).

### *exception* falaw.FalInsufficientFunds(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Account balance is insufficient — typical 402.

### *exception* falaw.FalModelHung(message, , model_id, elapsed_s)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

A model was queued but never returned — distinct from a network timeout.

Raised by higher-level orchestration that sets a per-call wall-clock
budget (e.g. “give up on this lipsync after 5 minutes and pick another model”).

### *exception* falaw.FalNonCanonicalArgument(message, , path)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

An argument cannot be canonicalised into falaw’s hashed JSON form.

Raised by [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical) when a value reaches a key-composition
site (the per-call cache key, `plan_hash`, the dry-run artifact id) that
JSON cannot represent faithfully: a non-JSON object, a non-finite float,
or a non-string mapping key. Deliberately **loud**: the old behaviour —
`json.dumps(..., default=str)` — silently collided structurally
different calls into one cache key, handing back the *wrong artifact* as
a supposed saving (falaw#17).

`path` names the offending node, e.g. `arguments.extra.ref`.

### *exception* falaw.FalPromptTooLong(message, , model, limit, length)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError), [`ValueError`](https://docs.python.org/3/builtins/exceptions.html#ValueError)

The prompt exceeds the model’s hard length cap (falaw#76).

Raised before any call is queued: fal answers an over-long prompt with an
HTTP 422 only after the request round-trips. `limit`/`length` are in
characters.

### *exception* falaw.FalRateLimited(message, , retry_after_s=None, \*\*kwargs)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal is throttling requests — typical 429.

`retry_after_s` is parsed from the `Retry-After` header if present,
else `None` (caller decides backoff).

### *exception* falaw.FalServerError(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

fal-side server error — typical 5xx. Generally retryable.

### *exception* falaw.FalTimeout(message, , elapsed_s, application=None)

Bases: [`FalError`](_autosummary/falaw.errors.html.md#falaw.errors.FalError)

The fal call timed out before producing a result.

### *exception* falaw.FalUnauthorized(message, , status_code, detail='', body=None, headers=None, application=None, url=None, cause=None)

Bases: [`FalHTTPError`](_autosummary/falaw.errors.html.md#falaw.errors.FalHTTPError)

Missing or invalid API credentials — typical 401.

### *class* falaw.LlmPrice(, provider, model, rates, long_prompt_over_tokens=None, long_prompt_rates=None, source='', date='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the first-party table: a model’s rates and where they came from.

### *class* falaw.LlmRate(, model, tier, per_call_usd, input_usd_per_mtok=None, output_usd_per_mtok=None, source='', date='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one routed model costs, on every basis the table knows.

#### model

The `model` argument value this row prices, e.g.
`"anthropic/claude-sonnet-4.5"`.

#### tier

fal’s billing tier for the router — `"standard"` or
`"premium"`. Informational; `per_call_usd` is already
materialized, so no multiplier arithmetic happens in code.

#### per_call_usd

What fal bills per request when this model is routed.

#### input_usd_per_mtok

Upstream list price per
`TOKENS_PER_RATE_UNIT` prompt tokens, or `None` when no
upstream rate is published for this id — unknown, never zero.

#### output_usd_per_mtok

The same for completion tokens.

#### source

Where these numbers came from.

#### date

ISO date they were read on. Rates move; a quote is only as
fresh as its row.

#### notes

Free-form caveats.

#### *property* has_token_rates *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when both token columns are populated, so a token quote is possible.

### *class* falaw.LlmReceipt(application, model, cache_hit, chars_in, chars_out, tokens_in=None, tokens_out=None, estimated_cost_usd=None, cost_source='unknown')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one eager LLM call cost — the record the eager path never kept.

The falaw half of falaw#50’s option B: the eager [`llm_complete()`](_autosummary/falaw.html.md#falaw.llm_complete)
returned a bare string, so LLM spend was invisible to every accounting
surface — not because estimates were `None`, but because \*nothing was
recorded at all\* (the field signature: real dollars spent beside
`plans: 0`). A caller that books receipts uses
[`llm_complete_with_receipt()`](_autosummary/falaw.html.md#falaw.llm_complete_with_receipt) and gets this alongside the text.

Field semantics, honest by construction:

- `chars_in` / `chars_out` are **measured facts** (prompt+system in,
  response text out).
- `tokens_in` / `tokens_out` are **best-effort** from the raw
  response’s usage block (OpenAI `prompt_tokens`/`completion_tokens`
  and Anthropic `input_tokens`/`output_tokens` shapes). `None`
  means *unrecorded by the provider response*, never zero.
- `estimated_cost_usd` is `0.0` on a cache hit (nothing was billed),
  else fal’s published per-request price **for the routed model**, read
  off the rate table ([`falaw.llm_ceiling_usd()`](_autosummary/falaw.html.md#falaw.llm_ceiling_usd)) — an estimate, not
  an observed bill. It is the routed model and not the `fal-ai/any-llm`
  record because the record carries fal’s *standard* tier, and a premium
  routed model bills ten times that: pricing every turn from the record
  under-reported premium spend tenfold in the ledger a consumer sums
  (falaw#55). `None` only when nothing can price it at all.
- `cost_source` says which of those cases produced the number, so a
  receipt consumer never has to guess: `"cache_hit"`,
  `"rate_table:per_call"`, `"registry:<kind>"` (the fallback for a
  model the table does not price), or `"unknown"`.

### *class* falaw.ModelRecord(\*, id, category, description='', aliases=(), quality_tier='', cost_hint='', cost_estimate=None, docs_url='', max_clip_seconds=None, single_character_recommended=False, supported_resolutions=(), default_negative_prompt='', max_prompt_chars=None, size_param='image_size', param_specs=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One entry in the fal model catalog.

#### default_negative_prompt *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Quality/realism negatives worth appending by default (e.g. to avoid the
plastic-skin look). Empty when none.

#### max_clip_seconds *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Practical max length of a single generated clip, in seconds (e.g. ~10
for Seedance). Drives the “this shot is too long, split it” warning.

#### max_prompt_chars *: [int](https://docs.python.org/3/builtins/functions.html#int) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Hard cap on `prompt` length, from the model’s OpenAPI schema. Calls
over it fail at fal with an HTTP 422 after queueing; falaw raises first.

#### param_specs *: [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Any](https://docs.python.org/3/library/typing.html#typing.Any)]]*

Discoverable per-model parameters that matter for choosing well, as
`{name: {"enum": [...], "default": ...}}` — e.g. Recraft’s `style`,
whose photoreal default silently overrides a woodcut prompt. See
[`falaw.model_params()`](_autosummary/falaw.html.md#falaw.model_params).

#### single_character_recommended *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the model handles a single character per shot far better than
multiple interacting ones — drives the “two characters, consider
shot/reverse-shot” warning.

#### size_param *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

`"image_size"` (fal’s named
presets, the default) or `"aspect_ratio"` (+ `resolution`).

* **Type:**
  Which argument carries the output size

#### supported_resolutions *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]*

Resolutions the model offers, cheap→expensive (e.g. (“720p”, “1080p”)).

### *class* falaw.Plan(calls=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An ordered sequence of [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) — a render plan, in essence.

Plans compose: `a + b` returns a new Plan with `a.calls` followed by
`b.calls`. `Plan(calls=())` is the identity. Plans are frozen, so
edits return new Plans (use [`with_call_replaced()`](_autosummary/falaw.html.md#falaw.Plan.with_call_replaced) for in-place-feel).

#### *property* cache_hit_savings_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

USD that would have been spent without the cache.

Equal to `sum(c.estimated_cost_usd for c in calls if c.cache_status == "hit"
and c.estimated_cost_usd is not None)`.

#### *property* has_unknown_costs *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True if any non-cache-hit call has no cost estimate.

Use this to refuse to gate on a budget when the estimate is incomplete.

#### *property* known_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The priced part of [`total_cost_usd`](_autosummary/falaw.html.md#falaw.Plan.total_cost_usd) — same number today,
but honest by construction.

`total_cost_usd` coerces unpriced calls to `$0.00` so sums stay
well-defined; a cumulative budget gate that reads it alone will
under-quote (400 unpriceable video calls total $0.00). Read this
together with [`unknown_call_count`](_autosummary/falaw.html.md#falaw.Plan.unknown_call_count): the true cost is
`known_cost_usd` *plus an unknown amount* spread over that many
calls, and a correct gate refuses when the count is nonzero rather
than pretending the unknown part is free (falaw#18).

#### *property* total_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Sum of [`CallPlan.billable_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.billable_cost_usd) across all calls.

#### *property* unknown_call_count *: [int](https://docs.python.org/3/builtins/functions.html#int)*

How many billable calls carry no price at all.

The countable form of [`has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs) — see
[`known_cost_usd`](_autosummary/falaw.html.md#falaw.Plan.known_cost_usd) for the budget-gate arithmetic it enables.

#### with_call_replaced(index, new_call)

Return a new Plan with `calls[index]` replaced.

* **Return type:**
  [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)

### *class* falaw.Pricer(\*, quote, table, version=<function Pricer.<lambda>>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One pricing rule, and the table that backs it.

The seam for a caller with their own numbers: build a `Pricer` closing
over a reconciled rate table and pass `pricers={\*\*DFLT_PRICERS,
LLM_RATES_PRICER: mine}` to [`reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan). The `table` /
`version` pair is stamped onto the re-priced call’s basis, so the next
audit can see which table produced which number.

`table` is also a **gate, not just a label**: a call whose basis names a
different table is reported as `unknown` rather than re-quoted, so a row
priced from your invoice is never silently re-quoted at falaw’s published
rate (or the reverse). Give your `Pricer` the same `table` string the
basis carries — [`falaw.llm_rates.CUSTOM_LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.CUSTOM_LLM_RATES_TABLE) for anything
a `plan_*` priced with `llm_rates=`.

#### quote *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[CostBasis](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)], [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)]*

Price `basis` in USD, or return `None` for *unknown*. May raise; a
raise is reported as `"unknown"` with the exception text as the reason,
so one unpriceable row never aborts a whole plan’s re-quote.

#### table *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Identity of the table this pricer reads, for the refreshed basis.

#### version *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Today’s version of that table, for the refreshed basis.

### *class* falaw.ProgressEvent(, kind, application, call_id, message='', pct=None, elapsed_s=0.0)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One step in the lifecycle of a fal call.

#### kind

Lifecycle stage. See `EventKind`.

#### application

fal model id (e.g. `"fal-ai/flux/dev"`).

#### call_id

A short hex string that uniquely identifies the call.
All events for one `call_fal` invocation share the same
`call_id`.

#### message

Free-form text. For `"log"` events this is the log
line; for `"error"` it’s `repr(exc)`; otherwise empty.

#### pct

Optional progress percentage in [0.0, 100.0]. fal’s
current API doesn’t surface this; included for forward
compatibility.

#### elapsed_s

Seconds since the call started.

### *class* falaw.PruneCandidate(key, path, bytes, last_modified=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One thing a prune would delete (or did).

`last_modified` is `None` when the age could not be determined — a
non-filesystem blob backend, or a manifest too corrupt to read. Such a
candidate is never selected by `older_than` (an unprovable age is not
evidence of staleness) and is evicted last under `max_bytes`.

### *class* falaw.PruneReport(area, dry_run, candidates=(), deleted=(), kept_entries=0, kept_bytes=0, rebillable_entries=0, unreferenced_candidates=0, errors=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a prune removed, or — with `dry_run=True` — would remove.

`rebillable_entries` is the number this exists for: cache entries that
will **cost money again** because of this prune. Its meaning is exact per
area, and each is a different claim:

- `manifests` — every dropped entry re-bills, so it equals the candidate
  count.
- `content` — entries whose recorded response names an asset that resolves
  (through the `url -> hash` index) to a blob being dropped. Those become
  *unmaterializable* from cache; they re-download if fal still serves the
  URL and re-render if it does not, and falaw cannot tell which from here.
  It mirrors the predicate [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) applies to a hit from
  the **built-in** converter. It is therefore an *upper* bound, not an exact
  population: with a custom `artifact_converter=` the entry is never
  dropped (it returns a broken artifact instead), and with
  `fetch_bytes=False` dropping the blob changes nothing. Both errors are
  overcounts — the safe direction for a number you read before spending.
- `assets` — only the copies whose blob is *also* gone. While the blob
  survives, re-materializing is a local copy and costs nothing.

`unreferenced_candidates` is the other half of the story, and only
`content` populates it: blobs that **no manifest points at**. falaw cannot
tell whether such a blob is garbage or the last copy of something
irreplaceable — [`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset) puts reference images and
locally-rendered `file://` media in the same store, and those never had a
fal response behind them. Counting them as `rebillable` would be wrong
(no cache entry re-bills), but reporting nothing would tell an operator the
prune is free when it may be destroying the only copy of a reference image.

#### *property* freed_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Bytes actually freed — or, under `dry_run`, that would be freed.

Sums `deleted`, **not** `candidates`. A deletion that failed (a
read-only volume, a permission error) leaves its bytes on disk, and a
capacity tool that reports them as reclaimed tells an operator staring
at a full disk that the problem is solved when it is not.

#### summary()

A human-readable line, phrased in the tense the run actually was.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* falaw.RepricedPlan(, plan, calls=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of [`reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) — the new Plan plus the per-call diff.

Read [`plan`](_autosummary/falaw.plan.html.md#module-falaw.plan) for the re-quoted plan (it is an ordinary
[`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan), so `known_cost_usd` / `unknown_call_count` /
`has_unknown_costs` mean what they always mean), and [`calls`](_autosummary/falaw.html.md#falaw.RepricedPlan.calls) for
what changed and why.

#### calls *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

One entry per call of the input plan, in order.

#### *property* changed *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

Calls whose price moved — the ones a re-quote exists to surface.

#### *property* known_delta_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Sum of [`CallRepricing.delta_usd`](_autosummary/falaw.html.md#falaw.CallRepricing.delta_usd) over calls priced both times.

The movement you can actually see. It says nothing about the calls in
[`unpriced`](_autosummary/falaw.html.md#falaw.RepricedPlan.unpriced) — read that alongside it, never this alone.

#### plan *: [Plan](_autosummary/falaw.plan.html.md#falaw.plan.Plan)*

The re-priced plan. Structurally identical to the input — same calls,
same order, same arguments, so `plan_hash` is unmoved — with today’s
costs.

#### *property* unpriced *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

`"unknown"` plus `"no_basis"`.

A budget gate refuses while this is non-empty, the same way it refuses
on [`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs) — the two are the same
judgement, one made at re-quote time.

* **Type:**
  Calls today’s rates cannot price

### *class* falaw.Result(assets=<factory>, raw=<factory>, application='', arguments=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A fal call result with parsed assets and the original raw response.

The raw response is kept so callers can inspect provider-specific fields
(timings, seed, has_nsfw_concepts, …) that we do not normalize.

### *class* falaw.Scene(, title, style='', characters=(), environments=(), shots=(), beats=(), notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The whole editable structure: cast, locations, shots, beats.

#### with_beat(beat)

Return a new Scene with `beat` replacing any existing beat with the same id.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### *class* falaw.Session(output_dir=<factory>, journal=<factory>, history=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A working session for a sequence of falaw operations.

```pycon
>>> import tempfile
>>> s = Session(output_dir=tempfile.mkdtemp())
>>> s.history
[]
```

### *class* falaw.Shot(, id, description='', framing='medium', environment='', characters=(), camera='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A visual frame: framing + environment + characters in view.

Beats anchor to a Shot via `shot_id`. The Shot itself has its own
rendered output (still or short clip used as the anchor for beat
lipsync renders).

### *class* falaw.ToolSpec(\*, name, description, func, input_schema=<factory>, output_schema=<factory>, tags=(), examples=(), version='0.0.1')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Single source of truth for a tool exposed by falaw.

A ToolSpec is what the registry stores. Bridges read it to produce
Claude-skill instructions, MCP tool descriptors, HTTP endpoints, etc.

### *class* falaw.Voice(, name, voice_id='', reference_audio_url='', model_id='', style_notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A character’s voice spec.

Three modes, choose any:

* `voice_id` — model-side voice id (e.g. ElevenLabs voice).
* `reference_audio_url` — a few seconds of audio to clone.
* `model_id` — override the default TTS model for this voice.

Always provide `name` for stable, human-readable referencing.

### falaw.animate_face(image_url, audio_url, , prompt='', quality='balanced', model_id=None, extra=None)

Animate a still face from audio. Image + audio → talking video.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.apply_note_to_beat(beat, note, , model='anthropic/claude-sonnet-4.5')

Use the LLM to apply a directorial note to a Beat.

* **Return type:**
  [`Beat`](_autosummary/falaw.scene.html.md#falaw.scene.Beat)

### falaw.apply_note_to_scene(scene, note, , model='anthropic/claude-sonnet-4.5')

Apply a cross-cutting note: LLM proposes per-beat edits, we apply them.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### falaw.beat_content_hash(beat, , character=None)

Hash everything that affects how the beat *renders*.

Includes the beat’s content + the character’s identity anchors
(face image, voice spec). Style/emotion changes invalidate the
cache; pure id renames do not.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.cache_get(application, arguments, , backend='fal', key_extra=None)

Return the raw fal response if cached, else None.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.cache_put(application, arguments, raw, , note='', wire_arguments=None, backend='fal', key_extra=None)

Persist a fal response. Returns the entry directory path.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id.
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – the arguments the entry is **keyed** on.
  * **raw** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)) – the fal response to store.
  * **note** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – free-form label recorded in the manifest.
  * **wire_arguments** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – the arguments actually sent to fal, when they differ
    from the key arguments (chained calls send URLs but are keyed on
    content hashes). Recorded for debugging only — it never affects
    the key.
  * **backend** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – which execution backend produced `raw` (falaw#15). Joins
    the cache key only when non-default, same rule as
    [`falaw.canonical.cache_key_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.cache_key_payload); recorded in the
    manifest under the same condition, for debugging.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

The manifest is written to a temporary file and moved into place with
[`os.replace()`](https://docs.python.org/3/library/os.html#os.replace), so a reader never sees a half-written entry. That is not
hypothetical since `execute_plan(concurrency=N)`: two calls of one Plan
that are structurally identical land on the same key, and an interleaved
`json.dump` would leave a permanently unparseable entry — a cache that
poisons itself under exactly the fan-out it exists to make cheap.

### falaw.cache_stats()

Quick summary of the cache: entry count, disk usage, and where it went.

`areas` is the part worth reading. Since falaw#14 the cache holds the
*bytes* of every generated asset, so a total on its own cannot distinguish
gigabytes of irreplaceable blobs from gigabytes of `assets/` copies that
cost nothing to regenerate — and only the second is safe to reclaim without
thinking. Each area’s economics, and the primitives that reclaim it, are in
[`falaw.prune`](_autosummary/falaw.prune.html.md#module-falaw.prune).

`size_bytes` is every byte under the cache root, unchanged in meaning from
before the breakdown existed: the `other` area absorbs whatever the named
areas do not claim, so the areas always sum to the whole.

(`+SKIP`ed — it reads the caller’s real cache, a multi-gigabyte walk on
the production box. `tests.test_prune` pins this against a throwaway
cache instead.)

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> stats = cache_stats()
>>> sorted(stats["areas"])
['assets', 'content', 'manifests', 'other', 'scenes', 'url_index']
```

### falaw.cache_usage()

Disk usage of the falaw cache, broken down by area.

The structured form behind [`falaw.cache_stats()`](_autosummary/falaw.html.md#falaw.cache_stats). Read the module
docstring for what each area costs to reclaim.

* **Return type:**
  [`CacheUsage`](_autosummary/falaw.prune.html.md#falaw.prune.CacheUsage)

### falaw.cached_call_fal(application, arguments, , key_arguments=None, refresh=False, on_event=None, backend='fal', key_extra=None)

Call a fal model, but reuse the cached response when present.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id.
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – model input dict — what is sent **on the wire**.
  * **key_arguments** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – what the cache entry is **keyed** on, when that differs
    from what goes on the wire. Defaults to `arguments`. The split
    exists because a chained call must send fal an expiring URL for its
    upstream input while being keyed on that input’s *content hash*, so
    a byte-identical upstream regeneration hits instead of re-billing.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – if True, bypass the cache and overwrite it with a fresh result.
  * **on_event** – Per-call subscriber for [`falaw.events.ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent).
    On a cache hit, a synthetic `cache_hit` event is emitted so
    UIs can show “skipped” instead of “running”.
  * **backend** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – which execution backend serves this call (falaw#15) —
    resolved via [`falaw.backends`](_autosummary/falaw.backends.html.md#module-falaw.backends). Also joins the cache key
    (non-default only), so two backends never share an entry. Despite
    the name, this function is no longer fal-specific; the name is
    kept because “fal” is still the only backend and every existing
    call site already spells it this way.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  Raw response (whether from cache or network).

### falaw.call_fal(application, arguments, , on_log=None, on_event=None, with_logs=True, journal_errors=True, api_key=None)

Call a fal model via fal_client.subscribe.

* **Parameters:**
  * **application** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – fal model id (e.g. `"fal-ai/flux/dev"`).
  * **arguments** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]) – Input arguments. Keys depend on the model.
  * **on_log** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Legacy log callback — receives raw log lines as strings.
    Defaults to no-op (use `on_event` for structured access).
  * **on_event** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]]) – Per-call subscriber for `ProgressEvent`s. Fires
    in addition to the global subscribers registered via
    :func:`falaw.events.subscribe`.
  * **with_logs** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Pass through to fal_client; when True the model streams logs.
  * **journal_errors** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – When True, exceptions are recorded as journal issues
    before being re-raised. The journal entry includes the application
    id and arguments so future agents can recognize the same trap.
  * **api_key** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Explicit fal key for this call. When `None` (default) the
    key bound via [`using_fal_credentials()`](_autosummary/falaw.html.md#falaw.using_fal_credentials) is used; when that is
    also unset, the fal SDK’s own `FAL_KEY` env-var lookup applies
    (the historical behaviour). A resolved key is used per-call via a
    dedicated `fal_client.SyncClient` — it is never written to a
    global or an env var.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  The raw response dict from the model.

### falaw.call_plan_from_dict(d)

Rebuild a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) from a [`call_plan_to_dict()`](_autosummary/falaw.html.md#falaw.call_plan_to_dict) dict.

`arguments` / `metadata` are copied (a deserialized plan owns its own
data); `expected_duration_s` is re-tupled. `backend` defaults to
`DFLT_BACKEND` when absent — a dict written before falaw#15 names no
backend, and it always meant `"fal"`.

`cost_basis` is absent on any plan written before falaw#60 and on any
call nothing stamped; it comes back as `None`, which
[`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) reports as `"no_basis"` — never as a quote
that is still current.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.call_plan_to_dict(call)

Convert a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) to a plain JSON-serializable dict.

The inverse of [`call_plan_from_dict()`](_autosummary/falaw.html.md#falaw.call_plan_from_dict). `expected_duration_s` (a
`tuple`) becomes a 2-element list since JSON has no tuple type;
everything else is already JSON-native.

`backend` (falaw#15) is a **tolerated-default addition**, not a
`PLAN_DICT_SCHEMA` bump: it is always written, but
[`call_plan_from_dict()`](_autosummary/falaw.html.md#falaw.call_plan_from_dict) defaults it to `DFLT_BACKEND` when
absent, so a dict from before this field existed still parses, and a
dict written by this version still parses under older falaw (the extra
key is simply never read there). No migration needed either direction.

`cost_basis` (falaw#60) is the same kind of addition under a stricter
rule — **omit-when-unset**: the key is written only when a basis exists, so
a plan built without one serializes to the exact bytes it did before the
field existed, and every stored plan, fixture and cassette is unmoved.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.cast_character(name, description, , image_url='', style='', quality='high', voice_id='', reference_audio_url='', voice_style='')

Create a Character with a canonical face (and optional voice).

If `image_url` is given, we skip face generation and use that
image directly. Otherwise we run text-to-image with the description
(+ optional `style` suffix), cache the result, and use the URL.

* **Return type:**
  [`Character`](_autosummary/falaw.scene.html.md#falaw.scene.Character)

### falaw.cast_voice(character, , voice_id='', reference_audio_url='', style_notes='', model_id='')

Attach or update the Voice on a Character.

* **Return type:**
  [`Character`](_autosummary/falaw.scene.html.md#falaw.scene.Character)

### falaw.catalogue_cost_basis(model_id, \*\*quantities)

A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a call priced from `models.json`.

`quantities` are [`falaw.estimate_call_cost()`](_autosummary/falaw.html.md#falaw.estimate_call_cost) keywords (`count`,
`seconds`, `megapixels`, `tokens`); `None` values are dropped, so a
hint that was never supplied is recorded as absent rather than as `null`
— the same thing the estimator saw.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

```pycon
>>> catalogue_cost_basis("fal-ai/flux/dev", seconds=None).quantities
{}
```

### falaw.check_model_liveness(ids=None, , status_of=None)

`{endpoint_id: is_live}` for `ids` (default: every catalogued model).

Only a 404 means dead; any other non-200 answer (rate limit, fal outage)
raises rather than report a live model as dead, which would get it deleted.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### falaw.clear_subscribers()

Drop all registered subscribers. Mostly for tests.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.composite_character_in_environment(character_image_url, environment_image_url, , prompt='', quality='balanced', model_id=None, extra=None)

Place a character into an environment as one composited still.

The single most user-visible primitive missing from muvid (per
interface_design_plan item E). With this, an agent can produce
“Thor in a bell tower” — the character anchor for a downstream
omnihuman lipsync — without manually compositing in an image editor.

Defaults to `fal-ai/flux-kontext/dev` (image_edit category at
balanced/high tier). Pass `model_id` to override
(e.g. `"fal-ai/flux-pro/kontext/max"` for highest quality, or
`"fal-ai/bytedance/seededit/v3/edit-image"` for SeedEdit).

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.content_ref_for_url(url, , store=None, fetcher=None, refresh=False, assume_immutable=None)

Materialize `url`’s bytes into `store` and return their content hash.

Idempotent and cheap on repeat, but *how* cheap depends on whether the URL
can change behind falaw’s back — and falaw decides that rather than asking
the caller to know (thorwhalen/falaw#23):

1. **Immutable URL** (fal’s own — see `is_immutable_url()`) with a
   remembered hash whose blob is present: returned immediately, \*\*no
   network\*\*. This is what makes re-executing an already-cached plan free
   rather than re-downloading every clip.
2. **Mutable URL** with a remembered hash: falaw **revalidates** — a
   conditional `GET` replaying the recorded `ETag` / `Last-Modified`
   (for `file://`, a `(mtime, size)` comparison). A `304` costs one
   round-trip and no payload; a `200` means the bytes really changed and
   the new ones are stored, so the content hash changes with them.
3. **No usable answer** — nothing remembered, no validators recorded, or a
   transport that cannot make conditional requests: a plain fetch.

Step 3 is the important default. A transport that cannot revalidate makes
falaw **re-fetch**, never trust: an unverifiable hint is not evidence.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The asset URL. `file://` is supported and treated as mutable.
  * **store** – Injected `lacing.ArtifactStore`; defaults to
    [`default_content_store()`](_autosummary/falaw.html.md#falaw.default_content_store).
  * **fetcher** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]]) – Injected byte source; defaults to [`default_url_fetcher()`](_autosummary/falaw.html.md#falaw.default_url_fetcher)
    (the built-in `urllib` transport unless [`using_url_fetcher()`](_autosummary/falaw.html.md#falaw.using_url_fetcher)
    has installed another). To support revalidation, a custom transport
    exposes a `conditional_fetch(url, validators) -> ConditionalOutcome`
    attribute; without one it is simply never asked.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Skip every shortcut and re-fetch unconditionally. Rarely needed
    now that mutable URLs revalidate on their own — keep it for a URL
    whose origin lies about its validators.
  * **assume_immutable** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool)]) – Override the host-based decision. `True` restores
    the old unconditional trust (use for a host you mint yourself, and
    prefer adding it to `IMMUTABLE_URL_HOSTS`); `False` forces
    revalidation even for fal.
* **Raises:**
  [**FalAssetFetchError**](_autosummary/falaw.html.md#falaw.FalAssetFetchError) – the bytes could not be retrieved, or the response
      was empty. Never returns a reference it could not back with bytes —
      a silent zero-byte “artifact” is the failure mode this guards.
* **Return type:**
  [`ContentRef`](_autosummary/falaw.content.html.md#falaw.content.ContentRef)

### falaw.cost_basis_from_dict(d)

Rebuild a [`CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) from a [`cost_basis_to_dict()`](_autosummary/falaw.html.md#falaw.cost_basis_to_dict) dict.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

### falaw.cost_basis_to_dict(basis)

Convert a [`CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) to a plain JSON-serializable dict.

`table` / `table_version` are omitted when empty, matching the
omit-when-unset discipline the rest of the wire shape follows — a basis
that names no table writes no key rather than a pair of `""`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.current_fal_key()

The fal API key bound for the current context, or `None`.

Resolution order callers should mirror: an explicit `api_key` argument
to [`call_fal()`](_autosummary/falaw.html.md#falaw.call_fal) wins over this context value, which in turn wins over
the fal SDK’s own `FAL_KEY` env-var lookup.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.dead_models(ids=None, , status_of=None)

The ids from [`check_model_liveness()`](_autosummary/falaw.html.md#falaw.check_model_liveness) that are no longer deployed.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.default_content_store()

The falaw-cache-rooted `lacing.ArtifactStore`.

Rooted at `<falaw cache dir>/content`, so it moves with
`$FALAW_CACHE_DIR` / `$FALAW_DATA_DIR` like every other piece of falaw
state. Constructed per call (the constructor only ensures directories
exist) so a test that re-points the cache dir gets a fresh store.

**Deleting a blob removes it from disk** rather than moving it to the OS
trash. `dol.Files` — the blob backend `from_directory` lays down —
trashes by default, including on Linux, which is a kind default for a
user’s own files and the wrong one here: every blob is derived data, and
the only thing in falaw that deletes one is [`falaw.prune.prune_content()`](_autosummary/falaw.prune.html.md#falaw.prune.prune_content),
whose whole job is to give the volume its space back. A trashed blob frees
nothing (the trash usually lives on the same volume) while the prune report
says it did (thorwhalen/falaw#66). The deletion is still an explicit,
`dry_run`-by-default operator act, so the trash was never the safety net.

### falaw.default_url_fetcher()

The transport used when no `fetcher=` argument is given.

[`using_url_fetcher()`](_autosummary/falaw.html.md#falaw.using_url_fetcher)’s installed fetcher if there is one, else the
built-in `urllib`-based one. Resolved at call time, so every falaw entry
point that reads asset bytes — [`content_ref_for_url()`](_autosummary/falaw.html.md#falaw.content_ref_for_url),
[`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset), [`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan) and
[`falaw.execute_plan_isolated()`](_autosummary/falaw.html.md#falaw.execute_plan_isolated) — honours an override installed after
they were imported.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]

### falaw.drop_cache_entry(application, arguments, , backend='fal', key_extra=None)

Delete the cache entry for `(application, arguments)`. Returns whether one existed.

The counterpart to [`cache_put()`](_autosummary/falaw.html.md#falaw.cache_put), and the mechanism that keeps a cache
from becoming a *trap*. An entry whose response can no longer be turned
into a usable artifact — fal deleted the URL and the bytes are not in the
content store — must be a **miss**, not a permanent failure: without this,
escaping one dead call means re-running the whole plan.
[`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) calls it.

The plan-level escapes are `execute_plan(plan, refresh=True)`, which
re-runs every call and **keeps** what it pays for, and
`execute_plan(plan, use_cache=False)`, which re-runs and keeps nothing —
so the second one bills again on the next run (falaw#49). Neither is a
substitute for this function: both re-bill *every* call in the plan, and
this drops the one entry that actually went bad.

Only the manifest is removed. Blobs in the content store are shared by
content hash across entries and are never dropped from here.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### falaw.edit_image(image_url, prompt, , quality='balanced', model_id=None, extra=None)

Edit an image with a natural-language instruction.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.establish_environment(name, description, , time_of_day='', lighting='', image_url='', quality='high')

Create an Environment with a canonical establishing image.

* **Return type:**
  [`Environment`](_autosummary/falaw.scene.html.md#falaw.scene.Environment)

### falaw.estimate_call_cost(record, , count=1, seconds=None, megapixels=None, tokens=None)

Cost of one fal call against `record`.

Returns `None` when the cost is **unknown**, so callers can
distinguish “free” from “we can’t say”. That happens when the record
carries no `cost_estimate` at all, or when its pricing is
quantity-based (`per_second` / `per_token`) and the caller did
not supply the quantity — an unpriceable call, not a free one.

Returning `0.0` for a missing quantity would be actively dangerous:
a `per_second` clip is the single most expensive thing fal bills
for, and a caller gating a budget on the answer would read
“$0.00” and spend real money without a prompt. `None` instead
propagates to `CallPlan.estimated_cost_usd` and lights up
`Plan.has_unknown_costs`, which exists for exactly this case.
Callers that know the quantity should pass it; `per_second` callers
can fall back to `record.max_clip_seconds` for an upper bound.

`per_megapixel` is deliberately different: an image’s pixel budget
has a sane house default (below), so it stays priceable.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### falaw.estimate_scene_cost(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, shot_seconds=None)

Estimate the USD cost of a full [`render_scene()`](_autosummary/falaw.html.md#falaw.render_scene) invocation.

Walks every shot + beat with the same `pick_model` semantics
the renderer uses, then sums per-call costs. Returns a structured
[`CostRollup`](_autosummary/falaw.html.md#falaw.CostRollup) with per-line breakdowns, plus a list of
“skipped” entries the caller should surface (typically: a model
with no `cost_estimate` populated).

`shot_seconds` is the assumed clip length used to price
`shots_as_video`. A [`Shot`](_autosummary/falaw.html.md#falaw.Shot) carries no duration of its own —
screen time comes from the renderer’s per-shot run — and
image-to-video models bill **per second**, so without this the video
lines are genuinely unpriceable and are reported in
`CostRollup.skipped` rather than silently priced at $0.00.
Pass the length you expect (the beat path has the same knob as
`estimated_seconds`).

* **Return type:**
  [`CostRollup`](_autosummary/falaw.cost.html.md#falaw.cost.CostRollup)

### falaw.execute_plan(plan, , on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1)

Execute a Plan, returning a list of materialized :class:

```
`
```

lacing.Artifact\`s.

This is the **halt** policy: the first call that raises ends the run, and its
exception propagates unchanged (falaw’s typed hierarchy — see
[`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors) — is what a caller classifies on, so it is never
wrapped). Use `execute_isolated()` when one bad call must not discard
the rest of a fan-out; it returns an [`ExecutionReport`](_autosummary/falaw.html.md#falaw.ExecutionReport) with one
outcome per call instead of raising.

* **Parameters:**
  * **plan** (Plan) – The Plan to execute.
  * **on_event** (Optional[Callable]) – Optional per-call event subscriber (passed to `call_fal`).
  * **dry_run** (bool) – When True, no fal calls are made; synthetic Artifacts are
    returned with placeholder `asset_id` and `url=None`. Useful
    for exercising downstream composition without an API key.
  * **use_cache** (bool) – When True (default), executes via `cached_call_fal` so
    cache hits skip the network. When False, the cache is not touched
    **at all** — neither read nor written. That is the historical
    meaning and it is unchanged: a caller who genuinely wants no cache
    interaction still has it. To re-run a plan and *keep* what the
    re-run paid for, leave `use_cache=True` and pass `refresh`.
  * **refresh** (bool) – When True, skip the cache **read** and keep the cache
    **write** — the third mode (falaw#49). Every call executes fresh,
    and every fresh result is stored under the key it would have been
    read from, so the next run (or a downstream consumer) hits it.
    Requires `use_cache=True`, since there is no key to write under
    when the cache is off; the contradictory combination raises rather
    than silently picking one. This is the mode to reach for behind a
    `force` / “re-verify this” switch: without it, forcing a re-run
    discards the result it just paid for and guarantees the next
    consumer re-bills.
  * **artifact_converter** (Optional[ResultToArtifact]) – Per-CallPlan converter from raw fal response to
    `lacing.Artifact`. When `None` (default), a built-in
    converter handles the common shapes (`{images: [{url}]}`,
    `{video: {url}}`, `{audio: {url}}`). Mutually exclusive with
    `content_store` / `fetch_bytes` / `asset_fetcher`, which
    configure the built-in converter only — passing both raises, rather
    than silently ignoring the ones a custom converter cannot honour.
    Converters do not own `cost_usd`: the executor stamps it from
    the observed run outcome after conversion, overwriting whatever
    the converter set (falaw#26).
  * **content_store** – Injected `lacing.ArtifactStore` that media bytes
    are materialized into. Defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store) (a directory store
    rooted in the falaw cache). Point this at an S3-backed store to
    share content — and therefore cache hits — across machines.
  * **fetch_bytes** (Optional[bool]) – Whether to download each media result so its `asset_id`
    is the SHA-256 of its bytes. Defaults to `DFLT_FETCH_BYTES`
    (true), overridable process-wide via `FETCH_BYTES_ENVVAR`.
    **Opting out forfeits caching for chained calls**: without bytes
    there is no content hash, so downstream calls fall back to keying
    on the upstream URL — which fal mints fresh per upload, so the
    downstream entry can never be reused across runs or machines. It
    also means `asset_id` is *not* a content hash, in violation of
    `lacing.Artifact`’s contract. Use it only when you genuinely
    want URL-only artifacts and no reuse.
  * **asset_fetcher** – Injected byte source (`url -> Iterable[bytes]`) used to
    read media results; defaults to
    [`falaw.content.default_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.default_url_fetcher). This is the per-call
    transport seam. A **hermetic test suite** usually wants the
    process-wide one instead — [`falaw.testing.fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets), built
    on [`falaw.content.using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher) — because a suite
    reaching falaw *through* its own public API has no `execute`
    call site to pass this to. Passing it here still wins over any
    installed default. (`$FALAW_FETCH_ARTIFACT_BYTES=0` also silences
    the network, but by turning content addressing off — see
    `FETCH_BYTES_ENVVAR`.)
  * **concurrency** (int) – How many calls may be in flight at once. `1`
    (`DFLT_CONCURRENCY`) runs the Plan sequentially, on the
    calling thread, exactly as it always has. Above 1, independent calls
    run on a thread pool bounded by this number — a Plan is I/O-bound
    (an HTTP request that fal takes tens of seconds to answer), so
    threads are the right tool and the bound is what keeps a 200-call
    fan-out from becoming 200 simultaneous paid requests. \*\*Chained
    calls are never parallelised with their producers\*\*: a call holding
    a `"<from N>"` placeholder waits for call `N`. Two things to
    weigh before raising it: the vendor’s rate limit, and memory —
    materializing a media result peaks at roughly twice the asset’s size
    (thorwhalen/lacing#25), so `concurrency` multiplies the peak.
* **Return type:**
  [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)

## Failure handling — a paid result is never discarded

Two different things can go wrong when reading a result’s bytes, and they
get two different answers:

- **A fresh call whose bytes cannot be fetched.** fal has already run — and
  billed — the generation. Raising would throw away a result we paid for,
  typically over a transient network failure. So the artifact **degrades**:
  `url` is kept, `bytes_size` stays 0, `asset_id` is a digest of the
  response and is *not* claimed to be a content hash, and a
  [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning) is emitted. Downstream key resolution reads
  > `bytes_size == 0` and falls back to the URL — a guaranteed cache
  > *miss*, never a wrong hit.
- **A cache hit that cannot be materialized** — fal deleted the URL and the
  bytes are not in the content store. The entry is unusable, so it is
  treated as a **miss**: it is invalidated and the call re-executed once.
  A cache must never become a trap whose only escape is re-billing the
  whole plan with `use_cache=False`.

## Placeholder resolution — the wire/key split

Any string argument equal to `"<from N>"` (for an integer `N`) is
rewritten *just before* the call is made — so a multi-step plan (e.g.
generate_image → image_to_video) can reference the upstream output without
the planner needing to know its URL. The rewrite happens after the upstream
call has executed; planning itself is unaffected.

It happens **twice**, into two different argument sets, because the same
value cannot serve both jobs:

- the **wire** arguments get `artifacts[N].url` — what fal needs in order
  to fetch the input;
- the **key** arguments get `sha256:<artifacts[N].asset_id>` — the
  upstream’s content hash, so a byte-identical upstream regeneration
  produces a downstream cache *hit* instead of re-billing the expensive
  call. Keying on the URL instead is the defect this split exists to fix
  (falaw#14): fal mints a unique URL per upload, so a URL-keyed downstream
  entry is unreachable the moment the upstream genuinely re-runs.

An upstream artifact with no materialized bytes has no content hash, so its
key ref falls back to the URL — a guaranteed miss, never a wrong hit.

* **rtype:**
  list
* **returns:**
  One `lacing.Artifact` per [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) in `plan.calls`,
  in the same order.

### falaw.execute_plan_isolated(plan, , on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1, halt_on_failure=False)

Execute a Plan with **per-call failure isolation**, returning a report.

The fan-out counterpart of `execute()`. Where `execute` raises on the
first failure — discarding every artifact produced before it, each of which
fal has already billed — this returns an [`ExecutionReport`](_autosummary/falaw.html.md#falaw.ExecutionReport)
carrying one [`CallOutcome`](_autosummary/falaw.html.md#falaw.CallOutcome) per call: the successes with their
artifacts, the failures with their exceptions, and the calls that never ran
with the reason why.

`len(report.outcomes) == len(plan.calls)` always, so a caller that built
something per call can zip against `report.outcomes` and stay aligned.

* **Parameters:**
  **halt_on_failure** (bool) – When True, stop *submitting* work as soon as any call
  fails; everything not yet started is reported `blocked` with a
  run-level reason. This is what `execute()` uses, and at
  `concurrency=1` it reproduces the historical sequential
  behaviour exactly. Note that at `concurrency > 1` calls already
* **Return type:**
  ExecutionReport
  in flight are **not** cancelled — a fal request cannot be recalled
  once made, and pretending otherwise would discard results that were
  billed anyway.

:param All other arguments are as `execute()`.:

## Three outcome states, not two

`failed` and `blocked` are different questions for the caller. A failed
call can be retried verbatim. A blocked one cannot: its input does not
exist, so it has to be re-planned after its producer succeeds. Any call
holding a `"<from N>"` placeholder whose call `N` did not succeed is
blocked, transitively.

### Examples

```pycon
>>> from falaw import CallPlan, Plan, execute_plan_isolated
>>> plan = Plan(calls=(CallPlan(tool="t", application="m",
...                             arguments={}, output_kind="image"),))
>>> report = execute_plan_isolated(plan, dry_run=True)
>>> report.is_complete, len(report.outcomes)
(True, 1)
```

### falaw.extract_models_from_corpus(path)

Yield ModelRecord-shaped dicts parsed from llms-full.txt.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.fetch_model_prices(endpoint_ids, , api_key=None, http_get=None, batch_size=50)

`{endpoint_id: {"unit_price", "unit", "currency"}}` from fal’s API.

Batches requests at `batch_size` ids (the endpoint caps at
`MAX_IDS_PER_REQUEST`). Ids the API does not know are simply
absent from the result — the caller decides what absence means. That
takes work: the live endpoint answers a batch containing even one
unknown id with a blanket 404, so a failed batch is bisected down to
the ids that actually price (O(unknown x log batch) extra requests).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.generate_audio(prompt, , kind='ambient', duration_s=None, model_id=None, extra=None)

Generate ambient/SFX/music from a prompt. Argument shape mirrors
[`falaw.plan_generate_audio()`](_autosummary/falaw.html.md#falaw.plan_generate_audio) exactly, so planned and eager calls
with identical inputs collapse to the same cache entry.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.generate_image(prompt, , quality='balanced', image_size='landscape_4_3', model_id=None, extra=None)

Generate an image from a text prompt.

`image_size` is a fal preset (`landscape_16_9` …); models that size by
`aspect_ratio` (`nano-banana-pro`) get the equivalent ratio, with
`extra={"resolution": "2K"}` for the pixel tier. A prompt over the
model’s cap raises [`falaw.FalPromptTooLong`](_autosummary/falaw.html.md#falaw.FalPromptTooLong) before any call. Check
[`falaw.model_params()`](_autosummary/falaw.html.md#falaw.model_params) for enums whose default may not be what you
want (Recraft `style` defaults to photoreal).

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.generate_image_with_refs(prompt, reference_image_urls, , quality='balanced', model_id=None, extra=None)

Generate a new image conditioned on one or more reference images.

The missing twin of [`generate_image()`](_autosummary/falaw.html.md#falaw.generate_image): text-to-image models silently
ignore reference images, so callers wanting a recurring subject to stay
consistent (a character’s face across storyboard panels) need a model that
actually ingests references. This routes to the `image_edit` category
(Flux Kontext et al.) and threads the references as `image_url` (first)

+ `image_urls` (all) — the same wire shape image-edit models understand.

Pass `model_id` to override the picked model.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.get_llm_price(provider, model, , prices=None)

The row for `model` (exact id), or `None` when the table does not list it.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)]

### falaw.get_llm_rate(model, , rates=None)

The row for `model`, or `None` when the table does not price it.

`None` is the load-bearing answer: it means *unknown*, and every caller
must propagate it as unknown rather than substituting the router’s flat
price for a model nobody has priced.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]

### falaw.image_to_video(image_url, prompt='', , quality='high', model_id=None, extra=None)

Animate a still image into a video.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.iter_render_scene(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=1)

Yield `(kind, result)` pairs as each shot/beat finishes.

`kind` ∈ `{"shot", "beat"}`. With `concurrency=1` results
arrive in submission order (shots before beats). With
`concurrency > 1` they arrive in completion order (use the
`"shot_id"` / `"beat_id"` keys to re-key by identity).

Cache hits are immediate: a fully-cached scene yields all results
in close succession even at `concurrency=1`.

### falaw.lipsync(video_url, audio_url, , quality='high', model_id=None, extra=None)

Re-sync mouth motion in an existing video to a new audio track.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.list_llm_prices(provider=None, , prices=None)

Every row, or the rows of one `provider`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)]

### falaw.list_llm_rates(, rates=None)

Every row in the table, in file order — the catalogue view.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]

### falaw.llm_call_cost_usd(provider, model, usage, , prices=None)

`(USD, known)` for one first-party call from the vendor’s reported `usage`.

- `provider="anthropic"`: `input_tokens` (uncached), `output_tokens`,
  `cache_read_input_tokens`, `cache_creation_input_tokens`.
- `provider="openai"`: Responses (`input_tokens` / `output_tokens` /
  `input_tokens_details.cached_tokens`) or Chat Completions
  (`prompt_tokens` / `completion_tokens` /
  `prompt_tokens_details.cached_tokens`). Input includes the cached part,
  which is billed at the cached rate; reasoning tokens are already in output.

`known` is `False` when the model is not in the table; the cost is then
priced at the provider’s dearest listed rates, which is not a ceiling for a
model pricier than any listed (see the module docstring). `prices=` swaps in the caller’s
own table, e.g. one reconciled against an invoice.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – `provider` is not one of `PROVIDERS`.
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### falaw.llm_ceiling_usd(model, , input_tokens=None, max_output_tokens=None, count=1, rates=None)

Ceiling cost in USD of `count` calls routing `model`, or `None`.

Two modes, decided by whether the caller passes a token hint at all.

**No hints** — quote fal’s published per-request price for the routed
model. That is a known vendor fact, and for the premium tier it is ten
times what the router’s flat record said:

```pycon
>>> flat = llm_ceiling_usd('anthropic/claude-sonnet-4.5')
>>> flat
0.01
```

**Both hints** — quote the **maximum** of that request price and
`input_tokens` in plus `max_output_tokens` out at the routed model’s
upstream rates. Taking the max is what makes it a ceiling: the flat price
binds a short prompt, the token sum binds a long one, and quoting either
alone under-quotes the other case.

```pycon
>>> long_prompt = llm_ceiling_usd(
...     'anthropic/claude-sonnet-4.5', input_tokens=20_000, max_output_tokens=4_000
... )
>>> round(long_prompt, 4)
0.12
>>> long_prompt > flat        # length raises the quote; the flat rate is a floor
True
```

Monotone in both hints, so a bigger hint never quotes less:

```pycon
>>> a = llm_ceiling_usd('openai/gpt-4o', input_tokens=1_000, max_output_tokens=100)
>>> b = llm_ceiling_usd('openai/gpt-4o', input_tokens=9_000, max_output_tokens=100)
>>> b >= a
True
```

Both hints are upper bounds the *caller* holds at plan time: the prompt is
already written, and `max_tokens` caps the response. Real output length
is unknowable before the call, which is exactly why this quotes the cap and
never a midpoint guess.

A token quote is therefore **all or nothing**. Asking for one that cannot
be bounded yields `None` — unknown, which forces approval — rather than a
number that looks like a ceiling and is not. That covers one hint without
the other (an uncapped response has no bound to quote):

```pycon
>>> llm_ceiling_usd('anthropic/claude-sonnet-4.5', input_tokens=50_000) is None
True
```

and a row with no published upstream token rates (the request price alone
is not a ceiling on a token bill nobody has priced):

```pycon
>>> llm_ceiling_usd(
...     'google/gemini-flash-1.5', input_tokens=50_000, max_output_tokens=1_000
... ) is None
True
```

* **Parameters:**
  * **model** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The `model` argument the call will send to the router.
  * **input_tokens** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – Upper bound on prompt tokens. `None` together with
    `max_output_tokens` selects the per-request basis alone.
  * **max_output_tokens** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – The response cap (any-llm’s `max_tokens`).
  * **count** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Number of identical calls.
  * **rates** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]]) – Override table, for a caller who has reconciled real rates
    against their own billing.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – a token hint or `count` that is not a plain `int`.
        A `bool` and a `float` both do the arithmetic silently —
        `True` prices one token, `1.5e4` prices a fractional one —
        so a caller who passed the wrong thing would get a number rather
        than a complaint.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – a negative token hint or `count`, which would make the
        token basis *lower* the ceiling — silently, and in the one
        direction a spend gate must never err.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### falaw.llm_complete(prompt, , system='', model='anthropic/claude-sonnet-4.5', temperature=0.7, extra=None)

Single-shot LLM completion. Returns the assistant text.

Delegates to [`llm_complete_with_receipt()`](_autosummary/falaw.html.md#falaw.llm_complete_with_receipt) (one argument-construction
site, so the two spellings share one cache identity) and discards the
receipt. A caller that accounts for spend wants the receipt variant.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.llm_complete_with_receipt(prompt, , system='', model='anthropic/claude-sonnet-4.5', temperature=0.7, extra=None)

Single-shot LLM completion that also reports what it cost.

Same arguments, same wire call, same cache identity as
[`llm_complete()`](_autosummary/falaw.html.md#falaw.llm_complete) — the receipt is a side channel, observed from the
call this function was making anyway (the cache layer’s `cache_hit`
event, the response’s usage block, the registry’s estimate). Library
surface only — deliberately NOT a registered tool: the tool surface
keeps the one string-returning `llm_complete`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`LlmReceipt`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.LlmReceipt)]

### falaw.llm_cost_basis(routed_model, , input_tokens=None, max_output_tokens=None, custom_rates=False)

A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a routed LLM call priced from the rate table.

`custom_rates=True` records that the quote came from a caller-supplied
`rates=` table rather than the committed one: there is no file to digest,
so the version stays empty and the table names itself
[`falaw.llm_rates.CUSTOM_LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.CUSTOM_LLM_RATES_TABLE). [`reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) then
**refuses** to re-quote it with the committed table, reporting `unknown`
— a caller’s reconciled $0.50 row re-priced at falaw’s published $0.01 is a
50x under-quote, not a price drop. Re-quote it by passing a
[`Pricer`](_autosummary/falaw.html.md#falaw.Pricer) over your table that names that same identity.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

### falaw.llm_prices_table_version()

The table’s version, a digest of its bytes (see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version)).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.make_call_plan(, tool, application, arguments, output_kind, backend='fal', estimated_cost_usd=None, expected_duration_s=None, metadata=None, consult_cache=True, cost_basis=None)

Build a [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) and (optionally) check the cache.

When `consult_cache=True` (the default), the cache is peeked using the
same key the eventual call would produce; `cache_status` is set to
`"hit"` if a cached entry exists, `"miss"` otherwise. This makes
`Plan.total_cost_usd` honest: a fully-cached Plan reports $0.

When `consult_cache=False` (e.g. for unit tests or “what would a fresh
run cost?” reporting), `cache_status` is `"unknown"`.

A **chained call** — arguments still holding a `"<from N>"` placeholder,
because the upstream call has not executed yet — is never peeked
(falaw#15, D2): its resolved key is not knowable at plan time, and the
unresolved key is never written by anything (`execute()` always keys
on the resolved form), so peeking it can only ever report a false
`"miss"` — never a real `"hit"`. That silently over-quotes cost and
under-reports the plan’s `cache_hit_savings_usd` on every chained call,
which under prepaid billing (a quote that may be *deducted*) is a billing
bug. `cache_status` is `"unknown"` here instead, exactly the case
`CacheStatus` documents it for.

`cost_basis` records how `estimated_cost_usd` was arrived at so the
quote can be re-run later (falaw#60). It is descriptive only: it never
reaches the cache key or [`plan_hash()`](_autosummary/falaw.html.md#falaw.plan_hash), and a plan built without one
is byte-identical to one built before the field existed.

* **Raises:**
  [**FalNonCanonicalArgument**](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument) – an argument cannot be hashed
      faithfully (non-JSON object, non-finite float, non-string mapping
      key). Raised here — while planning is still free — rather than at
      key-composition time on the way to the network (falaw#17).
* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.materialize_asset(url, , key_hint='', store=None, fetcher=None, refresh=False)

Download a remote asset to the cache and return the local path.

The local filename is content-addressed by the asset’s **bytes**, so two
URLs serving identical bytes resolve to one file. The extension is a
presentational hint for ffmpeg/PIL; the SHA-256 is the address.

Repeat calls are cheap, in three widening circles — this is what makes it
safe to call from a loop over 200 shots:

1. the file is already on disk here (no store lookup, no network) —
   **immutable URLs only**, see below;
2. the bytes are in the content store (no network, or one validating
   round-trip) — so it still works after fal has expired the URL;
3. otherwise, one download.

Circle 1 matters on its own: the content store is prunable, so an asset
can survive as a materialized file after its blob is gone.

**Circle 1 is taken only when the URL cannot change** — that is,
[`falaw.content.is_immutable_url()`](_autosummary/falaw.content.html.md#falaw.content.is_immutable_url) — because reaching it requires
trusting the `url -> hash` index to name the *current* bytes, and for an
arbitrary caller-supplied URL it does not (thorwhalen/falaw#23). A mutable
URL goes to circle 2, where [`falaw.content.content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url)
revalidates before reusing anything; when the bytes really are unchanged
that costs one conditional request and still no download, and when they
have changed you get the new file instead of silently getting the old one.

* **Parameters:**
  * **url** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – the remote asset URL. `file://` is supported and is used
    deliberately by downstream packages for locally-rendered media.
  * **key_hint** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – optional human-readable filename prefix.
  * **store** – injected `lacing.ArtifactStore`; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store).
  * **fetcher** – injected byte source (`url -> Iterable[bytes]`); defaults to
    the `urllib`-based one. The seam for custom transport (auth
    headers, retries) and for a hermetic test suite.
  * **refresh** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – re-fetch unconditionally, skipping every circle. Rarely needed
    now: a mutable URL revalidates on its own (falaw#23), so this is for
    an origin that lies about its validators.
* **Raises:**
  [**FalAssetFetchError**](_autosummary/falaw.errors.html.md#falaw.errors.FalAssetFetchError) – the bytes could not be retrieved.
      Unlike a generated-media artifact (which degrades to URL-only —
      see [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute)), there is nothing to degrade to
      here: the caller asked for a local file.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.model_constraints(id)

The capability/limit fields for a model — the “static reminder of
limitations” a shot-list builder surfaces. Resolves aliases.

Returns a JSON-able dict; `max_clip_seconds` etc. are `None` / empty
when unknown for that model.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.model_params(id)

Per-model parameters worth knowing before a call, `{name: spec}`.

Each spec is `{"enum": [...], "default": ...}`; resolves aliases. For
`fal-ai/recraft/v3/text-to-image` this shows `style` defaults to
`realistic_image` — a photograph, whatever the prompt describes — and
lists the styles that actually give a woodcut or a linocut. Empty when the
catalogue records none (fal’s OpenAPI schema is the full source).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.parse_response(raw, , application, arguments)

Best-effort parser over the common fal response shapes.

fal models return a variety of layouts (lists, single objects, bare URLs).
We normalize each into Asset(url, kind, …). Unknown shapes pass through
as `raw` only — callers can read `result.raw` for anything we miss.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.parse_screenplay(text, , title='', style='', model='anthropic/claude-sonnet-4.5')

Convert prose screenplay text into a Scene IR via an LLM call.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### falaw.pick_model(, category, quality_tier='balanced')

Pick a sensible fal model for a (category, quality) request.

First-match semantics: when several models share a tier, the earlier
entry wins. Curated entries are written first in `data/models.json`,
so they take precedence over corpus-merged additions. If no model
has the exact tier, neighboring tiers are tried. KeyError only when
the category is empty.

* **Return type:**
  [`ModelRecord`](_autosummary/falaw.base.html.md#falaw.base.ModelRecord)

### falaw.plan_animate_face(image_url, audio_url, , prompt='', quality='balanced', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.animate_face()`](_autosummary/falaw.html.md#falaw.animate_face) call (image + audio → talking video).

#### NOTE
the default avatar model is known to hang. For production-grade
behavior, callers should pass `model_id="fal-ai/bytedance/omnihuman/v1.5"`
or set `quality="high"` (which picks omnihuman).

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_composite_character_in_environment(character_image_url, environment_image_url, , prompt='', quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.composite_character_in_environment()`](_autosummary/falaw.html.md#falaw.composite_character_in_environment) call.

The character image anchors identity; the environment image anchors
location, lighting, palette. The default model is Flux Kontext dev.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_dependencies(plan)

Per-call set of the call indices it references via `"<from N>"`.

The Plan’s dependency DAG, read straight off the placeholders — one
`frozenset` per call, in plan order, so `deps[3] == {1}` means call 3
consumes call 1’s output. An empty set means the call is **independent** and
may run concurrently with any other independent call, which is what
`execute_isolated()` schedules on.

Also the plan’s structural validator, and it runs \*\*before a cent is
spent\*\*: a malformed reference used to surface only when execution reached
the offending call, i.e. after every call before it had been billed.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – a placeholder that is not `"<from N>"` for an integer
      `N`; an `N` outside the plan; or an `N` that does not run
      *before* the referencing call (including a self-reference) — the
      output would not exist yet, so it can only ever be a bug.
* **Return type:**
  [*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[*frozenset*](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[*int*](https://docs.python.org/3/builtins/functions.html#int)], …]

```pycon
>>> a = CallPlan(tool="t", application="m", arguments={}, output_kind="image")
>>> b = CallPlan(tool="t", application="m",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_dependencies(Plan(calls=(a, b)))
(frozenset(), frozenset({0}))
>>> plan_dependencies(Plan(calls=(b,)))
Traceback (most recent call last):
    ...
ValueError: Placeholder '<from 0>' in call 0 references call 0, which does not run before it. ...
```

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[`int`](https://docs.python.org/3/builtins/functions.html#int)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### falaw.plan_edit_image(image_url, prompt, , quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.edit_image()`](_autosummary/falaw.html.md#falaw.edit_image) call (Flux Kontext / SeedEdit / OmniGen).

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_from_dict(d)

Rebuild a [`Plan`](_autosummary/falaw.html.md#falaw.Plan) from a [`plan_to_dict()`](_autosummary/falaw.html.md#falaw.plan_to_dict) dict.

Raises `ValueError` if `d` carries an unrecognized `schema` tag — a
plan written by an incompatible future version should fail loudly, not
silently lose calls. A missing `schema` is tolerated (treated as v1) so
hand-written plans stay easy.

* **Return type:**
  [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)

### falaw.plan_generate_audio(prompt, , kind='ambient', duration_s=None, model_id=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.generate_audio()`](_autosummary/falaw.html.md#falaw.generate_audio) call (prompt → ambient/SFX/music).

The planning primitive behind an ambient bed or music cue (falaw#10):
the user who leaves their editor to hunt a city-night ambience is the
user this generates one for — costed, cached and planned like every
other call. Mirrors the eager [`falaw.generate_audio()`](_autosummary/falaw.html.md#falaw.generate_audio) signature so
a planned call and an eager call with identical inputs collapse to the
same cache entry. Pure data; no network at plan time.

`kind` selects the default model (see `GENERATE_AUDIO_DEFAULTS`
for why these are explicit ids). `duration_s` reaches the model as its
integer `duration` argument where the model takes one (mmaudio does)
AND feeds the cost estimate; for models with no duration argument it is
estimator-only.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_generate_image(prompt, , quality='balanced', image_size='landscape_4_3', model_id=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.generate_image()`](_autosummary/falaw.html.md#falaw.generate_image) call without executing it.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_generate_image_with_refs(prompt, reference_image_urls, , quality='balanced', model_id=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.generate_image_with_refs()`](_autosummary/falaw.html.md#falaw.generate_image_with_refs) call.

The planning sibling of the eager op: text-to-image models ignore
reference images, so a caller wanting a recurring subject to stay
consistent must route to a reference-capable model. This picks the
`image_edit` category (Flux Kontext et al.) and threads the references
as `image_url` (first) + `image_urls` (all), the same wire shape the
eager op uses — so a planned and an eager call with identical inputs
collapse to one cache entry.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_hash(plan)

Stable, plan-scoped **structural idempotency key** for a whole [`Plan`](_autosummary/falaw.html.md#falaw.Plan).

Answers “does this whole plan match one I already ran?” — the handle a job
manager (its first customer, `nw.jobs`) uses to dedup double-submits and
to replay a resumed render for free. It is computed *before* execution and
with `<from N>` placeholders intact, so it is stable across re-plans of the
same structural request.

The digest canonicalizes each call over `{app, args, tool}`
([`falaw.canonical.plan_identity_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload)) — matching
`_synthetic_artifact()`’s canonicalization, and deliberately **not** the
per-call content-addressed cache key (`falaw.cache._key()`, which keys on
`{app, args}` with no `tool`). `plan_hash` and the per-call cache key
therefore key on *different* bytes and must not be assumed to agree
call-for-call. Both projections live side by side in [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical)
with one shared byte-form ([`falaw.canonical.canonical_blob()`](_autosummary/falaw.canonical.html.md#falaw.canonical.canonical_blob) — sorted
keys, **no** `default=str` fallback, no NaN), so an argument the form
cannot represent faithfully raises
[`falaw.errors.FalNonCanonicalArgument`](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument) instead of colliding, and a
new identity-bearing field is an explicit decision about both hashes.

Two structurally-identical plans hash equal; changing any call’s `app`,
`args`, or `tool` — or the *order* of calls — changes the hash.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...              arguments={"prompt": "a tiger"}, output_kind="image")
>>> b = CallPlan(tool="image_to_video", application="fal-ai/svd",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(a, b)))
True
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(b, a)))
False
```

`backend` (falaw#15) joins the hashed payload too, so a plan built for
one backend never dedups against the structurally-identical plan for
another — and, since it is included only when non-default, every plan
made of today’s (all-`"fal"`) calls hashes exactly as it did before:

```pycon
>>> comfy = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                   arguments={"prompt": "a tiger"}, output_kind="image",
...                   backend="comfyui")
>>> plan_hash(Plan(calls=(a,))) == plan_hash(Plan(calls=(comfy,)))
False
```

### falaw.plan_image_to_video(image_url, prompt='', , quality='high', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.image_to_video()`](_autosummary/falaw.html.md#falaw.image_to_video) call.

`duration_s` is used only for cost estimation when the model is priced
`per_second`; it does not get passed to fal unless the caller puts it
in `extra` (different models have different argument names).

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_lipsync(video_url, audio_url, , quality='high', model_id=None, duration_s=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.lipsync()`](_autosummary/falaw.html.md#falaw.lipsync) call (existing video + new audio → re-synced video).

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_llm_complete(prompt, , system='', model=None, temperature=0.7, output_kind='text', input_tokens=None, max_output_tokens=None, llm_rates=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.llm_complete()`](_autosummary/falaw.html.md#falaw.llm_complete) call without executing it.

Routes through `fal-ai/any-llm` with the *exact same* application id and
argument shape as the eager [`falaw.llm_complete()`](_autosummary/falaw.html.md#falaw.llm_complete), so a planned call
and an eager call with identical inputs collapse to the same cache entry.

`output_kind` is `"text"` for a free-form completion or `"json"` when
the prompt asks for a strict-JSON response — it tells `falaw.execute()`
what kind of `lacing.Artifact` to materialize. Either way the LLM
response is materialized to a content-addressed cache *file*
(`Artifact.path`), because LLM output is text, not a URL.

**Cost is a ceiling for the routed model**, not the router’s flat price
(falaw#50, option A). `fal-ai/any-llm` is one registry record routing
thirty models at two published request tiers, so the record’s single
`per_call` figure under-quotes every premium model tenfold — including
this function’s own default. [`falaw.llm_ceiling_usd()`](_autosummary/falaw.html.md#falaw.llm_ceiling_usd) prices the
`model` argument instead:

- no token hints → fal’s published per-request price for that model;
- `input_tokens` **and** `max_output_tokens` → the greater of that
  price and the upstream per-token cost of a prompt that long capped at
  that many output tokens. Monotone in both hints;
- a model the rate table does not price, or a token quote that cannot be
  bounded → `estimated_cost_usd` is `None`, which lights up
  [`falaw.plan.Plan.has_unknown_costs`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.has_unknown_costs) and forces approval. It is
  > never quietly the router’s flat price.

`input_tokens` is what opts a call into a token quote. Given one,
`max_output_tokens` defaults to whatever the caller already put under
`MAX_OUTPUT_TOKENS_ARGUMENT` in `extra` — the cap is a fact of the
call, not a second thing to remember. Without one, `extra` is left alone:
capping your response is not asking to be quoted by tokens, and reading the
cap as half a token quote would drop a call that has a perfectly good
request price down to forced approval.

Both hints are **estimator-only**: they never enter `arguments`, so adding
one to an existing call site changes the quote without moving the cache key
or the plan hash.

`llm_rates` swaps in a caller’s own rate table — the seam for someone who
has reconciled real numbers against their fal invoice.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_text_to_speech(text, , quality='balanced', voice=None, model_id=None, duration_s=None, tokens=None, extra=None, metadata=None, consult_cache=True)

Plan a [`falaw.text_to_speech()`](_autosummary/falaw.html.md#falaw.text_to_speech) call (text → audio Artifact).

Mirrors the eager [`falaw.text_to_speech()`](_autosummary/falaw.html.md#falaw.text_to_speech) signature so a planned
call and an eager call with identical inputs collapse to the same
cache entry. `voice` semantics are model-specific.

`duration_s` and `tokens` are *optional* hints used only by the
cost estimator — they never enter `arguments`, so they cannot move
the cache identity. `duration_s` prices `per_second` records (the
produced audio’s actual duration comes back on the materialized
Artifact); `tokens` prices `per_token` records. For
character-billed TTS (ElevenLabs — its registry record says so
explicitly) pass `tokens=len(text)`. Without the matching hint a
quantity-priced record stays `estimated_cost_usd=None` — unknown,
never free, which forces approval downstream.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan_to_dict(plan)

Convert a [`Plan`](_autosummary/falaw.html.md#falaw.Plan) to a plain JSON-serializable dict.

The result round-trips through [`plan_from_dict()`](_autosummary/falaw.html.md#falaw.plan_from_dict). This is the
substrate primitive a consumer (a persistence layer, an MCP transport, a
plan-diff tool) builds on — falaw owns the wire shape of its own Plan so
every consumer agrees on it. Carries a `schema` tag (`PLAN_DICT_SCHEMA`)
so a future breaking change is detectable.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.prune_assets(, older_than=None, max_bytes=None, dry_run=True, store=None)

Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22).

`assets/` holds a **copy** of each blob rather than a hard link, which is
deliberate ([`falaw.content.write_blob_to_file()`](_autosummary/falaw.content.html.md#falaw.content.write_blob_to_file) explains why a link
would let a consumer corrupt the content store) but doubles the on-disk cost
of every materialized asset. That makes this the prune to reach for first:
while a blob survives, re-materializing its asset is a local copy and costs
nothing, so `rebillable_entries` counts only the copies whose blob is
*also* gone.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop copies last written more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
  * **store** – injected `lacing.ArtifactStore`, used only to ask whether
    each dropped copy still has a blob behind it; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store).
* **Returns:**
  with `area="assets"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given.

### falaw.prune_content(, older_than=None, max_bytes=None, dry_run=True, store=None)

Reclaim content-addressed blobs — the gigabytes (falaw#22).

This is the **expensive** prune. A blob is the last copy of an asset once
fal has expired its URL (“expired files are permanently deleted and cannot
be recovered”), so dropping one can turn a free cache hit into a re-rendered
clip. The report says how many entries that applies to *before* you commit;
read `rebillable_entries`.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop blobs last written more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
  * **store** – injected `lacing.ArtifactStore`; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store), whose deletes remove
    the file. An injected store keeps its own backend’s delete policy:
    a stock `ArtifactStore.from_directory` store moves each blob to
    the OS trash, which frees no space on that volume even though the
    report counts the bytes as freed.
* **Return type:**
  [*PruneReport*](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)

Blobs are listed and deleted through lacing’s contained
`iter_blobs`/`delete_blob` (falaw#66): nothing outside the blob root is
listed or removed, and an in-root symlink is unlinked, never its target.

* **Returns:**
  with `area="content"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given — see `_require_a_bound()`.

Both bounds may be combined; a blob selected by either is dropped.

```pycon
>>> report = prune_content(older_than=timedelta(days=90))
>>> report.dry_run
True
```

### falaw.prune_manifests(, older_than=None, max_bytes=None, dry_run=True)

Reclaim cache entries — the fal responses themselves (falaw#22).

Manifests are kilobytes, so this is rarely where the disk is; it is here
because a stale *entry* is its own problem — it pins a model version and a
price you may no longer want served from cache.

Unlike [`prune_content()`](_autosummary/falaw.html.md#falaw.prune_content), the cost is unconditional: every dropped
entry re-bills its call on the next run, so `rebillable_entries` always
equals the candidate count.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop entries stored more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta). Read from the manifest’s own
    `stored_at`, falling back to file mtime.
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
* **Returns:**
  with `area="manifests"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given.

Only the manifest is removed; blobs are shared by content hash across
entries and are never dropped from here — the same rule
[`falaw.drop_cache_entry()`](_autosummary/falaw.html.md#falaw.drop_cache_entry) follows.

### falaw.refresh_full_docs(, docs_dir=None, max_workers=16, force=False, journal=True)

Re-crawl per-page docs and rebuild `fal_ai_docs_full.md`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh_llm_rates(, write=False, http_get_any_llm_doc=None, http_get_openrouter_models=None, rates_path=None, proposed_path=None, diff_path=None, today=None)

Fetch both sources, propose a fresh table, diff it — never overwrite.

`rates_path` (default: the committed `llm_rates.json`) is read only,
never written: this function has no code path that touches the committed
table, by design — a repriced or retiered model is a decision for a human
to promote, not something a refresh job gets to make silently. With
`write=True`, the freshly-derived table is written to `proposed_path`
(default: `llm_rates.proposed.json` next to the committed file) and the
diff report to `diff_path` (default: `llm_rates.diff.txt`).

Returns a summary dict: `{"fetched_at", "committed_models",
"proposed_models", "diff", "write", "proposed_path", "diff_path"}`. The
two path keys are `None` when `write=False`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh_llms(, docs_dir=None, journal=True)

Refresh `llms.txt` and `llms-full.txt`; return a summary dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh_model_prices(, write=False, api_key=None, http_get=None, models_path=None, today=None)

Refresh `models.json` cost estimates from fal’s pricing API.

Returns a summary dict; `write=False` (default) reports what would
change without touching the file. `models_path` and `today` exist
for tests (the fetch date lands in each estimate’s `notes`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh_models_from_corpus(, path=None, write=False)

Merge corpus-discovered models into models.json (additive).

Returns `{added, total, from_corpus, write}` summary. Setting
`write=False` (the default) reports what *would* change without
touching the file.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh_state()

Return the saved per-source refresh state (etags, last fetch times).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.register_tool(\*\*spec_kwargs)

Decorator: register the wrapped function as a falaw tool.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)

```pycon
>>> @register_tool(name='echo', description='echo back', tags=('demo',))
... def _echo(x): return x
>>> get_tool('echo').name
'echo'
```

### falaw.remove_background(image_url, , quality='high', model_id=None, extra=None)

Remove the background from an image.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.render_beat(beat, character, , tts_quality='balanced', lipsync_quality='high', tts_model_id=None, avatar_model_id=None, force=False)

Render one Beat to a lipsynced video. Returns a small manifest dict.

* **Parameters:**
  * **tts_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the TTS model. When provided, takes precedence
    over the character’s voice.model_id and over `tts_quality`-based
    `pick_model`. Use this to force a specific TTS engine for one
    beat (e.g. eleven-v3 for emotional delivery, multilingual-v2 for
    consistency).
  * **avatar_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the avatar/lipsync model (e.g.
    `"fal-ai/bytedance/omnihuman/v1.5"` to bypass the default
    `ai-avatar` which is known to hang).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.render_scene(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=1)

Render every shot and beat. Returns a manifest dict.

`concurrency` controls how many shots/beats run in parallel
against fal. The work is HTTP-bound, so a thread pool is enough.
Default `1` preserves serial behavior. Use
[`iter_render_scene()`](_autosummary/falaw.html.md#falaw.iter_render_scene) instead if you want results yielded as
each unit completes (for live UI updates).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.render_shot(shot, , environment=None, characters=(), style='', as_video=False, quality='balanced', image_model_id=None, image_to_video_model_id=None, force=False)

Render a Shot as a still (default) or a short clip.

* **Parameters:**
  * **image_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the image-gen model used for the storyboard
    still (defaults to `pick_model(category="image", …)`).
  * **image_to_video_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the image-to-video model used when
    `as_video=True` (e.g. `"fal-ai/minimax/hailuo-02/pro/image-to-video"`).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.reprice_plan(plan, \*, pricers={'llm_rates': Pricer(quote=<function \_quote_from_llm_rates>, table='falaw/data/llm_rates.json', version=<functools._lru_cache_wrapper object>), 'model_catalogue': Pricer(quote = <function \_quote_from_catalogue>, table='falaw/data/models.json', version=<functools._lru_cache_wrapper object>)})

Re-quote every call in `plan` against today’s rate tables.

Pure data: no network, no billing API, nothing executed. Returns a new
[`RepricedPlan`](_autosummary/falaw.html.md#falaw.RepricedPlan) — the input plan is untouched.

A call carrying a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) is re-quoted through the pricer
its basis names, with the exact quantity hints that produced the original
figure, and comes back with a basis re-stamped to today’s table version. A
call carrying no basis is reported as `"no_basis"` and its cost is
**cleared to** `None`: the frozen number is a fact about a past moment,
and silently re-presenting it as a current quote is the failure this
function exists to prevent. That clearing lights up
[`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs), which is exactly right — nobody can
say what that call costs today.

A basis is only re-quoted by a pricer reading the **same table** it names.
A mismatch is `"unknown"`, not a re-quote: two tables are two sets of
books, and pricing a caller’s reconciled $0.50 row at falaw’s published
$0.01 would be a 50x under-quote wearing the clothes of a price drop. Pass
a [`Pricer`](_autosummary/falaw.html.md#falaw.Pricer) naming that table to re-quote against the same books.

`cache_status` is carried through unchanged. Re-pricing answers “what
would this cost?”, not “is it still cached?”; peeking the cache here would
quietly turn a re-quote into an I/O operation and make a hit look like a
price drop. Re-plan the calls if you want a fresh cache reading.

* **Parameters:**
  * **plan** ([`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)) – The plan to re-quote, typically just deserialized with
    [`falaw.plan_from_dict()`](_autosummary/falaw.html.md#falaw.plan_from_dict).
  * **pricers** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer)]) – Pricing rules by [`falaw.CostBasis.pricer`](_autosummary/falaw.html.md#falaw.CostBasis.pricer) name. The seam
    for a caller who has reconciled real numbers against their own
    invoice — see [`Pricer`](_autosummary/falaw.html.md#falaw.Pricer).
* **Return type:**
  [`RepricedPlan`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan)

```pycon
>>> from falaw import Plan, CallPlan, reprice_plan
>>> stale = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                  arguments={"prompt": "a tiger"}, output_kind="image",
...                  estimated_cost_usd=0.025)
>>> out = reprice_plan(Plan(calls=(stale,)))
>>> out.calls[0].status
'no_basis'
>>> out.plan.calls[0].estimated_cost_usd is None   # unknown, not $0.025
True
>>> out.plan.has_unknown_costs
True
```

### falaw.scene_from_dict(d)

Inverse of asdict: reconstruct a Scene from a plain dict.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### falaw.storyboard_shot(shot, , environment=None, characters=(), style='', quality='balanced')

Render a storyboard still for a Shot.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.subscribe(callback)

Register `callback` to receive every emitted ProgressEvent.

Returns the callback unchanged so it can be used as a decorator:

```default
@subscribe
def log_to_file(ev: ProgressEvent) -> None:
    ...
```

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`ProgressEvent`](_autosummary/falaw.events.html.md#falaw.events.ProgressEvent)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]

### falaw.talking_avatar_from_text(text, image_url, , voice=None, prompt='', tts_quality='balanced', avatar_quality='balanced')

text + face image → talking video. Two fal calls, one Result.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.text_to_speech(text, , quality='balanced', voice=None, model_id=None, extra=None)

Synthesize speech. `voice` semantics are model-specific.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.text_to_video(prompt, , quality='high', model_id=None, extra=None)

Generate a video from a text prompt.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.unsubscribe(callback)

Remove a previously [`subscribe()`](_autosummary/falaw.html.md#falaw.subscribe)’d callback. No-op if absent.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### falaw.upscale_image(image_url, , scale=2.0, model_id=None, extra=None)

Upscale an image.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.using_fal_credentials(key)

Bind `key` as the fal credential for every [`call_fal()`](_autosummary/falaw.html.md#falaw.call_fal) in this context.

Intended for server-side bring-your-own-key flows: wrap a unit of work
that will make one or more fal calls, and they all authenticate with
`key` instead of the server’s `FAL_KEY` env var — without any
intermediate function needing a credential parameter.

A falsy `key` is a deliberate no-op (the context is left untouched), so
a caller can pass an optional header value straight through without
special-casing “no BYO key — fall back to the server/env key”.

Thread/async safe: this is `ocracy.kit.using_credentials(fal=key)`, backed
by a [`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar), so the binding is visible only within
the entering context (and asyncio tasks or copied contexts it spawns),
never to concurrent
requests.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`None`](https://docs.python.org/3/builtins/constants.html#None)]

### falaw.using_url_fetcher(fetcher)

Make `fetcher` falaw’s default asset transport for the duration.

The **public seam** for replacing falaw’s network transport wholesale —
with an authenticated client, a retrying one, a local mirror, or (the
common case) an in-memory fake in a test suite.

Prefer this to reaching for the module’s private default: it covers every
entry point in one place, it needs no `monkeypatch`, and it cannot be
invalidated by an internal rename. It is also the only mechanism that
reaches a call falaw makes from a thread you did not create — see
`_DEFAULT_FETCHER` for why that matters.

An explicitly passed `fetcher=` / `asset_fetcher=` still wins: this
changes the *default*, never an explicit choice.

Nests and restores, so an inner block cannot leak over an outer one:

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]]

```pycon
>>> from lacing import ArtifactStore
>>> store = ArtifactStore.in_memory()
>>> before = default_url_fetcher()
>>> with using_url_fetcher(lambda url: [b"faked"]):
...     content_ref_for_url("https://fal.media/x.png", store=store).bytes_size
5
>>> default_url_fetcher() is before
True
```

For a ready-made fake with pinned bytes and 404s, see [`falaw.testing`](_autosummary/falaw.testing.html.md#module-falaw.testing).

### falaw.video_model_constraints()

`model_constraints` for every video model in the catalog — the data a
shot-list builder shows as its model-limits reference.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.voice_clone(reference_audio_url, text, , model_id=None, extra=None)

Generate speech in a cloned voice.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### Modules

| [`journal`](_autosummary/falaw.journal.html.md#module-falaw.journal)                     | Agent journal: append-only log of notes, issues and improvements.                                                     |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| [`account`](_autosummary/falaw.account.html.md#module-falaw.account)                     | Account health probe.                                                                                                 |
| [`backends`](_autosummary/falaw.backends.html.md#module-falaw.backends)                   | Execution backend registry — where `CallPlan.backend` dispatches (falaw#15).                                          |
| [`base`](_autosummary/falaw.base.html.md#module-falaw.base)                           | Core types: ToolSpec and ModelRecord.                                                                                 |
| [`bridges`](_autosummary/falaw.bridges.html.md#module-falaw.bridges)                     | Bridges: derive auxiliary surfaces (skill, MCP, HTTP service) from the tool registry.                                 |
| [`cache`](_autosummary/falaw.cache.html.md#module-falaw.cache)                         | Content-addressed cache for fal calls.                                                                                |
| [`canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical)                 | One canonical byte-form for everything falaw hashes (falaw#17).                                                       |
| [`content`](_autosummary/falaw.content.html.md#module-falaw.content)                     | Content addressing for fal-produced media — identify a file by *what it is*.                                          |
| [`core`](_autosummary/falaw.core.html.md#module-falaw.core)                           | Wrapper over `fal_client.subscribe` with progress events and auto-journaling.                                         |
| [`corpus`](_autosummary/falaw.corpus.html.md#module-falaw.corpus)                       | Extract model records from `misc/docs/llms-full.txt`.                                                                 |
| [`cost`](_autosummary/falaw.cost.html.md#module-falaw.cost)                           | Cost estimation: ModelRecord.cost_estimate + Scene rollups.                                                           |
| [`degrade`](_autosummary/falaw.degrade.html.md#module-falaw.degrade)                     | Where falaw sends "I carried on, but you should know" — one collection point.                                         |
| [`errors`](_autosummary/falaw.errors.html.md#module-falaw.errors)                       | Typed exceptions for falaw operations.                                                                                |
| [`events`](_autosummary/falaw.events.html.md#module-falaw.events)                       | Structured progress events for fal calls.                                                                             |
| [`liveness`](_autosummary/falaw.liveness.html.md#module-falaw.liveness)                   | Is a catalogued endpoint still deployed? (falaw#76)                                                                   |
| [`llm_prices`](_autosummary/falaw.llm_prices.html.md#module-falaw.llm_prices)               | One first-party LLM price table: what a direct Anthropic / OpenAI call costs (falaw#81).                              |
| [`llm_rates`](_autosummary/falaw.llm_rates.html.md#module-falaw.llm_rates)                 | Per-routed-model LLM rates: the data behind a *ceiling* quote (falaw#50, option A).                                   |
| [`llm_rates_refresh`](_autosummary/falaw.llm_rates_refresh.html.md#module-falaw.llm_rates_refresh) | Refresh proposals for the LLM rate table, never a silent overwrite (falaw#56).                                        |
| [`local`](_autosummary/falaw.local.html.md#module-falaw.local)                         | Local utilities: ffmpeg + PIL glue for stitching fal outputs.                                                         |
| [`operations`](_autosummary/falaw.operations.html.md#module-falaw.operations)               | Task-level verbs: stable, agent-friendly entry points.                                                                |
| [`outcomes`](_autosummary/falaw.outcomes.html.md#module-falaw.outcomes)                   | Per-call outcomes of running a [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan) — the partial-result type. |
| [`plan`](_autosummary/falaw.plan.html.md#module-falaw.plan)                           | Plan / Execute primitives — separate planning (data) from execution (effects).                                        |
| [`pricing`](_autosummary/falaw.pricing.html.md#module-falaw.pricing)                     | Read fal's pricing API into `models.json` cost estimates (falaw#18).                                                  |
| [`prune`](_autosummary/falaw.prune.html.md#module-falaw.prune)                         | Capacity management for the falaw cache: see what is on disk, and reclaim it.                                         |
| [`refresh`](_autosummary/falaw.refresh.html.md#module-falaw.refresh)                     | Refresh fal.ai docs locally with conditional GETs.                                                                    |
| [`registry`](_autosummary/falaw.registry.html.md#module-falaw.registry)                   | Tool and model registries.                                                                                            |
| [`reprice`](_autosummary/falaw.reprice.html.md#module-falaw.reprice)                     | Re-quote a persisted [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan) at today's rates (falaw#60).         |
| [`results`](_autosummary/falaw.results.html.md#module-falaw.results)                     | Result wrapper: parse fal responses into typed assets, lazy download.                                                 |
| [`scene`](_autosummary/falaw.scene.html.md#module-falaw.scene)                         | Scene IR: the editable structure that survives all the way to the pixels.                                             |
| [`schema_export`](_autosummary/falaw.schema_export.html.md#module-falaw.schema_export)         | Export the Python SSOT as committed JSON for the TypeScript twin (`ts/`).                                             |
| [`session`](_autosummary/falaw.session.html.md#module-falaw.session)                     | Session: optional stateful controller over a sequence of falaw operations.                                            |
| [`testing`](_autosummary/falaw.testing.html.md#module-falaw.testing)                     | Make a suite that uses falaw genuinely offline — the fake asset transport.                                            |


# _autosummary/falaw.journal.html.md

# falaw.journal

Agent journal: append-only log of notes, issues and improvements.

Why this exists: agents (and falaw itself) leave traces here whenever
something is harder than expected, surprising, or worth remembering. The
Claude skill teaches future sessions to read recent entries before doing
novel work, so each session starts smarter than the last.

Usage:

```pycon
>>> from falaw import journal
>>> journal.note("schnell tier returns 1024x1024 by default")
>>> journal.issue("FLUX dev refused a benign prompt as NSFW",
...               suggestion="Try guidance_scale=2.0")
>>> journal.improvement("Add an `upscale_image` wrapper for clarity-upscaler",
...                     tags=("backlog",))
```

### Functions

| `improvement`(text, \*\*kw)   |    |
|-------------------------------|----|
| `issue`(text, \*\*kw)         |    |
| `note`(text, \*\*kw)          |    |
| `recent`([n])                 |    |

### Classes

| [`Journal`](_autosummary/falaw.journal.html.md#falaw.journal.Journal)([directory])                               | File-backed append-only journal.   |
|-----------------------------------------------------------------------------------------------------|------------------------------------|
| [`JournalEntry`](_autosummary/falaw.journal.html.md#falaw.journal.JournalEntry)(\*, id, timestamp, kind, text[, ...]) |                                    |

### *class* falaw.journal.Journal(directory=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

File-backed append-only journal.

Each entry becomes a single JSON file. Names are timestamp-prefixed, with
a process-wide sequence number breaking same-tick ties, so
`sorted(os.listdir(...))` yields insertion order without parsing.

```pycon
>>> import tempfile
>>> j = Journal(tempfile.mkdtemp())
>>> _ = j.append(kind="note", text="hello")
>>> _ = j.append(kind="issue", text="boom", suggestion="try X")
>>> [e.kind for e in j]
['note', 'issue']
```

### *class* falaw.journal.JournalEntry(\*, id, timestamp, kind, text, tags=(), suggestion='', context=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)


# _autosummary/falaw.liveness.html.md

# falaw.liveness

Is a catalogued endpoint still deployed? (falaw#76)

fal retires endpoints without notice (`fal-ai/imagen4/preview/ultra` answers
`404 Application "imagen4" not found`), and `pick_model` would keep
picking them. fal’s OpenAPI endpoint answers 404 for a dead id and 200 for a
live one without a billed call or an API key, so one GET per id is a free
liveness probe.

### Module Attributes

| [`OPENAPI_URL`](_autosummary/falaw.liveness.html.md#falaw.liveness.OPENAPI_URL)   | `?endpoint_id=<id>` selects the endpoint.                          |
|----------------------------------------------------------------|--------------------------------------------------------------------|
| [`StatusGetter`](_autosummary/falaw.liveness.html.md#falaw.liveness.StatusGetter)  | `url -> HTTP status`; injectable so tests never touch the network. |

### Functions

| [`check_model_liveness`](_autosummary/falaw.liveness.html.md#falaw.liveness.check_model_liveness)([ids, status_of])   | `{endpoint_id: is_live}` for `ids` (default: every catalogued model).                                             |
|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| [`dead_models`](_autosummary/falaw.liveness.html.md#falaw.liveness.dead_models)([ids, status_of])            | The ids from [`check_model_liveness()`](_autosummary/falaw.liveness.html.md#falaw.liveness.check_model_liveness) that are no longer deployed. |

### falaw.liveness.OPENAPI_URL *= 'https://fal.ai/api/openapi/queue/openapi.json'*

`?endpoint_id=<id>` selects the endpoint.

### falaw.liveness.StatusGetter

`url -> HTTP status`; injectable so tests never touch the network.

alias of `Callable`[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`int`](https://docs.python.org/3/builtins/functions.html#int)]

### falaw.liveness.check_model_liveness(ids=None, , status_of=None)

`{endpoint_id: is_live}` for `ids` (default: every catalogued model).

Only a 404 means dead; any other non-200 answer (rate limit, fal outage)
raises rather than report a live model as dead, which would get it deleted.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### falaw.liveness.dead_models(ids=None, , status_of=None)

The ids from [`check_model_liveness()`](_autosummary/falaw.liveness.html.md#falaw.liveness.check_model_liveness) that are no longer deployed.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/falaw.llm_prices.html.md

# falaw.llm_prices

One first-party LLM price table: what a direct Anthropic / OpenAI call costs (falaw#81).

[`falaw.llm_rates`](_autosummary/falaw.llm_rates.html.md#module-falaw.llm_rates) prices LLMs *routed through fal* (`fal-ai/any-llm`).
This module prices calls made straight to the model vendor, the way reelee’s
agent loop and chat relay and foley’s rungs make them. Those three used to keep
a private copy of the same numbers and drifted; this is the one home.

```pycon
>>> llm_call_cost_usd("anthropic", "claude-opus-4-7",
...                   {"input_tokens": 1_000_000, "output_tokens": 0})
(5.0, True)
```

The answer is `(usd, known)`. A model the table does not list is \*\*unknown,
not free\*\*: it is priced at the dearest rates listed for that provider and
`known` is `False`. The cost is then a *floor on a guess*, not a ceiling: an
unlisted model can cost more than the dearest listed one (an older, pricier
model). A spend cap can count it; a caller that must never under-count should
refuse an unpriced model by testing `known`:

```pycon
>>> usd, known = llm_call_cost_usd("anthropic", "claude-from-the-future",
...                                {"input_tokens": 1_000_000})
>>> known, usd > 0
(False, True)
```

`usage` is the vendor’s own reported usage, a mapping or an object with
attributes, in either of OpenAI’s two shapes (Responses or Chat Completions):

```pycon
>>> llm_call_cost_usd("openai", "gpt-5", {"prompt_tokens": 1000,
...     "completion_tokens": 100, "prompt_tokens_details": {"cached_tokens": 1000}})
(0.001125, True)
```

The table is data, not code: [`PRICES_FILENAME`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.PRICES_FILENAME) under `falaw/data`, one
row per model, each row carrying its own `source` and `date`. Nothing here
touches the network. Model ids are matched **exactly**: a dated snapshot is not
priced as its family, because older snapshots can cost more.

### Module Attributes

| [`PRICES_FILENAME`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.PRICES_FILENAME)       | Basename of the price table under `falaw/data`.                        |
|------------------------------------------------------------------------|------------------------------------------------------------------------|
| [`TOKENS_PER_PRICE_UNIT`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.TOKENS_PER_PRICE_UNIT) | Token count the table's `*_usd_per_mtok` columns are quoted per.       |
| [`COST_DECIMALS`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.COST_DECIMALS)         | Digits the returned USD is rounded to, which only removes float noise. |
| [`PROVIDERS`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.PROVIDERS)             | First-party providers the table prices.                                |

### Functions

| [`get_llm_price`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.get_llm_price)(provider, model, \*[, prices])   | The row for `model` (exact id), or `None` when the table does not list it.                                                                                       |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`list_llm_prices`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.list_llm_prices)([provider, prices])            | Every row, or the rows of one `provider`.                                                                                                                        |
| [`llm_call_cost_usd`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.llm_call_cost_usd)(provider, model, usage, \*)  | `(USD, known)` for one first-party call from the vendor's reported `usage`.                                                                                      |
| [`llm_prices_table_version`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.llm_prices_table_version)()                     | The table's version, a digest of its bytes (see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version)). |
| [`load_llm_prices`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.load_llm_prices)()                              | The committed table, keyed by `(provider, model)`.                                                                                                               |

### Classes

| [`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)(\*, provider, model, rates[, ...])       | One row of the first-party table: a model's rates and where they came from.   |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
| [`LlmPriceRates`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPriceRates)(\*, input_usd_per_mtok, ...[, ...]) | USD per million tokens for one prompt-length regime of one model.             |

### falaw.llm_prices.COST_DECIMALS *= 9*

Digits the returned USD is rounded to, which only removes float noise.

### *class* falaw.llm_prices.LlmPrice(, provider, model, rates, long_prompt_over_tokens=None, long_prompt_rates=None, source='', date='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One row of the first-party table: a model’s rates and where they came from.

### *class* falaw.llm_prices.LlmPriceRates(, input_usd_per_mtok, output_usd_per_mtok, cache_read_usd_per_mtok=None, cache_write_usd_per_mtok=None, cache_write_1h_usd_per_mtok=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

USD per million tokens for one prompt-length regime of one model.

### falaw.llm_prices.PRICES_FILENAME *= 'first_party_llm_prices.json'*

Basename of the price table under `falaw/data`.

### falaw.llm_prices.PROVIDERS *= ('anthropic', 'openai')*

First-party providers the table prices.

### falaw.llm_prices.TOKENS_PER_PRICE_UNIT *= 1000000*

Token count the table’s `*_usd_per_mtok` columns are quoted per.

### falaw.llm_prices.get_llm_price(provider, model, , prices=None)

The row for `model` (exact id), or `None` when the table does not list it.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)]

### falaw.llm_prices.list_llm_prices(provider=None, , prices=None)

Every row, or the rows of one `provider`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)]

### falaw.llm_prices.llm_call_cost_usd(provider, model, usage, , prices=None)

`(USD, known)` for one first-party call from the vendor’s reported `usage`.

- `provider="anthropic"`: `input_tokens` (uncached), `output_tokens`,
  `cache_read_input_tokens`, `cache_creation_input_tokens`.
- `provider="openai"`: Responses (`input_tokens` / `output_tokens` /
  `input_tokens_details.cached_tokens`) or Chat Completions
  (`prompt_tokens` / `completion_tokens` /
  `prompt_tokens_details.cached_tokens`). Input includes the cached part,
  which is billed at the cached rate; reasoning tokens are already in output.

`known` is `False` when the model is not in the table; the cost is then
priced at the provider’s dearest listed rates, which is not a ceiling for a
model pricier than any listed (see the module docstring). `prices=` swaps in the caller’s
own table, e.g. one reconciled against an invoice.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – `provider` is not one of [`PROVIDERS`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.PROVIDERS).
* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### falaw.llm_prices.llm_prices_table_version()

The table’s version, a digest of its bytes (see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version)).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.llm_prices.load_llm_prices()

The committed table, keyed by `(provider, model)`. Read once per process.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`LlmPrice`](_autosummary/falaw.llm_prices.html.md#falaw.llm_prices.LlmPrice)]


# _autosummary/falaw.llm_rates.html.md

# falaw.llm_rates

Per-routed-model LLM rates: the data behind a *ceiling* quote (falaw#50, option A).

`fal-ai/any-llm` is one registry record that routes thirty different models,
so a single record-level `cost_estimate` cannot express what a call actually
costs. Until now every planned LLM call quoted that one record’s flat
$0.001 — which under-quotes in the one direction a cost gate must never err.
Two facts, both read off the vendor’s own documentation, say by how much:

1. **fal bills the router per request, at two tiers.** Its endpoint doc says
   “$0.001 per requests” and “Premium models are charged at 10x the rate of
   standard models”, followed by the verbatim premium list. falaw’s default
   model — `anthropic/claude-sonnet-4.5` — is on that list, so *every*
   planned LLM call has been quoting a tenth of the published price.
2. **The routed model has an upstream per-token rate.** any-llm is “powered by
   OpenRouter” and shares its model-id namespace, so OpenRouter’s published
   prompt/completion rates are what fal resells. A reseller cannot durably
   charge less than its own cost, which makes those rates a floor on the true
   price of a long prompt on a big model.

Neither basis dominates the other: the flat request price wins on a short
prompt, the token rates win on a long one. So the ceiling is the \*\*maximum
over every basis known for that model\*\* ([`llm_ceiling_usd()`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.llm_ceiling_usd)) — the only
composition that cannot under-quote either way, and monotone non-decreasing in
both token hints.

The table itself is data, not code: [`RATES_FILENAME`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.RATES_FILENAME) under `falaw/data`,
versioned, one row per routed model, each row carrying its own `source` and
`date`. Nothing here touches the network — planning stays free.

```pycon
>>> rate = get_llm_rate('anthropic/claude-sonnet-4.5')
>>> rate.tier
'premium'
>>> rate.per_call_usd
0.01
```

A model the table does not know is **unknown, not free** — the estimator
returns `None` so the plan lights up `has_unknown_costs` and forces
approval, rather than falling back to the router’s flat price:

```pycon
>>> get_llm_rate('some/model-fal-never-heard-of') is None
True
>>> llm_ceiling_usd('some/model-fal-never-heard-of') is None
True
```

### Module Attributes

| [`RATES_FILENAME`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.RATES_FILENAME)                   | Basename of the rate table under `falaw/data`.                                                                              |
|-----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| [`TOKENS_PER_RATE_UNIT`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.TOKENS_PER_RATE_UNIT)             | Token count the table's `*_per_mtok` columns are quoted per.                                                                |
| [`DEFAULT_STALENESS_THRESHOLD_DAYS`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.DEFAULT_STALENESS_THRESHOLD_DAYS) | Warn once a priced row's `date` is this many days old.                                                                      |
| [`LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LLM_RATES_TABLE)                  | Identity a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) records for an LLM-priced call. |
| [`CUSTOM_LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.CUSTOM_LLM_RATES_TABLE)           | What a basis records when the caller quoted against their own `rates=` table.                                               |

### Functions

| [`get_llm_rate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.get_llm_rate)(model, \*[, rates])                 | The row for `model`, or `None` when the table does not price it.                                                                               |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| [`list_llm_rates`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.list_llm_rates)(\*[, rates])                      | Every row in the table, in file order — the catalogue view.                                                                                    |
| [`llm_ceiling_usd`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.llm_ceiling_usd)(model, \*[, input_tokens, ...])  | Ceiling cost in USD of `count` calls routing `model`, or `None`.                                                                               |
| [`llm_rates_table_version`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.llm_rates_table_version)()                        | The rate table's version — see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version). |
| [`load_llm_rates`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.load_llm_rates)()                                 | The committed rate table, keyed by routed-model id.                                                                                            |
| [`warn_if_stale`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.warn_if_stale)(rate, \*[, threshold_days, today]) | Warn (never raise) when `rate` is older than `threshold_days`.                                                                                 |

### Classes

| [`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)(\*, model, tier, per_call_usd[, ...])   | What one routed model costs, on every basis the table knows.   |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------|

### Exceptions

| [`LlmRatesStaleWarning`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRatesStaleWarning)   | A quote was served from a rate row older than the staleness threshold.   |
|-------------------------------------------------------------------------|--------------------------------------------------------------------------|

### falaw.llm_rates.CUSTOM_LLM_RATES_TABLE *= 'caller-supplied'*

What a basis records when the caller quoted against their own `rates=` table.

It has no file to digest, so its `table_version` stays empty — and, more
importantly, **the committed table must never re-price it**. Two tables are two
sets of books: re-quoting a $0.50 row from a caller’s reconciled invoice against
falaw’s $0.01 published rate is a 50x *under-quote* wearing the clothes of a
price drop. [`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) therefore refuses a basis whose `table`
does not match the pricer’s, reporting it as unknown; a caller who wants their
own numbers re-quoted passes a [`falaw.Pricer`](_autosummary/falaw.html.md#falaw.Pricer) that names this table.

### falaw.llm_rates.DEFAULT_STALENESS_THRESHOLD_DAYS *= 90*

Warn once a priced row’s `date` is this many days old.

`llm_rates.json` has no refresh job that runs itself (falaw#56): a
repriced or retiered model does not fail any test, so a quote can go stale
silently. This is the default threshold [`llm_ceiling_usd()`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.llm_ceiling_usd) checks a
row’s `date` against before returning a quote.

### falaw.llm_rates.LLM_RATES_TABLE *= 'falaw/data/llm_rates.json'*

Identity a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) records for an LLM-priced call.

### *class* falaw.llm_rates.LlmRate(, model, tier, per_call_usd, input_usd_per_mtok=None, output_usd_per_mtok=None, source='', date='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one routed model costs, on every basis the table knows.

#### model

The `model` argument value this row prices, e.g.
`"anthropic/claude-sonnet-4.5"`.

#### tier

fal’s billing tier for the router — `"standard"` or
`"premium"`. Informational; `per_call_usd` is already
materialized, so no multiplier arithmetic happens in code.

#### per_call_usd

What fal bills per request when this model is routed.

#### input_usd_per_mtok

Upstream list price per
[`TOKENS_PER_RATE_UNIT`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.TOKENS_PER_RATE_UNIT) prompt tokens, or `None` when no
upstream rate is published for this id — unknown, never zero.

#### output_usd_per_mtok

The same for completion tokens.

#### source

Where these numbers came from.

#### date

ISO date they were read on. Rates move; a quote is only as
fresh as its row.

#### notes

Free-form caveats.

#### *property* has_token_rates *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when both token columns are populated, so a token quote is possible.

### *exception* falaw.llm_rates.LlmRatesStaleWarning

Bases: [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning)

A quote was served from a rate row older than the staleness threshold.

### falaw.llm_rates.RATES_FILENAME *= 'llm_rates.json'*

Basename of the rate table under `falaw/data`.

### falaw.llm_rates.TOKENS_PER_RATE_UNIT *= 1000000*

Token count the table’s `*_per_mtok` columns are quoted per.

### falaw.llm_rates.get_llm_rate(model, , rates=None)

The row for `model`, or `None` when the table does not price it.

`None` is the load-bearing answer: it means *unknown*, and every caller
must propagate it as unknown rather than substituting the router’s flat
price for a model nobody has priced.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]

### falaw.llm_rates.list_llm_rates(, rates=None)

Every row in the table, in file order — the catalogue view.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]

### falaw.llm_rates.llm_ceiling_usd(model, , input_tokens=None, max_output_tokens=None, count=1, rates=None)

Ceiling cost in USD of `count` calls routing `model`, or `None`.

Two modes, decided by whether the caller passes a token hint at all.

**No hints** — quote fal’s published per-request price for the routed
model. That is a known vendor fact, and for the premium tier it is ten
times what the router’s flat record said:

```pycon
>>> flat = llm_ceiling_usd('anthropic/claude-sonnet-4.5')
>>> flat
0.01
```

**Both hints** — quote the **maximum** of that request price and
`input_tokens` in plus `max_output_tokens` out at the routed model’s
upstream rates. Taking the max is what makes it a ceiling: the flat price
binds a short prompt, the token sum binds a long one, and quoting either
alone under-quotes the other case.

```pycon
>>> long_prompt = llm_ceiling_usd(
...     'anthropic/claude-sonnet-4.5', input_tokens=20_000, max_output_tokens=4_000
... )
>>> round(long_prompt, 4)
0.12
>>> long_prompt > flat        # length raises the quote; the flat rate is a floor
True
```

Monotone in both hints, so a bigger hint never quotes less:

```pycon
>>> a = llm_ceiling_usd('openai/gpt-4o', input_tokens=1_000, max_output_tokens=100)
>>> b = llm_ceiling_usd('openai/gpt-4o', input_tokens=9_000, max_output_tokens=100)
>>> b >= a
True
```

Both hints are upper bounds the *caller* holds at plan time: the prompt is
already written, and `max_tokens` caps the response. Real output length
is unknowable before the call, which is exactly why this quotes the cap and
never a midpoint guess.

A token quote is therefore **all or nothing**. Asking for one that cannot
be bounded yields `None` — unknown, which forces approval — rather than a
number that looks like a ceiling and is not. That covers one hint without
the other (an uncapped response has no bound to quote):

```pycon
>>> llm_ceiling_usd('anthropic/claude-sonnet-4.5', input_tokens=50_000) is None
True
```

and a row with no published upstream token rates (the request price alone
is not a ceiling on a token bill nobody has priced):

```pycon
>>> llm_ceiling_usd(
...     'google/gemini-flash-1.5', input_tokens=50_000, max_output_tokens=1_000
... ) is None
True
```

* **Parameters:**
  * **model** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The `model` argument the call will send to the router.
  * **input_tokens** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – Upper bound on prompt tokens. `None` together with
    `max_output_tokens` selects the per-request basis alone.
  * **max_output_tokens** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – The response cap (any-llm’s `max_tokens`).
  * **count** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – Number of identical calls.
  * **rates** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]]) – Override table, for a caller who has reconciled real rates
    against their own billing.
* **Raises:**
  * [**TypeError**](https://docs.python.org/3/builtins/exceptions.html#TypeError) – a token hint or `count` that is not a plain `int`.
        A `bool` and a `float` both do the arithmetic silently —
        `True` prices one token, `1.5e4` prices a fractional one —
        so a caller who passed the wrong thing would get a number rather
        than a complaint.
  * [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – a negative token hint or `count`, which would make the
        token basis *lower* the ceiling — silently, and in the one
        direction a spend gate must never err.
* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`float`](https://docs.python.org/3/builtins/functions.html#float)]

### falaw.llm_rates.llm_rates_table_version()

The rate table’s version — see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version).

Cached per process, like the table itself.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.llm_rates.load_llm_rates()

The committed rate table, keyed by routed-model id.

Cached: the file is read once per process. Tests and callers with their
own reconciled numbers pass a mapping to `rates=` instead of mutating
this one.

* **Return type:**
  [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`LlmRate`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.LlmRate)]

### falaw.llm_rates.warn_if_stale(rate, , threshold_days=90, today=None)

Warn (never raise) when `rate` is older than `threshold_days`.

Safe to call at import time or on every quote: it only ever calls
[`warnings.warn()`](https://docs.python.org/3/library/warnings.html#warnings.warn), so a stale table degrades a quote’s trustworthiness
without ever breaking a caller. Returns the warning message when one was
issued, else `None` (handy for tests, which can assert on it without
a `pytest.warns` context).

`stacklevel=2` attributes the warning to [`llm_ceiling_usd()`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.llm_ceiling_usd)’s call
site here rather than to every external caller of *that* — a fixed
location, not one that moves with the caller. That matters because
Python’s default warning filter only dedups a repeated warning by its
exact `(message, category, module, lineno)`: a fixed location plus a
message keyed on `(model, date)` means a caller quoting the same stale
row in a loop (including a blank/unknown `date`, which always reads as
stale) sees the warning once per process, not once per call.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/falaw.llm_rates_refresh.html.md

# falaw.llm_rates_refresh

Refresh proposals for the LLM rate table, never a silent overwrite (falaw#56).

`falaw/data/llm_rates.json` was assembled by hand from two live sources and
has no refresh job, so it ages without saying so (falaw#50 option A found
this). Both sources move on their own schedule:

1. fal’s own any-llm endpoint doc (`ANY_LLM_DOC_URL`) — the `model` enum,
   the flat per-request price, and the verbatim premium list.
2. OpenRouter’s public catalogue (`OPENROUTER_MODELS_URL`) — the upstream
   `pricing.prompt` / `pricing.completion` any-llm resells.

[`refresh_llm_rates()`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.refresh_llm_rates) fetches both through an injectable seam, parses them
with pure functions ([`parse_any_llm_doc()`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.parse_any_llm_doc), [`parse_openrouter_models()`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.parse_openrouter_models)),
and diffs the result against the committed table. A price change is a
decision, not something this module gets to make on your behalf — the
committed `llm_rates.json` a test pins the premium set against is **never**
written here. `write=True` persists a *proposed* table plus a
human-readable diff alongside it; promoting the proposal over the committed
file is a separate, human step.

```pycon
>>> from falaw.llm_rates_refresh import parse_any_llm_doc
>>> doc = parse_any_llm_doc(
...     '- **Price**: $0.001 per requests\n'
...     '- **`model`** (`ModelEnum`, _optional_):\n'
...     '  Name of the model to use. Premium models are charged at 10x the '
...     'rate of standard models, they include: a/b, c/d. Default value: `"a/b"`\n'
...     '  - Options: `"a/b"`, `"c/d"`, `"e/f"`\n'
... )
>>> doc.base_per_call_usd
0.001
>>> doc.premium_multiple
10
>>> sorted(doc.premium_models)
['a/b', 'c/d']
>>> doc.model_enum
('a/b', 'c/d', 'e/f')
```

### Module Attributes

| [`ANY_LLM_DOC_URL`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.ANY_LLM_DOC_URL)         | model enum, flat price, premium list.                                    |
|--------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`OPENROUTER_MODELS_URL`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.OPENROUTER_MODELS_URL)   | upstream per-token list prices.                                          |
| [`PROPOSED_RATES_FILENAME`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.PROPOSED_RATES_FILENAME) | Basename of the proposed table a refresh writes under `falaw/data`.      |
| [`RATES_DIFF_FILENAME`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.RATES_DIFF_FILENAME)     | Basename of the human-readable diff a refresh writes under `falaw/data`. |
| [`TOKEN_RATE_PRECISION`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.TOKEN_RATE_PRECISION)    | Decimal places kept for a converted `*_usd_per_mtok` rate.               |

### Functions

| [`build_proposed_rows`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.build_proposed_rows)(any_llm, ...)         | One rate-table row per id in `any_llm.model_enum`, freshly derived.                                                 |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
| [`diff_llm_rate_tables`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.diff_llm_rate_tables)(committed_rows, ...) | Compare two `rows` lists (as loaded from the table JSON) by model id.                                               |
| [`format_llm_rates_diff`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.format_llm_rates_diff)(diff)               | Render [`diff_llm_rate_tables()`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.diff_llm_rate_tables)'s result as a human-readable report. |
| [`parse_any_llm_doc`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.parse_any_llm_doc)(text)                   | Pure parse of fal's any-llm `llms.txt` into [`AnyLlmDoc`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.AnyLlmDoc).             |
| [`parse_openrouter_models`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.parse_openrouter_models)(payload)          | `{model_id: (input_usd_per_mtok, output_usd_per_mtok)}` from OpenRouter.                                            |
| [`refresh_llm_rates`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.refresh_llm_rates)(\*[, write, ...])       | Fetch both sources, propose a fresh table, diff it — never overwrite.                                               |

### Classes

| [`AnyLlmDoc`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.AnyLlmDoc)(\*, base_per_call_usd, ...)   | The pricing-relevant facts parsed out of fal's any-llm doc.   |
|------------------------------------------------------------------------------------------|---------------------------------------------------------------|

### falaw.llm_rates_refresh.ANY_LLM_DOC_URL *= 'https://fal.ai/models/fal-ai/any-llm/llms.txt'*

model enum, flat price, premium list.

* **Type:**
  fal’s any-llm endpoint doc

### *class* falaw.llm_rates_refresh.AnyLlmDoc(, base_per_call_usd, premium_multiple, premium_models, model_enum)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The pricing-relevant facts parsed out of fal’s any-llm doc.

#### base_per_call_usd

The flat per-request price fal publishes for the
standard tier.

#### premium_multiple

How many times the base price a premium-tier
request costs.

#### premium_models

The routed-model ids fal’s doc lists as premium,
verbatim (a model here that is not in `model_enum` belongs to a
different endpoint, e.g. a vision-only variant, and is not a rate
table row).

#### model_enum

Every routed-model id the `model` argument accepts, in
the doc’s own order.

### falaw.llm_rates_refresh.OPENROUTER_MODELS_URL *= 'https://openrouter.ai/api/v1/models'*

upstream per-token list prices.

* **Type:**
  OpenRouter’s public model catalogue

### falaw.llm_rates_refresh.PROPOSED_RATES_FILENAME *= 'llm_rates.proposed.json'*

Basename of the proposed table a refresh writes under `falaw/data`.

### falaw.llm_rates_refresh.RATES_DIFF_FILENAME *= 'llm_rates.diff.txt'*

Basename of the human-readable diff a refresh writes under `falaw/data`.

### falaw.llm_rates_refresh.TOKEN_RATE_PRECISION *= 6*

Decimal places kept for a converted `*_usd_per_mtok` rate.

### falaw.llm_rates_refresh.build_proposed_rows(any_llm, openrouter_rates, , date)

One rate-table row per id in `any_llm.model_enum`, freshly derived.

The whole table is regenerated from the two sources every run — there is
no merge with the committed file here. The committed file only changes
when a human promotes a proposal, so there is nothing stale to carry
forward into it.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.llm_rates_refresh.diff_llm_rate_tables(committed_rows, proposed_rows)

Compare two `rows` lists (as loaded from the table JSON) by model id.

Returns `{"added", "removed", "changed", "unchanged"}`: `added`/
`removed` are sorted model-id lists; `changed` is a list of
`{"model", "field", "old", "new", "source", "date"}` for every
`_DIFFED_FIELDS` value that moved (`source`/`date` themselves
are provenance, not a price fact, so they never appear as a \*diffed
field\* even though every row gets a fresh stamp — but the *proposed*
row’s own `source`/`date` ride along on every change entry, so the
human-readable diff carries where the new number came from, not just
what it is); `unchanged` is a count.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.llm_rates_refresh.format_llm_rates_diff(diff)

Render [`diff_llm_rate_tables()`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.diff_llm_rate_tables)’s result as a human-readable report.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.llm_rates_refresh.parse_any_llm_doc(text)

Pure parse of fal’s any-llm `llms.txt` into [`AnyLlmDoc`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.AnyLlmDoc).

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – the doc is missing the price line, the `model` field,
      its premium sentence, or its options list — any of which means
      fal changed the doc’s shape and a human should look, not that the
      refresh should guess.
* **Return type:**
  [`AnyLlmDoc`](_autosummary/falaw.llm_rates_refresh.html.md#falaw.llm_rates_refresh.AnyLlmDoc)

### falaw.llm_rates_refresh.parse_openrouter_models(payload)

`{model_id: (input_usd_per_mtok, output_usd_per_mtok)}` from OpenRouter.

Pure parse of the `GET /api/v1/models` body. OpenRouter quotes
`pricing.prompt`/`pricing.completion` as USD-per-token strings; this
scales them to the table’s per-million-token unit. A model with a
missing, non-numeric, or non-positive prompt/completion price is left out
— unknown, never a guessed zero.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`float`](https://docs.python.org/3/builtins/functions.html#float)]]

### falaw.llm_rates_refresh.refresh_llm_rates(, write=False, http_get_any_llm_doc=None, http_get_openrouter_models=None, rates_path=None, proposed_path=None, diff_path=None, today=None)

Fetch both sources, propose a fresh table, diff it — never overwrite.

`rates_path` (default: the committed `llm_rates.json`) is read only,
never written: this function has no code path that touches the committed
table, by design — a repriced or retiered model is a decision for a human
to promote, not something a refresh job gets to make silently. With
`write=True`, the freshly-derived table is written to `proposed_path`
(default: `llm_rates.proposed.json` next to the committed file) and the
diff report to `diff_path` (default: `llm_rates.diff.txt`).

Returns a summary dict: `{"fetched_at", "committed_models",
"proposed_models", "diff", "write", "proposed_path", "diff_path"}`. The
two path keys are `None` when `write=False`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.local.html.md

# falaw.local

Local utilities: ffmpeg + PIL glue for stitching fal outputs.

The fal calls produce URLs. This module materializes them into local
files and assembles them into watchable scenes (concat, transitions,
thumbnails, etc.). Anything that’s cheap to do locally lives here so
the cloud calls stay focused on generation.

`ffmpeg` is the one external dependency. The functions raise a clear
error if it’s missing.

### Functions

| [`concatenate_clips`](_autosummary/falaw.local.html.md#falaw.local.concatenate_clips)(clip_urls, \*, output_path)   | Materialize each clip and concatenate into one mp4.   |
|--------------------------------------------------------------------------------------------------|-------------------------------------------------------|
| [`extract_thumbnail`](_autosummary/falaw.local.html.md#falaw.local.extract_thumbnail)(clip_url, \*, output_path)    | Save a single frame from a clip as a PNG.             |
| `has_ffmpeg`()                                                                                   |                                                       |
| [`overlay_audio`](_autosummary/falaw.local.html.md#falaw.local.overlay_audio)(video_url, audio_url, \*, ...)    | Mix an audio track onto a video clip.                 |

### falaw.local.concatenate_clips(clip_urls, , output_path, transition_s=0.0, audio=True)

Materialize each clip and concatenate into one mp4.

* **Parameters:**
  * **clip_urls** ([`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – ordered iterable of fal-served clip URLs (or local paths).
  * **output_path** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – where to write the concatenated mp4.
  * **transition_s** ([`float`](https://docs.python.org/3/builtins/functions.html#float)) – crossfade duration in seconds (0 = hard cut).
  * **audio** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – whether to include audio in the output.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  The output path.

### falaw.local.extract_thumbnail(clip_url, , output_path, at_seconds=1.0)

Save a single frame from a clip as a PNG.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.local.overlay_audio(video_url, audio_url, , output_path, replace_audio=True)

Mix an audio track onto a video clip.

With `replace_audio=True` (default) the original audio is dropped.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/falaw.operations.audio.html.md

# falaw.operations.audio

Audio operations: text-to-speech and friends.

### Functions

| [`generate_audio`](_autosummary/falaw.operations.audio.html.md#falaw.operations.audio.generate_audio)(prompt, \*[, kind, ...])           | Generate ambient/SFX/music from a prompt.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------|
| [`text_to_speech`](_autosummary/falaw.operations.audio.html.md#falaw.operations.audio.text_to_speech)(text, \*[, quality, voice, ...])   | Synthesize speech.                          |
| [`voice_clone`](_autosummary/falaw.operations.audio.html.md#falaw.operations.audio.voice_clone)(reference_audio_url, text, \*[, ...]) | Generate speech in a cloned voice.          |

### falaw.operations.audio.generate_audio(prompt, , kind='ambient', duration_s=None, model_id=None, extra=None)

Generate ambient/SFX/music from a prompt. Argument shape mirrors
[`falaw.plan_generate_audio()`](_autosummary/falaw.html.md#falaw.plan_generate_audio) exactly, so planned and eager calls
with identical inputs collapse to the same cache entry.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.audio.text_to_speech(text, , quality='balanced', voice=None, model_id=None, extra=None)

Synthesize speech. `voice` semantics are model-specific.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.audio.voice_clone(reference_audio_url, text, , model_id=None, extra=None)

Generate speech in a cloned voice.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)


# _autosummary/falaw.operations.avatar.html.md

# falaw.operations.avatar

Avatar / lip-sync operations and composers.

Two distinct primitives, often confused:

* `animate_face` — image + audio → talking video. The face is a still;
  the model animates lips/expressions over the audio. Backed by ai-avatar
  / omnihuman (“avatar” registry category).
* `lipsync` — video + audio → re-synced video. The body and motion
  come from an existing video; only the mouth is replaced to match new
  audio. Backed by sync-lipsync / kling-lipsync (“lipsync” category).

Use `animate_face` when you only have a portrait. Use `lipsync` when
you already have a video clip.

The composer `talking_avatar_from_text` chains TTS → `animate_face`.

### Functions

| [`animate_face`](_autosummary/falaw.operations.avatar.html.md#falaw.operations.avatar.animate_face)(image_url, audio_url, \*[, ...])     | Animate a still face from audio.                                |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`lipsync`](_autosummary/falaw.operations.avatar.html.md#falaw.operations.avatar.lipsync)(video_url, audio_url, \*[, quality, ...]) | Re-sync mouth motion in an existing video to a new audio track. |
| [`talking_avatar_from_text`](_autosummary/falaw.operations.avatar.html.md#falaw.operations.avatar.talking_avatar_from_text)(text, image_url, \*)     | text + face image → talking video.                              |

### falaw.operations.avatar.animate_face(image_url, audio_url, , prompt='', quality='balanced', model_id=None, extra=None)

Animate a still face from audio. Image + audio → talking video.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.avatar.lipsync(video_url, audio_url, , quality='high', model_id=None, extra=None)

Re-sync mouth motion in an existing video to a new audio track.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.avatar.talking_avatar_from_text(text, image_url, , voice=None, prompt='', tts_quality='balanced', avatar_quality='balanced')

text + face image → talking video. Two fal calls, one Result.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)


# _autosummary/falaw.operations.html.md

# falaw.operations

Task-level verbs: stable, agent-friendly entry points.

Each submodule registers its functions via the `register_tool` decorator,
so importing this package populates the ToolRegistry as a side effect.

### Modules

| [`audio`](_autosummary/falaw.operations.audio.html.md#module-falaw.operations.audio)                 | Audio operations: text-to-speech and friends.                         |
|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`avatar`](_autosummary/falaw.operations.avatar.html.md#module-falaw.operations.avatar)               | Avatar / lip-sync operations and composers.                           |
| [`images`](_autosummary/falaw.operations.images.html.md#module-falaw.operations.images)               | Image-generation and image-manipulation operations.                   |
| [`llm`](_autosummary/falaw.operations.llm.html.md#module-falaw.operations.llm)                     | LLM-driven tools: text completion, screenplay → Scene IR, IR editing. |
| [`preproduction`](_autosummary/falaw.operations.preproduction.html.md#module-falaw.operations.preproduction) | Pre-production: produce reusable identity anchors for a Scene.        |
| [`render`](_autosummary/falaw.operations.render.html.md#module-falaw.operations.render)               | Beat / shot / scene rendering with caching.                           |
| [`video`](_autosummary/falaw.operations.video.html.md#module-falaw.operations.video)                 | Video-generation operations: text-to-video and image-to-video.        |


# _autosummary/falaw.operations.images.html.md

# falaw.operations.images

Image-generation and image-manipulation operations.

### Functions

| [`composite_character_in_environment`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.composite_character_in_environment)(...[, ...])    | Place a character into an environment as one composited still.    |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`edit_image`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.edit_image)(image_url, prompt, \*[, quality, ...]) | Edit an image with a natural-language instruction.                |
| [`generate_image`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.generate_image)(prompt, \*[, quality, ...])        | Generate an image from a text prompt.                             |
| [`generate_image_with_refs`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.generate_image_with_refs)(prompt, ...[, ...])      | Generate a new image conditioned on one or more reference images. |
| [`remove_background`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.remove_background)(image_url, \*[, quality, ...])  | Remove the background from an image.                              |
| [`upscale_image`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.upscale_image)(image_url, \*[, scale, ...])        | Upscale an image.                                                 |

### falaw.operations.images.composite_character_in_environment(character_image_url, environment_image_url, , prompt='', quality='balanced', model_id=None, extra=None)

Place a character into an environment as one composited still.

The single most user-visible primitive missing from muvid (per
interface_design_plan item E). With this, an agent can produce
“Thor in a bell tower” — the character anchor for a downstream
omnihuman lipsync — without manually compositing in an image editor.

Defaults to `fal-ai/flux-kontext/dev` (image_edit category at
balanced/high tier). Pass `model_id` to override
(e.g. `"fal-ai/flux-pro/kontext/max"` for highest quality, or
`"fal-ai/bytedance/seededit/v3/edit-image"` for SeedEdit).

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.images.edit_image(image_url, prompt, , quality='balanced', model_id=None, extra=None)

Edit an image with a natural-language instruction.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.images.generate_image(prompt, , quality='balanced', image_size='landscape_4_3', model_id=None, extra=None)

Generate an image from a text prompt.

`image_size` is a fal preset (`landscape_16_9` …); models that size by
`aspect_ratio` (`nano-banana-pro`) get the equivalent ratio, with
`extra={"resolution": "2K"}` for the pixel tier. A prompt over the
model’s cap raises [`falaw.FalPromptTooLong`](_autosummary/falaw.html.md#falaw.FalPromptTooLong) before any call. Check
[`falaw.model_params()`](_autosummary/falaw.html.md#falaw.model_params) for enums whose default may not be what you
want (Recraft `style` defaults to photoreal).

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.images.generate_image_with_refs(prompt, reference_image_urls, , quality='balanced', model_id=None, extra=None)

Generate a new image conditioned on one or more reference images.

The missing twin of [`generate_image()`](_autosummary/falaw.operations.images.html.md#falaw.operations.images.generate_image): text-to-image models silently
ignore reference images, so callers wanting a recurring subject to stay
consistent (a character’s face across storyboard panels) need a model that
actually ingests references. This routes to the `image_edit` category
(Flux Kontext et al.) and threads the references as `image_url` (first)

+ `image_urls` (all) — the same wire shape image-edit models understand.

Pass `model_id` to override the picked model.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.images.remove_background(image_url, , quality='high', model_id=None, extra=None)

Remove the background from an image.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.images.upscale_image(image_url, , scale=2.0, model_id=None, extra=None)

Upscale an image.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)


# _autosummary/falaw.operations.llm.html.md

# falaw.operations.llm

LLM-driven tools: text completion, screenplay → Scene IR, IR editing.

These wrap fal-ai/any-llm so the agent (or the user) can invoke an LLM
inline as part of a falaw workflow — e.g. parsing a treatment into a
structured Scene, or rewriting a Beat from a directorial note.

We intentionally keep these thin. The contract is: any-llm is the
universal LLM endpoint, you pick the model via `model`, and we return
plain Python (str or parsed dict).

### Functions

| [`apply_note_to_beat`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.apply_note_to_beat)(beat, note, \*[, model])       | Use the LLM to apply a directorial note to a Beat.                      |
|----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`apply_note_to_scene`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.apply_note_to_scene)(scene, note, \*[, model])     | Apply a cross-cutting note: LLM proposes per-beat edits, we apply them. |
| [`llm_complete`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete)(prompt, \*[, system, model, ...])    | Single-shot LLM completion.                                             |
| [`llm_complete_with_receipt`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete_with_receipt)(prompt, \*[, ...])      | Single-shot LLM completion that also reports what it cost.              |
| [`parse_screenplay`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.parse_screenplay)(text, \*[, title, style, model]) | Convert prose screenplay text into a Scene IR via an LLM call.          |

### Classes

| [`LlmReceipt`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.LlmReceipt)(application, model, cache_hit, ...)   | What one eager LLM call cost — the record the eager path never kept.   |
|---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|

### *class* falaw.operations.llm.LlmReceipt(application, model, cache_hit, chars_in, chars_out, tokens_in=None, tokens_out=None, estimated_cost_usd=None, cost_source='unknown')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What one eager LLM call cost — the record the eager path never kept.

The falaw half of falaw#50’s option B: the eager [`llm_complete()`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete)
returned a bare string, so LLM spend was invisible to every accounting
surface — not because estimates were `None`, but because \*nothing was
recorded at all\* (the field signature: real dollars spent beside
`plans: 0`). A caller that books receipts uses
[`llm_complete_with_receipt()`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete_with_receipt) and gets this alongside the text.

Field semantics, honest by construction:

- `chars_in` / `chars_out` are **measured facts** (prompt+system in,
  response text out).
- `tokens_in` / `tokens_out` are **best-effort** from the raw
  response’s usage block (OpenAI `prompt_tokens`/`completion_tokens`
  and Anthropic `input_tokens`/`output_tokens` shapes). `None`
  means *unrecorded by the provider response*, never zero.
- `estimated_cost_usd` is `0.0` on a cache hit (nothing was billed),
  else fal’s published per-request price **for the routed model**, read
  off the rate table ([`falaw.llm_ceiling_usd()`](_autosummary/falaw.html.md#falaw.llm_ceiling_usd)) — an estimate, not
  an observed bill. It is the routed model and not the `fal-ai/any-llm`
  record because the record carries fal’s *standard* tier, and a premium
  routed model bills ten times that: pricing every turn from the record
  under-reported premium spend tenfold in the ledger a consumer sums
  (falaw#55). `None` only when nothing can price it at all.
- `cost_source` says which of those cases produced the number, so a
  receipt consumer never has to guess: `"cache_hit"`,
  `"rate_table:per_call"`, `"registry:<kind>"` (the fallback for a
  model the table does not price), or `"unknown"`.

### falaw.operations.llm.apply_note_to_beat(beat, note, , model='anthropic/claude-sonnet-4.5')

Use the LLM to apply a directorial note to a Beat.

* **Return type:**
  [`Beat`](_autosummary/falaw.scene.html.md#falaw.scene.Beat)

### falaw.operations.llm.apply_note_to_scene(scene, note, , model='anthropic/claude-sonnet-4.5')

Apply a cross-cutting note: LLM proposes per-beat edits, we apply them.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### falaw.operations.llm.llm_complete(prompt, , system='', model='anthropic/claude-sonnet-4.5', temperature=0.7, extra=None)

Single-shot LLM completion. Returns the assistant text.

Delegates to [`llm_complete_with_receipt()`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete_with_receipt) (one argument-construction
site, so the two spellings share one cache identity) and discards the
receipt. A caller that accounts for spend wants the receipt variant.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.operations.llm.llm_complete_with_receipt(prompt, , system='', model='anthropic/claude-sonnet-4.5', temperature=0.7, extra=None)

Single-shot LLM completion that also reports what it cost.

Same arguments, same wire call, same cache identity as
[`llm_complete()`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.llm_complete) — the receipt is a side channel, observed from the
call this function was making anyway (the cache layer’s `cache_hit`
event, the response’s usage block, the registry’s estimate). Library
surface only — deliberately NOT a registered tool: the tool surface
keeps the one string-returning `llm_complete`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`LlmReceipt`](_autosummary/falaw.operations.llm.html.md#falaw.operations.llm.LlmReceipt)]

### falaw.operations.llm.parse_screenplay(text, , title='', style='', model='anthropic/claude-sonnet-4.5')

Convert prose screenplay text into a Scene IR via an LLM call.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)


# _autosummary/falaw.operations.preproduction.html.md

# falaw.operations.preproduction

Pre-production: produce reusable identity anchors for a Scene.

The downstream renderer reuses Character.reference_image_url for every
shot featuring that character, and Character.voice for every line they
speak. So the quality of the *anchors* set here propagates everywhere.

Each function returns an updated entity (Character / Environment) you
can feed back into the Scene via `scene.with_character(...)` etc.

### Functions

| [`cast_character`](_autosummary/falaw.operations.preproduction.html.md#falaw.operations.preproduction.cast_character)(name, description, \*[, ...])   | Create a Character with a canonical face (and optional voice).   |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`cast_voice`](_autosummary/falaw.operations.preproduction.html.md#falaw.operations.preproduction.cast_voice)(character, \*[, voice_id, ...])     | Attach or update the Voice on a Character.                       |
| [`establish_environment`](_autosummary/falaw.operations.preproduction.html.md#falaw.operations.preproduction.establish_environment)(name, description, \*)   | Create an Environment with a canonical establishing image.       |
| [`storyboard_shot`](_autosummary/falaw.operations.preproduction.html.md#falaw.operations.preproduction.storyboard_shot)(shot, \*[, environment, ...])  | Render a storyboard still for a Shot.                            |

### falaw.operations.preproduction.cast_character(name, description, , image_url='', style='', quality='high', voice_id='', reference_audio_url='', voice_style='')

Create a Character with a canonical face (and optional voice).

If `image_url` is given, we skip face generation and use that
image directly. Otherwise we run text-to-image with the description
(+ optional `style` suffix), cache the result, and use the URL.

* **Return type:**
  [`Character`](_autosummary/falaw.scene.html.md#falaw.scene.Character)

### falaw.operations.preproduction.cast_voice(character, , voice_id='', reference_audio_url='', style_notes='', model_id='')

Attach or update the Voice on a Character.

* **Return type:**
  [`Character`](_autosummary/falaw.scene.html.md#falaw.scene.Character)

### falaw.operations.preproduction.establish_environment(name, description, , time_of_day='', lighting='', image_url='', quality='high')

Create an Environment with a canonical establishing image.

* **Return type:**
  [`Environment`](_autosummary/falaw.scene.html.md#falaw.scene.Environment)

### falaw.operations.preproduction.storyboard_shot(shot, , environment=None, characters=(), style='', quality='balanced')

Render a storyboard still for a Shot.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)


# _autosummary/falaw.operations.render.html.md

# falaw.operations.render

Beat / shot / scene rendering with caching.

The directorial workflow only works if a single IR edit causes only the
*affected* parts to re-render. Each renderer here:

1. Computes a content hash from the IR (`beat_content_hash` /
   `shot_content_hash`) plus the relevant identity anchors.
2. Stores the result in `falaw.cache` keyed by that hash.
3. Returns a small dict with `url`, `cache_hit`, and `hash` so the
   scene renderer can build a manifest.

Outputs:

* `render_beat` — TTS (using the speaker’s voice) → lipsync to the
  speaker’s reference image. Returns a video URL.
* `render_shot` — a still or short clip of the visual frame
  (storyboard or i2v depending on quality).
* `render_scene` — orchestrates beats and shots, returns a manifest
  dict that can be persisted next to the Scene file.

### Functions

| [`iter_render_scene`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.iter_render_scene)(scene, \*[, tts_quality, ...])   | Yield `(kind, result)` pairs as each shot/beat finishes.          |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`manifest_path_for`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.manifest_path_for)(scene, \*[, dir])                | Return where `render_scene`'s manifest would be saved by default. |
| [`render_beat`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.render_beat)(beat, character, \*[, ...])            | Render one Beat to a lipsynced video.                             |
| [`render_scene`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.render_scene)(scene, \*[, tts_quality, ...])        | Render every shot and beat.                                       |
| [`render_shot`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.render_shot)(shot, \*[, environment, ...])          | Render a Shot as a still (default) or a short clip.               |

### falaw.operations.render.iter_render_scene(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=1)

Yield `(kind, result)` pairs as each shot/beat finishes.

`kind` ∈ `{"shot", "beat"}`. With `concurrency=1` results
arrive in submission order (shots before beats). With
`concurrency > 1` they arrive in completion order (use the
`"shot_id"` / `"beat_id"` keys to re-key by identity).

Cache hits are immediate: a fully-cached scene yields all results
in close succession even at `concurrency=1`.

### falaw.operations.render.manifest_path_for(scene, , dir=None)

Return where `render_scene`’s manifest would be saved by default.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.operations.render.render_beat(beat, character, , tts_quality='balanced', lipsync_quality='high', tts_model_id=None, avatar_model_id=None, force=False)

Render one Beat to a lipsynced video. Returns a small manifest dict.

* **Parameters:**
  * **tts_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the TTS model. When provided, takes precedence
    over the character’s voice.model_id and over `tts_quality`-based
    `pick_model`. Use this to force a specific TTS engine for one
    beat (e.g. eleven-v3 for emotional delivery, multilingual-v2 for
    consistency).
  * **avatar_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the avatar/lipsync model (e.g.
    `"fal-ai/bytedance/omnihuman/v1.5"` to bypass the default
    `ai-avatar` which is known to hang).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.operations.render.render_scene(scene, , tts_quality='balanced', lipsync_quality='high', shot_quality='balanced', shots_as_video=False, force=False, concurrency=1)

Render every shot and beat. Returns a manifest dict.

`concurrency` controls how many shots/beats run in parallel
against fal. The work is HTTP-bound, so a thread pool is enough.
Default `1` preserves serial behavior. Use
[`iter_render_scene()`](_autosummary/falaw.operations.render.html.md#falaw.operations.render.iter_render_scene) instead if you want results yielded as
each unit completes (for live UI updates).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.operations.render.render_shot(shot, , environment=None, characters=(), style='', as_video=False, quality='balanced', image_model_id=None, image_to_video_model_id=None, force=False)

Render a Shot as a still (default) or a short clip.

* **Parameters:**
  * **image_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the image-gen model used for the storyboard
    still (defaults to `pick_model(category="image", …)`).
  * **image_to_video_model_id** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Override the image-to-video model used when
    `as_video=True` (e.g. `"fal-ai/minimax/hailuo-02/pro/image-to-video"`).
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.operations.video.html.md

# falaw.operations.video

Video-generation operations: text-to-video and image-to-video.

### Functions

| [`image_to_video`](_autosummary/falaw.operations.video.html.md#falaw.operations.video.image_to_video)(image_url[, prompt, quality, ...])   | Animate a still image into a video.   |
|------------------------------------------------------------------------------------------------------|---------------------------------------|
| [`text_to_video`](_autosummary/falaw.operations.video.html.md#falaw.operations.video.text_to_video)(prompt, \*[, quality, ...])           | Generate a video from a text prompt.  |

### falaw.operations.video.image_to_video(image_url, prompt='', , quality='high', model_id=None, extra=None)

Animate a still image into a video.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.operations.video.text_to_video(prompt, , quality='high', model_id=None, extra=None)

Generate a video from a text prompt.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)


# _autosummary/falaw.outcomes.html.md

# falaw.outcomes

Per-call outcomes of running a [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan) — the partial-result type.

A [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan) is a fan-out: 200 panels is 200 [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan)
entries in one Plan. A `list[Artifact]` return has no room to say  *“call 7
failed, here are the other 199”*, so the only thing a bare list can do when one
call raises is throw the whole run away — including artifacts that were already
generated and **already billed**.

This module is the vocabulary for saying it properly. Three states, not two:

`succeeded`
: The call ran (or was served from cache) and produced an
  `lacing.Artifact`.

`failed`
: The call raised. The exception is kept, so the caller can classify it
  ([`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors) distinguishes rate-limiting from a locked account)
  and retry precisely this one call.

`blocked`
: The call never ran, because something it depends on did not succeed — a
  `"<from N>"` placeholder pointing at a failed upstream — or because the
  run was halted by an earlier failure.

The third state is what makes a chained plan safe to resume: a blocked call
must be *re-planned*, not retried, and a caller that cannot tell “blocked” from
“failed” will retry a call whose input does not exist.

### Examples

```pycon
>>> from falaw.plan import CallPlan
>>> call = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                 arguments={"prompt": "a tiger"}, output_kind="image",
...                 estimated_cost_usd=0.025)
>>> boom = CallOutcome(index=0, call=call, status="failed",
...                    error=RuntimeError("content filter"))
>>> boom.ok
False
>>> report = ExecutionReport(outcomes=(boom,))
>>> report.is_complete
False
>>> report.estimated_spend_usd
0.0
>>> [o.index for o in report.failed]
[0]
```

### Module Attributes

| [`CallStatus`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallStatus)   | What became of one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) in a run.   |
|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|

### Classes

| [`CallOutcome`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome)(\*, index, call, status[, ...])   | What happened to one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan), at its position in the Plan.   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`ExecutionReport`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.ExecutionReport)([outcomes])                   | The result of running a Plan: one [`CallOutcome`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome) per call, in order.            |

### *class* falaw.outcomes.CallOutcome(, index, call, status, artifact=None, error=None, cache_hit=False, blocked_by=(), reason='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What happened to one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan), at its position in the Plan.

`index` is the call’s position in `plan.calls` and is the identity a
caller retries or re-plans by — a report always carries exactly one outcome
per call, in plan order, so `index` is also the safe key for zipping a
Plan against anything the caller built alongside it.

#### artifact *: Artifact | [None](https://docs.python.org/3/builtins/constants.html#None)*

The materialized artifact. Set if and only if `status == "succeeded"`.

#### blocked_by *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[int](https://docs.python.org/3/builtins/functions.html#int), ...]*

Indices of the calls whose non-success blocked this one. `()` when the
call was blocked for a run-level reason rather than a dependency.

#### cache_hit *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Whether the result came from the cache **as observed at run time**.

Not the same thing as [`falaw.CallPlan.cache_status`](_autosummary/falaw.html.md#falaw.CallPlan.cache_status), which is a
*prediction* made at plan time and can be wrong in both directions (a
concurrent run filled the entry; a hit turned out to be unusable and was
re-executed). Run-level cost accounting reads this one.

#### call *: [CallPlan](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)*

The call this outcome is about — enough to retry it verbatim.

#### error *: [BaseException](https://docs.python.org/3/builtins/exceptions.html#BaseException) | [None](https://docs.python.org/3/builtins/constants.html#None)*

The exception the call raised. Set if and only if `status == "failed"`.

Kept as the exception object rather than a string so the caller can use
falaw’s typed hierarchy ([`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors)) to decide between backing off,
switching models, and giving up.

#### index *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Position in `plan.calls`. Stable, and unique within a report.

#### *property* ok *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

Shorthand for `status == "succeeded"`.

#### reason *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Human-readable explanation. Required for `blocked`; free otherwise.

#### status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['succeeded', 'failed', 'blocked']*

`"succeeded"` / `"failed"` / `"blocked"`. See the module docstring.

### falaw.outcomes.CallStatus

What became of one [`CallPlan`](_autosummary/falaw.html.md#falaw.CallPlan) in a run. See the module docstring.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘succeeded’, ‘failed’, ‘blocked’]

### *class* falaw.outcomes.ExecutionReport(outcomes=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The result of running a Plan: one [`CallOutcome`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome) per call, in order.

`len(report.outcomes) == len(plan.calls)` **always**, including on a run
where most calls failed. That invariant is the whole point: a consumer that
built something per call (nw builds one skeleton annotation per call) can
zip against `outcomes` and stay aligned. Zipping against a
*shorter* list of successes is the silent mis-pairing this type exists to
prevent.

#### artifacts_or_raise()

Every artifact in plan order, or re-raise the first failure’s exception.

The bridge back to the plain `list[Artifact]` contract of
[`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan). The exception raised is the **original** one
the call raised, unwrapped — falaw’s typed error hierarchy
([`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors)) is only useful to a caller if it survives the trip
through the executor.

A run with no failures but some blocked calls raises too: the list would
otherwise be short, and a short list is exactly the silent mis-pairing
this type exists to prevent.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[`Artifact`]

#### *property* blocked *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes whose call never ran, in plan order.

#### *property* cache_hit_savings_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Estimated USD *not* spent because a succeeded call was served from cache.

The run-time counterpart of [`falaw.Plan.cache_hit_savings_usd`](_autosummary/falaw.html.md#falaw.Plan.cache_hit_savings_usd)
(which is a plan-time prediction).

#### *property* estimated_spend_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

succeeded calls that were **not** cache hits.

Estimated, because it sums [`falaw.CallPlan.estimated_cost_usd`](_autosummary/falaw.html.md#falaw.CallPlan.estimated_cost_usd) —
falaw does not read fal’s invoice. Two deliberate exclusions:

- **Cache hits cost nothing**, and this reads the *observed*
  [`CallOutcome.cache_hit`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome.cache_hit), not the plan-time prediction.
- **Failed calls are not counted.** The vendor may or may not have
  billed a call that raised, and falaw cannot know which; adding an
  estimate for it would be inventing a number. Read
  [`failed`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.ExecutionReport.failed) to see how many calls are unaccounted for.

* **Type:**
  Estimated USD billed by this run

#### *property* failed *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes whose call raised, in plan order.

#### *property* has_unknown_costs *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when a call that actually billed has no price estimate.

The run-level twin of [`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs), and the
reason [`estimated_spend_usd`](_autosummary/falaw.outcomes.html.md#falaw.outcomes.ExecutionReport.estimated_spend_usd) must never be read on its own: an
unpriced call contributes `0.0` to the sum, so a report reading
`$0.00` means *either* “nothing was spent” *or* “we do not know what
was spent”. Those are not the same answer, and a budget gate that
cannot tell them apart approves the second one.

#### *property* is_complete *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when every call succeeded.

#### *property* produced *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[Artifact, ...]*

The artifacts that were made, in plan order.

**Shorter than the Plan when anything failed** — deliberately named so
it does not read like something to zip a per-call sequence against. Use
`outcomes` for anything positional.

#### *property* succeeded *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallOutcome](_autosummary/falaw.outcomes.html.md#falaw.outcomes.CallOutcome), ...]*

Outcomes that produced an artifact, in plan order.

#### summary()

A small JSON-able digest — counts, spend, and which indices failed.

For logs, telemetry and run records, where the artifacts and exception
objects themselves are not serializable.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.plan.html.md

# falaw.plan

Plan / Execute primitives — separate planning (data) from execution (effects).

Today every falaw operation is eager: `generate_image(prompt)` makes the
API call immediately. That means:

- A budget gate has to predict cost from outside the call and *hope* the
  prediction matches what the operation will actually do.
- Tests need a fake fal_client to exercise any code that touches an
  operation, even when the test is about composition or cost.
- A UI can’t show “you’re about to spend $4.12, click confirm” without
  a separate, parallel cost-prediction code path.

The fix is to give every operation two surfaces:

1. `plan_X(...) -> CallPlan`: pure data describing the call that *would*
   happen — model_id, arguments, predicted cost, cache status. No API contact.
2. `execute(plan, ...) -> list[Artifact]`: turns a Plan (one or more
   CallPlans) into materialized Artifacts. The eager wrappers
   (`generate_image`, etc.) are now thin: `execute(plan_X(...))[0]`.

A higher-level orchestrator (a music-video render, a storyboard generation)
builds a Plan by composing CallPlans across multiple operations. The Plan
gets a typed `total_cost_usd`, can be inspected, edited, dry-run, or
serialized — all without the network.

### Examples

```pycon
>>> from falaw.plan import CallPlan, Plan
>>> p1 = CallPlan(
...     tool="generate_image",
...     application="fal-ai/flux/dev",
...     arguments={"prompt": "a tiger", "image_size": "landscape_4_3"},
...     output_kind="image",
...     estimated_cost_usd=0.025,
...     cache_status="miss",
... )
>>> p2 = CallPlan(
...     tool="image_to_video",
...     application="fal-ai/minimax/hailuo-02/pro/image-to-video",
...     arguments={"image_url": "<from p1>"},
...     output_kind="video",
...     estimated_cost_usd=0.50,
...     cache_status="miss",
... )
>>> plan = Plan(calls=(p1, p2))
>>> plan.total_cost_usd
0.525
>>> plan.cache_hit_savings_usd
0.0
>>> [c.tool for c in plan.calls]
['generate_image', 'image_to_video']
```

Plans concatenate, so an orchestrator can build one shot’s Plan and then
append it to a scene-level Plan:

```pycon
>>> shot_plan = Plan(calls=(p1,))
>>> scene_plan = Plan(calls=()) + shot_plan + Plan(calls=(p2,))
>>> len(scene_plan.calls)
2
```

### Module Attributes

| [`CacheStatus`](_autosummary/falaw.plan.html.md#falaw.plan.CacheStatus)        | Whether the cache will short-circuit this call.                                                                                                                        |
|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`OutputKind`](_autosummary/falaw.plan.html.md#falaw.plan.OutputKind)         | Coarse class of what this call produces.                                                                                                                               |
| [`PLAN_DICT_SCHEMA`](_autosummary/falaw.plan.html.md#falaw.plan.PLAN_DICT_SCHEMA)   | The `schema` tag [`plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_to_dict) writes and [`plan_from_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_from_dict) expects. |
| [`DFLT_FETCH_BYTES`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_FETCH_BYTES)   | Whether [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) downloads media results to content-address them.                                                    |
| [`FETCH_BYTES_ENVVAR`](_autosummary/falaw.plan.html.md#falaw.plan.FETCH_BYTES_ENVVAR) | Env var overriding [`DFLT_FETCH_BYTES`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_FETCH_BYTES) process-wide.                                                                     |
| [`DFLT_CONCURRENCY`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_CONCURRENCY)   | How many calls of a Plan run at once by default.                                                                                                                       |
| [`CONTENT_REF_PREFIX`](_autosummary/falaw.plan.html.md#falaw.plan.CONTENT_REF_PREFIX) | Prefix marking a content hash where a cache key would otherwise hold a URL.                                                                                            |

### Functions

| [`call_plan_from_dict`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_from_dict)(d)                            | Rebuild a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) from a [`call_plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_to_dict) dict.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`call_plan_to_dict`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_to_dict)(call)                           | Convert a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) to a plain JSON-serializable dict.                                                  |
| [`cost_basis_from_dict`](_autosummary/falaw.plan.html.md#falaw.plan.cost_basis_from_dict)(d)                           | Rebuild a [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) from a [`cost_basis_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.cost_basis_to_dict) dict. |
| [`cost_basis_to_dict`](_autosummary/falaw.plan.html.md#falaw.plan.cost_basis_to_dict)(basis)                         | Convert a [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) to a plain JSON-serializable dict.                                                 |
| [`execute`](_autosummary/falaw.plan.html.md#falaw.plan.execute)(plan, \*[, on_event, dry_run, ...])       | Execute a Plan, returning a list of materialized :class:<br/><br/>```<br/>`<br/>```<br/><br/>lacing.Artifact\`s.                                        |
| [`execute_isolated`](_autosummary/falaw.plan.html.md#falaw.plan.execute_isolated)(plan, \*[, on_event, ...])       | Execute a Plan with **per-call failure isolation**, returning a report.                                                                                 |
| [`extract_first_url`](_autosummary/falaw.plan.html.md#falaw.plan.extract_first_url)(raw)                            | Find the first asset URL in a fal response, regardless of shape.                                                                                        |
| [`make_call_plan`](_autosummary/falaw.plan.html.md#falaw.plan.make_call_plan)(\*, tool, application, ...[, ...]) | Build a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) and (optionally) check the cache.                                                     |
| [`plan_dependencies`](_autosummary/falaw.plan.html.md#falaw.plan.plan_dependencies)(plan)                           | Per-call set of the call indices it references via `"<from N>"`.                                                                                        |
| [`plan_from_dict`](_autosummary/falaw.plan.html.md#falaw.plan.plan_from_dict)(d)                                 | Rebuild a [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan) from a [`plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_to_dict) dict.            |
| [`plan_hash`](_autosummary/falaw.plan.html.md#falaw.plan.plan_hash)(plan)                                   | Stable, plan-scoped **structural idempotency key** for a whole [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan).                                   |
| [`plan_to_dict`](_autosummary/falaw.plan.html.md#falaw.plan.plan_to_dict)(plan)                                | Convert a [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan) to a plain JSON-serializable dict.                                                      |

### Classes

| [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)(\*, tool, application[, backend, ...])   | A single planned fal call.                                                                                    |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)(\*, pricer, priced[, quantities, ...])  | How a [`CallPlan.estimated_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.estimated_cost_usd) was arrived at (falaw#60). |
| [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)([calls])                                     | An ordered sequence of [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) — a render plan, in essence. |

### falaw.plan.CONTENT_REF_PREFIX *= 'sha256:'*

Prefix marking a content hash where a cache key would otherwise hold a URL.

Self-describing on purpose: a reader of a cache manifest can tell at a glance
that an argument was keyed on *what the upstream produced* rather than \*where
it was served from\*, and the prefixed form can never be confused with a
literal URL argument.

### falaw.plan.CacheStatus

Whether the cache will short-circuit this call.

- `hit`: A cached response exists and will be returned without an API call.
- `miss`: No cache entry; the call will hit fal.
- `stale`: An entry exists but is expected to be invalidated (e.g. a
  `force=True` re-render asked for it). Today we don’t distinguish stale
  from miss for cost — both are billed.
- `unknown`: Plan was built without consulting the cache.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘hit’, ‘miss’, ‘stale’, ‘unknown’]

### *class* falaw.plan.CallPlan(\*, tool, application, backend='fal', arguments, output_kind, estimated_cost_usd=None, cache_status='unknown', expected_duration_s=None, metadata=<factory>, key_extra=<factory>, cost_basis=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single planned fal call. Pure data — no API contact yet.

`application` and `arguments` are the *exact* tuple
`cached_call_fal(application, arguments)` would take, so a Plan can be
cache-checked, executed, or replayed without ambiguity.

#### application *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The fal model id that will be invoked (e.g. `"fal-ai/flux/dev"`).
Backend-scoped: what it names depends on [`backend`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.backend).

#### arguments *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Keyword arguments to pass to fal. Will be JSON-canonicalized for
cache key computation; should be JSON-serializable.

#### backend *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which execution backend `execute_plan()` dispatches this call to
(falaw#15) — see [`falaw.backends`](_autosummary/falaw.backends.html.md#module-falaw.backends). Defaults to
[`falaw.canonical.DFLT_BACKEND`](_autosummary/falaw.canonical.html.md#falaw.canonical.DFLT_BACKEND) (`"fal"`), the only backend until a
second one lands. Enters the per-call cache key and `plan_hash` only
when it is **not** the default, so every call falaw has ever planned or
cached keeps its exact pre-#15 digest — see [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical).

#### *property* billable_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Cost that will actually be billed (0 on cache hit, estimate otherwise).

Returns `0.0` (not `None`) on cache hit or unknown estimate so
sums are well-defined; use [`estimated_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.estimated_cost_usd) `is None` to
check unknown status explicitly.

#### cache_status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['hit', 'miss', 'stale', 'unknown']*

Whether the cache will short-circuit this call. `"hit"` means
`execute` won’t bill, so [`Plan.total_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.total_cost_usd) and
[`Plan.cache_hit_savings_usd`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.cache_hit_savings_usd) reflect that.

#### cost_basis *: [CostBasis](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) | [None](https://docs.python.org/3/builtins/constants.html#None)*

How [`estimated_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.estimated_cost_usd) was computed, or `None` when nothing
recorded it (falaw#60). Present, the quote can be re-run against today’s
rate table by [`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan); absent, that function reports the
call as `"no_basis"` and clears its cost to unknown rather than passing a
stale figure off as current. Purely descriptive — it stays out of
[`plan_hash()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_hash) and the per-call cache key, and out of the serialized
dict entirely when unset, so every plan falaw has ever hashed or persisted
is unmoved.

#### estimated_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

Predicted cost in USD. `None` when the model has no `cost_estimate`
populated (callers can distinguish “free” from “unknown”).

#### expected_duration_s *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[float](https://docs.python.org/3/builtins/functions.html#float), [float](https://docs.python.org/3/builtins/functions.html#float)] | [None](https://docs.python.org/3/builtins/constants.html#None)*

`(min, max)` duration the model can produce, or `None` if no
duration contract is known. Plan-level validators can check that the
requested duration fits this range and raise `FalDurationOutOfRange`
*before* the call instead of letting it silently truncate.

#### key_extra *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Identity beyond the wire arguments — entries that change what the call
*produces* without appearing in `arguments`. Participates in the
per-call cache key AND `plan_hash`, under omit-if-empty (an empty dict
leaves every existing key byte-identical). Unlike `metadata`, which is
deliberately identity-free labelling, putting something here says “a
cached result minted without this value must not be reused”. First
customer: nw’s Transform `impl_version` (nw#27) — “same interface,
changed behaviour” must miss the cache without renaming anything.

#### metadata *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

Free-form labels for downstream consumers. Conventional keys:
`shot_id`, `beat_id`, `character_name`, `strategy`.

#### output_kind *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['image', 'video', 'audio', 'json', 'text', 'binary']*

What kind of Artifact this call will produce.

#### tool *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

High-level tool name — `"generate_image"`, `"image_to_video"`, etc.
Distinct from `application` because one tool may dispatch to several
fal models depending on quality tier.

### *class* falaw.plan.CostBasis(\*, pricer, priced, quantities=<factory>, table='', table_version='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

How a [`CallPlan.estimated_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.estimated_cost_usd) was arrived at (falaw#60).

A frozen quote goes stale the moment the rate table moves — 0.0.46 moved
falaw’s LLM prices tenfold — and a persisted plan cannot be re-quoted from
`application` and `arguments` alone: the quantity hints that priced it
(the clip’s duration, the prompt’s token bound) are estimator-only and
never enter the wire arguments. This records them, plus *which* table
priced them, so a saved plan can be re-quoted faithfully
([`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan)) and audited after the fact.

Pure data. Nothing here reaches the network, and nothing here enters
[`plan_hash()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_hash) or the per-call cache key: a basis says what a call
*cost*, never what it *produces*, so stamping one leaves every existing
digest byte-identical.

#### priced *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

a `falaw.ModelRecord.id` for the
catalogue pricer, the routed `model` argument for the LLM pricer.
Distinct from [`CallPlan.application`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.application), which for `fal-ai/any-llm`
names the router rather than the model that sets the price.

* **Type:**
  The entity that was priced

#### pricer *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Which pricing rule produced the figure — a key into
[`falaw.reprice.DFLT_PRICERS`](_autosummary/falaw.reprice.html.md#falaw.reprice.DFLT_PRICERS). `"model_catalogue"` for a record
priced out of `models.json`, `"llm_rates"` for a routed LLM priced out
of `llm_rates.json`. An unrecognized name re-prices as *unknown*, never
as the old number.

#### quantities *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)*

The estimator-only shape hints fed to the pricer, by keyword —
`{"seconds": 6.0}`, `{"input_tokens": 20000, "max_output_tokens": 4000}`.
Absent keys mean the hint was not supplied, which is what made a
quantity-priced call unpriceable in the first place. Omit-when-unset: a
hint that was `None` at plan time is left out rather than written as
`null`.

#### table *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Identity of the rate table consulted, e.g.
`"falaw/data/llm_rates.json"`. Free-form so a caller’s own reconciled
table can name itself.

#### table_version *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The version of that table at plan time — falaw stamps a short content
digest, so a table whose numbers moved gets a different value even when
its declared version did not. Empty means unversioned/unknown.

### falaw.plan.DFLT_CONCURRENCY *= 1*

How many calls of a Plan run at once by default.

One — a Plan executes sequentially unless the caller asks otherwise. Every call
is a paid vendor request, so parallelism is opt-in: it is the caller who knows
their rate limit, their budget, and whether they want 200 renders started at
once. ([`falaw.render_scene()`](_autosummary/falaw.html.md#falaw.render_scene) makes the same choice for the same reason.)

### falaw.plan.DFLT_FETCH_BYTES *= True*

Whether [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) downloads media results to content-address them.

True — an artifact whose `asset_id` is not the SHA-256 of its bytes breaks
`lacing.Artifact`’s contract and puts an expiring location into every
downstream cache key.

### falaw.plan.FETCH_BYTES_ENVVAR *= 'FALAW_FETCH_ARTIFACT_BYTES'*

Env var overriding [`DFLT_FETCH_BYTES`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_FETCH_BYTES) process-wide.

Not a production setting — see the `fetch_bytes` argument of [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute)
for what opting out costs. Read at call time, so it can be set after import.

**Do not reach for this to make a test suite offline.** It works, and that is
the trap: it silences the network by turning content addressing **off**, so the
suite becomes hermetic and simultaneously stops exercising the feature. Every
`asset_id` becomes a digest of the response rather than the SHA-256 of the
bytes, chained calls key on URLs, and no test then covers the path production
actually takes. Install a fake transport instead —
[`falaw.testing.fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets) is one line in a `conftest.py` — and the
suite stays offline *with* content addressing under test, on bytes it controls.
Both consumers that hit this chose the fake for exactly that reason
(thorwhalen/falaw#27).

### falaw.plan.OutputKind

Coarse class of what this call produces. Mirrors `lacing.Artifact.kind`
so the producer knows what shape of Artifact to materialize.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘image’, ‘video’, ‘audio’, ‘json’, ‘text’, ‘binary’]

### falaw.plan.PLAN_DICT_SCHEMA *= 'falaw.plan/v1'*

The `schema` tag [`plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_to_dict) writes and [`plan_from_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_from_dict)
expects. Bumped only on a breaking change to the dict shape.

### *class* falaw.plan.Plan(calls=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An ordered sequence of [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) — a render plan, in essence.

Plans compose: `a + b` returns a new Plan with `a.calls` followed by
`b.calls`. `Plan(calls=())` is the identity. Plans are frozen, so
edits return new Plans (use [`with_call_replaced()`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.with_call_replaced) for in-place-feel).

#### *property* cache_hit_savings_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

USD that would have been spent without the cache.

Equal to `sum(c.estimated_cost_usd for c in calls if c.cache_status == "hit"
and c.estimated_cost_usd is not None)`.

#### *property* has_unknown_costs *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True if any non-cache-hit call has no cost estimate.

Use this to refuse to gate on a budget when the estimate is incomplete.

#### *property* known_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

The priced part of [`total_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.total_cost_usd) — same number today,
but honest by construction.

`total_cost_usd` coerces unpriced calls to `$0.00` so sums stay
well-defined; a cumulative budget gate that reads it alone will
under-quote (400 unpriceable video calls total $0.00). Read this
together with [`unknown_call_count`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.unknown_call_count): the true cost is
`known_cost_usd` *plus an unknown amount* spread over that many
calls, and a correct gate refuses when the count is nonzero rather
than pretending the unknown part is free (falaw#18).

#### *property* total_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Sum of [`CallPlan.billable_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan.billable_cost_usd) across all calls.

#### *property* unknown_call_count *: [int](https://docs.python.org/3/builtins/functions.html#int)*

How many billable calls carry no price at all.

The countable form of [`has_unknown_costs`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.has_unknown_costs) — see
[`known_cost_usd`](_autosummary/falaw.plan.html.md#falaw.plan.Plan.known_cost_usd) for the budget-gate arithmetic it enables.

#### with_call_replaced(index, new_call)

Return a new Plan with `calls[index]` replaced.

* **Return type:**
  [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)

### falaw.plan.call_plan_from_dict(d)

Rebuild a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) from a [`call_plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_to_dict) dict.

`arguments` / `metadata` are copied (a deserialized plan owns its own
data); `expected_duration_s` is re-tupled. `backend` defaults to
`DFLT_BACKEND` when absent — a dict written before falaw#15 names no
backend, and it always meant `"fal"`.

`cost_basis` is absent on any plan written before falaw#60 and on any
call nothing stamped; it comes back as `None`, which
[`falaw.reprice_plan()`](_autosummary/falaw.html.md#falaw.reprice_plan) reports as `"no_basis"` — never as a quote
that is still current.

* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan.call_plan_to_dict(call)

Convert a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) to a plain JSON-serializable dict.

The inverse of [`call_plan_from_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_from_dict). `expected_duration_s` (a
`tuple`) becomes a 2-element list since JSON has no tuple type;
everything else is already JSON-native.

`backend` (falaw#15) is a **tolerated-default addition**, not a
[`PLAN_DICT_SCHEMA`](_autosummary/falaw.plan.html.md#falaw.plan.PLAN_DICT_SCHEMA) bump: it is always written, but
[`call_plan_from_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.call_plan_from_dict) defaults it to `DFLT_BACKEND` when
absent, so a dict from before this field existed still parses, and a
dict written by this version still parses under older falaw (the extra
key is simply never read there). No migration needed either direction.

`cost_basis` (falaw#60) is the same kind of addition under a stricter
rule — **omit-when-unset**: the key is written only when a basis exists, so
a plan built without one serializes to the exact bytes it did before the
field existed, and every stored plan, fixture and cassette is unmoved.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.plan.cost_basis_from_dict(d)

Rebuild a [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) from a [`cost_basis_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.cost_basis_to_dict) dict.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

### falaw.plan.cost_basis_to_dict(basis)

Convert a [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis) to a plain JSON-serializable dict.

`table` / `table_version` are omitted when empty, matching the
omit-when-unset discipline the rest of the wire shape follows — a basis
that names no table writes no key rather than a pair of `""`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.plan.execute(plan, , on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1)

Execute a Plan, returning a list of materialized :class:

```
`
```

lacing.Artifact\`s.

This is the **halt** policy: the first call that raises ends the run, and its
exception propagates unchanged (falaw’s typed hierarchy — see
[`falaw.errors`](_autosummary/falaw.errors.html.md#module-falaw.errors) — is what a caller classifies on, so it is never
wrapped). Use [`execute_isolated()`](_autosummary/falaw.plan.html.md#falaw.plan.execute_isolated) when one bad call must not discard
the rest of a fan-out; it returns an [`ExecutionReport`](_autosummary/falaw.html.md#falaw.ExecutionReport) with one
outcome per call instead of raising.

* **Parameters:**
  * **plan** (Plan) – The Plan to execute.
  * **on_event** (Optional[Callable]) – Optional per-call event subscriber (passed to `call_fal`).
  * **dry_run** (bool) – When True, no fal calls are made; synthetic Artifacts are
    returned with placeholder `asset_id` and `url=None`. Useful
    for exercising downstream composition without an API key.
  * **use_cache** (bool) – When True (default), executes via `cached_call_fal` so
    cache hits skip the network. When False, the cache is not touched
    **at all** — neither read nor written. That is the historical
    meaning and it is unchanged: a caller who genuinely wants no cache
    interaction still has it. To re-run a plan and *keep* what the
    re-run paid for, leave `use_cache=True` and pass `refresh`.
  * **refresh** (bool) – When True, skip the cache **read** and keep the cache
    **write** — the third mode (falaw#49). Every call executes fresh,
    and every fresh result is stored under the key it would have been
    read from, so the next run (or a downstream consumer) hits it.
    Requires `use_cache=True`, since there is no key to write under
    when the cache is off; the contradictory combination raises rather
    than silently picking one. This is the mode to reach for behind a
    `force` / “re-verify this” switch: without it, forcing a re-run
    discards the result it just paid for and guarantees the next
    consumer re-bills.
  * **artifact_converter** (Optional[ResultToArtifact]) – Per-CallPlan converter from raw fal response to
    `lacing.Artifact`. When `None` (default), a built-in
    converter handles the common shapes (`{images: [{url}]}`,
    `{video: {url}}`, `{audio: {url}}`). Mutually exclusive with
    `content_store` / `fetch_bytes` / `asset_fetcher`, which
    configure the built-in converter only — passing both raises, rather
    than silently ignoring the ones a custom converter cannot honour.
    Converters do not own `cost_usd`: the executor stamps it from
    the observed run outcome after conversion, overwriting whatever
    the converter set (falaw#26).
  * **content_store** – Injected `lacing.ArtifactStore` that media bytes
    are materialized into. Defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store) (a directory store
    rooted in the falaw cache). Point this at an S3-backed store to
    share content — and therefore cache hits — across machines.
  * **fetch_bytes** (Optional[bool]) – Whether to download each media result so its `asset_id`
    is the SHA-256 of its bytes. Defaults to [`DFLT_FETCH_BYTES`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_FETCH_BYTES)
    (true), overridable process-wide via [`FETCH_BYTES_ENVVAR`](_autosummary/falaw.plan.html.md#falaw.plan.FETCH_BYTES_ENVVAR).
    **Opting out forfeits caching for chained calls**: without bytes
    there is no content hash, so downstream calls fall back to keying
    on the upstream URL — which fal mints fresh per upload, so the
    downstream entry can never be reused across runs or machines. It
    also means `asset_id` is *not* a content hash, in violation of
    `lacing.Artifact`’s contract. Use it only when you genuinely
    want URL-only artifacts and no reuse.
  * **asset_fetcher** – Injected byte source (`url -> Iterable[bytes]`) used to
    read media results; defaults to
    [`falaw.content.default_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.default_url_fetcher). This is the per-call
    transport seam. A **hermetic test suite** usually wants the
    process-wide one instead — [`falaw.testing.fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets), built
    on [`falaw.content.using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher) — because a suite
    reaching falaw *through* its own public API has no `execute`
    call site to pass this to. Passing it here still wins over any
    installed default. (`$FALAW_FETCH_ARTIFACT_BYTES=0` also silences
    the network, but by turning content addressing off — see
    [`FETCH_BYTES_ENVVAR`](_autosummary/falaw.plan.html.md#falaw.plan.FETCH_BYTES_ENVVAR).)
  * **concurrency** (int) – How many calls may be in flight at once. `1`
    ([`DFLT_CONCURRENCY`](_autosummary/falaw.plan.html.md#falaw.plan.DFLT_CONCURRENCY)) runs the Plan sequentially, on the
    calling thread, exactly as it always has. Above 1, independent calls
    run on a thread pool bounded by this number — a Plan is I/O-bound
    (an HTTP request that fal takes tens of seconds to answer), so
    threads are the right tool and the bound is what keeps a 200-call
    fan-out from becoming 200 simultaneous paid requests. \*\*Chained
    calls are never parallelised with their producers\*\*: a call holding
    a `"<from N>"` placeholder waits for call `N`. Two things to
    weigh before raising it: the vendor’s rate limit, and memory —
    materializing a media result peaks at roughly twice the asset’s size
    (thorwhalen/lacing#25), so `concurrency` multiplies the peak.
* **Return type:**
  [*list*](https://docs.python.org/3/builtins/stdtypes.html#list)

## Failure handling — a paid result is never discarded

Two different things can go wrong when reading a result’s bytes, and they
get two different answers:

- **A fresh call whose bytes cannot be fetched.** fal has already run — and
  billed — the generation. Raising would throw away a result we paid for,
  typically over a transient network failure. So the artifact **degrades**:
  `url` is kept, `bytes_size` stays 0, `asset_id` is a digest of the
  response and is *not* claimed to be a content hash, and a
  [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning) is emitted. Downstream key resolution reads
  > `bytes_size == 0` and falls back to the URL — a guaranteed cache
  > *miss*, never a wrong hit.
- **A cache hit that cannot be materialized** — fal deleted the URL and the
  bytes are not in the content store. The entry is unusable, so it is
  treated as a **miss**: it is invalidated and the call re-executed once.
  A cache must never become a trap whose only escape is re-billing the
  whole plan with `use_cache=False`.

## Placeholder resolution — the wire/key split

Any string argument equal to `"<from N>"` (for an integer `N`) is
rewritten *just before* the call is made — so a multi-step plan (e.g.
generate_image → image_to_video) can reference the upstream output without
the planner needing to know its URL. The rewrite happens after the upstream
call has executed; planning itself is unaffected.

It happens **twice**, into two different argument sets, because the same
value cannot serve both jobs:

- the **wire** arguments get `artifacts[N].url` — what fal needs in order
  to fetch the input;
- the **key** arguments get `sha256:<artifacts[N].asset_id>` — the
  upstream’s content hash, so a byte-identical upstream regeneration
  produces a downstream cache *hit* instead of re-billing the expensive
  call. Keying on the URL instead is the defect this split exists to fix
  (falaw#14): fal mints a unique URL per upload, so a URL-keyed downstream
  entry is unreachable the moment the upstream genuinely re-runs.

An upstream artifact with no materialized bytes has no content hash, so its
key ref falls back to the URL — a guaranteed miss, never a wrong hit.

* **rtype:**
  list
* **returns:**
  One `lacing.Artifact` per [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) in `plan.calls`,
  in the same order.

### falaw.plan.execute_isolated(plan, , on_event=None, dry_run=False, use_cache=True, refresh=False, artifact_converter=None, content_store=None, fetch_bytes=None, asset_fetcher=None, concurrency=1, halt_on_failure=False)

Execute a Plan with **per-call failure isolation**, returning a report.

The fan-out counterpart of [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute). Where `execute` raises on the
first failure — discarding every artifact produced before it, each of which
fal has already billed — this returns an [`ExecutionReport`](_autosummary/falaw.html.md#falaw.ExecutionReport)
carrying one [`CallOutcome`](_autosummary/falaw.html.md#falaw.CallOutcome) per call: the successes with their
artifacts, the failures with their exceptions, and the calls that never ran
with the reason why.

`len(report.outcomes) == len(plan.calls)` always, so a caller that built
something per call can zip against `report.outcomes` and stay aligned.

* **Parameters:**
  **halt_on_failure** (bool) – When True, stop *submitting* work as soon as any call
  fails; everything not yet started is reported `blocked` with a
  run-level reason. This is what [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) uses, and at
  `concurrency=1` it reproduces the historical sequential
  behaviour exactly. Note that at `concurrency > 1` calls already
* **Return type:**
  ExecutionReport
  in flight are **not** cancelled — a fal request cannot be recalled
  once made, and pretending otherwise would discard results that were
  billed anyway.

:param All other arguments are as [`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute).:

## Three outcome states, not two

`failed` and `blocked` are different questions for the caller. A failed
call can be retried verbatim. A blocked one cannot: its input does not
exist, so it has to be re-planned after its producer succeeds. Any call
holding a `"<from N>"` placeholder whose call `N` did not succeed is
blocked, transitively.

### Examples

```pycon
>>> from falaw import CallPlan, Plan, execute_plan_isolated
>>> plan = Plan(calls=(CallPlan(tool="t", application="m",
...                             arguments={}, output_kind="image"),))
>>> report = execute_plan_isolated(plan, dry_run=True)
>>> report.is_complete, len(report.outcomes)
(True, 1)
```

### falaw.plan.extract_first_url(raw)

Find the first asset URL in a fal response, regardless of shape.

Public (no underscore) because it is now used from [`falaw.prune`](_autosummary/falaw.prune.html.md#module-falaw.prune) as
well: deciding whether pruning a blob makes a cache entry unmaterializable
is the *same* question this answers for `execute`, and the two must not
drift into two opinions about what a response’s asset is.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.plan.make_call_plan(, tool, application, arguments, output_kind, backend='fal', estimated_cost_usd=None, expected_duration_s=None, metadata=None, consult_cache=True, cost_basis=None)

Build a [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan) and (optionally) check the cache.

When `consult_cache=True` (the default), the cache is peeked using the
same key the eventual call would produce; `cache_status` is set to
`"hit"` if a cached entry exists, `"miss"` otherwise. This makes
`Plan.total_cost_usd` honest: a fully-cached Plan reports $0.

When `consult_cache=False` (e.g. for unit tests or “what would a fresh
run cost?” reporting), `cache_status` is `"unknown"`.

A **chained call** — arguments still holding a `"<from N>"` placeholder,
because the upstream call has not executed yet — is never peeked
(falaw#15, D2): its resolved key is not knowable at plan time, and the
unresolved key is never written by anything ([`execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) always keys
on the resolved form), so peeking it can only ever report a false
`"miss"` — never a real `"hit"`. That silently over-quotes cost and
under-reports the plan’s `cache_hit_savings_usd` on every chained call,
which under prepaid billing (a quote that may be *deducted*) is a billing
bug. `cache_status` is `"unknown"` here instead, exactly the case
[`CacheStatus`](_autosummary/falaw.plan.html.md#falaw.plan.CacheStatus) documents it for.

`cost_basis` records how `estimated_cost_usd` was arrived at so the
quote can be re-run later (falaw#60). It is descriptive only: it never
reaches the cache key or [`plan_hash()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_hash), and a plan built without one
is byte-identical to one built before the field existed.

* **Raises:**
  [**FalNonCanonicalArgument**](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument) – an argument cannot be hashed
      faithfully (non-JSON object, non-finite float, non-string mapping
      key). Raised here — while planning is still free — rather than at
      key-composition time on the way to the network (falaw#17).
* **Return type:**
  [`CallPlan`](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)

### falaw.plan.plan_dependencies(plan)

Per-call set of the call indices it references via `"<from N>"`.

The Plan’s dependency DAG, read straight off the placeholders — one
`frozenset` per call, in plan order, so `deps[3] == {1}` means call 3
consumes call 1’s output. An empty set means the call is **independent** and
may run concurrently with any other independent call, which is what
[`execute_isolated()`](_autosummary/falaw.plan.html.md#falaw.plan.execute_isolated) schedules on.

Also the plan’s structural validator, and it runs \*\*before a cent is
spent\*\*: a malformed reference used to surface only when execution reached
the offending call, i.e. after every call before it had been billed.

* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – a placeholder that is not `"<from N>"` for an integer
      `N`; an `N` outside the plan; or an `N` that does not run
      *before* the referencing call (including a self-reference) — the
      output would not exist yet, so it can only ever be a bug.
* **Return type:**
  [*tuple*](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[*frozenset*](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[*int*](https://docs.python.org/3/builtins/functions.html#int)], …]

```pycon
>>> a = CallPlan(tool="t", application="m", arguments={}, output_kind="image")
>>> b = CallPlan(tool="t", application="m",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_dependencies(Plan(calls=(a, b)))
(frozenset(), frozenset({0}))
>>> plan_dependencies(Plan(calls=(b,)))
Traceback (most recent call last):
    ...
ValueError: Placeholder '<from 0>' in call 0 references call 0, which does not run before it. ...
```

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`frozenset`](https://docs.python.org/3/builtins/stdtypes.html#frozenset)[[`int`](https://docs.python.org/3/builtins/functions.html#int)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]

### falaw.plan.plan_from_dict(d)

Rebuild a [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan) from a [`plan_to_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_to_dict) dict.

Raises `ValueError` if `d` carries an unrecognized `schema` tag — a
plan written by an incompatible future version should fail loudly, not
silently lose calls. A missing `schema` is tolerated (treated as v1) so
hand-written plans stay easy.

* **Return type:**
  [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)

### falaw.plan.plan_hash(plan)

Stable, plan-scoped **structural idempotency key** for a whole [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan).

Answers “does this whole plan match one I already ran?” — the handle a job
manager (its first customer, `nw.jobs`) uses to dedup double-submits and
to replay a resumed render for free. It is computed *before* execution and
with `<from N>` placeholders intact, so it is stable across re-plans of the
same structural request.

The digest canonicalizes each call over `{app, args, tool}`
([`falaw.canonical.plan_identity_payload()`](_autosummary/falaw.canonical.html.md#falaw.canonical.plan_identity_payload)) — matching
`_synthetic_artifact()`’s canonicalization, and deliberately **not** the
per-call content-addressed cache key (`falaw.cache._key()`, which keys on
`{app, args}` with no `tool`). `plan_hash` and the per-call cache key
therefore key on *different* bytes and must not be assumed to agree
call-for-call. Both projections live side by side in [`falaw.canonical`](_autosummary/falaw.canonical.html.md#module-falaw.canonical)
with one shared byte-form ([`falaw.canonical.canonical_blob()`](_autosummary/falaw.canonical.html.md#falaw.canonical.canonical_blob) — sorted
keys, **no** `default=str` fallback, no NaN), so an argument the form
cannot represent faithfully raises
[`falaw.errors.FalNonCanonicalArgument`](_autosummary/falaw.errors.html.md#falaw.errors.FalNonCanonicalArgument) instead of colliding, and a
new identity-bearing field is an explicit decision about both hashes.

Two structurally-identical plans hash equal; changing any call’s `app`,
`args`, or `tool` — or the *order* of calls — changes the hash.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> a = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...              arguments={"prompt": "a tiger"}, output_kind="image")
>>> b = CallPlan(tool="image_to_video", application="fal-ai/svd",
...              arguments={"image_url": "<from 0>"}, output_kind="video")
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(a, b)))
True
>>> plan_hash(Plan(calls=(a, b))) == plan_hash(Plan(calls=(b, a)))
False
```

`backend` (falaw#15) joins the hashed payload too, so a plan built for
one backend never dedups against the structurally-identical plan for
another — and, since it is included only when non-default, every plan
made of today’s (all-`"fal"`) calls hashes exactly as it did before:

```pycon
>>> comfy = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                   arguments={"prompt": "a tiger"}, output_kind="image",
...                   backend="comfyui")
>>> plan_hash(Plan(calls=(a,))) == plan_hash(Plan(calls=(comfy,)))
False
```

### falaw.plan.plan_to_dict(plan)

Convert a [`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan) to a plain JSON-serializable dict.

The result round-trips through [`plan_from_dict()`](_autosummary/falaw.plan.html.md#falaw.plan.plan_from_dict). This is the
substrate primitive a consumer (a persistence layer, an MCP transport, a
plan-diff tool) builds on — falaw owns the wire shape of its own Plan so
every consumer agrees on it. Carries a `schema` tag ([`PLAN_DICT_SCHEMA`](_autosummary/falaw.plan.html.md#falaw.plan.PLAN_DICT_SCHEMA))
so a future breaking change is detectable.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.pricing.html.md

# falaw.pricing

Read fal’s pricing API into `models.json` cost estimates (falaw#18).

19 of the catalogue’s 40 models carry no structured `cost_estimate`, so
`Plan.has_unknown_costs` — correctly — forces approval on nearly every
plan. fal publishes the real numbers (`GET /v1/models/pricing`), and this
module turns them into catalogue entries with `source="api"`:

```pycon
>>> from falaw import refresh_model_prices
>>> summary = refresh_model_prices()          # dry-run: reports, writes nothing
>>> summary = refresh_model_prices(write=True)   # persists to models.json
```

Design rules, in order of importance:

- **This is a refresh job, not a runtime lookup.** `plan_*` never touches
  the network; plans price themselves from the committed catalogue.
- **An unmapped billing unit is recorded, never guessed.** fal’s `unit` is
  a free string (“image”, “megapixel”, GPU/compute units, …); only units with
  an unambiguous `CostKind` mapping become estimates. A
  GPU-second is machine time, not output time — mapping it to `per_second`
  would misprice by the model’s realtime factor, so it stays unpriced and
  visible in the summary instead.
- **The API wins, loudly.** A hand-written price the API disagrees with is
  overwritten, and the delta is reported — stale pricing data is the normal
  case, not the exception.

### Module Attributes

| [`PRICING_URL`](_autosummary/falaw.pricing.html.md#falaw.pricing.PRICING_URL)         | fal's unit-pricing endpoint.                                       |
|----------------------------------------------------------------------|--------------------------------------------------------------------|
| [`MAX_IDS_PER_REQUEST`](_autosummary/falaw.pricing.html.md#falaw.pricing.MAX_IDS_PER_REQUEST) | The endpoint's documented per-request cap on `endpoint_id` values. |
| [`MAX_429_RETRIES`](_autosummary/falaw.pricing.html.md#falaw.pricing.MAX_429_RETRIES)     | Attempts per request before giving up on a rate-limited endpoint.  |

### Functions

| [`fetch_model_prices`](_autosummary/falaw.pricing.html.md#falaw.pricing.fetch_model_prices)(endpoint_ids, \*[, ...])     | `{endpoint_id: {"unit_price", "unit", "currency"}}` from fal's API.   |
|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| [`refresh_model_prices`](_autosummary/falaw.pricing.html.md#falaw.pricing.refresh_model_prices)(\*[, write, api_key, ...]) | Refresh `models.json` cost estimates from fal's pricing API.          |

### falaw.pricing.MAX_429_RETRIES *= 5*

Attempts per request before giving up on a rate-limited endpoint.

The bisection around unknown ids multiplies request count, and the live
endpoint rate-limits aggressively — a refresh job should wait its turn,
not die halfway with a partial price table.

### falaw.pricing.MAX_IDS_PER_REQUEST *= 50*

The endpoint’s documented per-request cap on `endpoint_id` values.

### falaw.pricing.PRICING_URL *= 'https://api.fal.ai/v1/models/pricing'*

fal’s unit-pricing endpoint. Accepts 1-50 `endpoint_id` values per call.

### falaw.pricing.fetch_model_prices(endpoint_ids, , api_key=None, http_get=None, batch_size=50)

`{endpoint_id: {"unit_price", "unit", "currency"}}` from fal’s API.

Batches requests at `batch_size` ids (the endpoint caps at
[`MAX_IDS_PER_REQUEST`](_autosummary/falaw.pricing.html.md#falaw.pricing.MAX_IDS_PER_REQUEST)). Ids the API does not know are simply
absent from the result — the caller decides what absence means. That
takes work: the live endpoint answers a batch containing even one
unknown id with a blanket 404, so a failed batch is bisected down to
the ids that actually price (O(unknown x log batch) extra requests).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

### falaw.pricing.refresh_model_prices(, write=False, api_key=None, http_get=None, models_path=None, today=None)

Refresh `models.json` cost estimates from fal’s pricing API.

Returns a summary dict; `write=False` (default) reports what would
change without touching the file. `models_path` and `today` exist
for tests (the fetch date lands in each estimate’s `notes`).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.prune.html.md

# falaw.prune

Capacity management for the falaw cache: see what is on disk, and reclaim it.

Before thorwhalen/falaw#14 the cache held kilobyte JSON manifests. It now
content-addresses every media result, so it also holds the **bytes** of every
generated image, clip and audio track. On any machine that has rendered a real
project that is gigabytes, and on the production box (a 40 GB volume) it is a
capacity question rather than a cosmetic one.

Nothing here runs automatically, and that is the design rather than an omission.
**Deleting a blob is a spending decision**: [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) treats a
cache entry it cannot turn back into bytes as a *miss* and re-executes it, so
disk reclaimed today is re-billed the next time that beat renders. Every prune
is therefore `dry_run=True` by default and returns a [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
that says — before anything is removed — how many cache entries the prune would
put back on the invoice.

The cache has six areas, with very different economics — and only one of them
is genuinely cheap to reclaim:

`manifests`
: The fal responses, keyed by call. Small. Dropping one \*\*re-bills that
  call\*\*, unconditionally.

`content`
: The content-addressed blobs (falaw#14). This is where the gigabytes are.
  Dropping one re-bills every call whose asset URL has since expired — fal
  deletes expired files permanently, so the blob was the last copy.

`assets`
: [`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset) output — a *copy* of a blob, not a hard
  link (deliberately; see [`falaw.content.write_blob_to_file()`](_autosummary/falaw.content.html.md#falaw.content.write_blob_to_file)). It
  doubles the on-disk cost of every materialized asset and is the **cheapest**
  thing to reclaim: while the blob survives, re-materializing is a local copy
  and costs nothing.

`url_index`
: `url -> content hash` hints. Tiny — and **not** droppable freely, despite
  the name. [`falaw.content.content_ref_for_url()`](_autosummary/falaw.content.html.md#falaw.content.content_ref_for_url) reaches a blob *only*
  through [`falaw.content.remembered_ref()`](_autosummary/falaw.content.html.md#falaw.content.remembered_ref), so deleting this index orphans
  the entire `content` area: every entry whose fal URL has since expired
  re-**renders**, and the bytes it needed are still on disk, unreachable.
  Nothing here prunes it.

`scenes`
: Saved Scene IR. Not media and not a cache — it is *authored input*, so
  losing one is lost work rather than a re-render. Nothing here prunes it.

`other`
: Whatever the named areas do not claim: a crashed write’s `.part`
  leftover, or a directory a future falaw feature adds without telling this
  module. Present so that the totals cannot silently under-report.

### Examples

What is actually on disk, broken down by area (`+SKIP`ed because it reads
the caller’s real cache, which on the production box is a multi-gigabyte walk —
`tests.test_prune` pins the same behaviour against a throwaway one):

```pycon
>>> usage = cache_usage()
>>> sorted(a.name for a in usage.areas)
['assets', 'content', 'manifests', 'other', 'scenes', 'url_index']
```

A prune is a dry run unless you say otherwise, and refuses to run unbounded
(this one raises before touching the disk, so it is safe to execute):

```pycon
>>> prune_content()
Traceback (most recent call last):
    ...
ValueError: prune_content needs a bound: pass older_than= and/or max_bytes=...
```

### Functions

| [`cache_usage`](_autosummary/falaw.prune.html.md#falaw.prune.cache_usage)()                                     | Disk usage of the falaw cache, broken down by area.                            |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| [`prune_assets`](_autosummary/falaw.prune.html.md#falaw.prune.prune_assets)(\*[, older_than, max_bytes, ...])    | Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22). |
| [`prune_content`](_autosummary/falaw.prune.html.md#falaw.prune.prune_content)(\*[, older_than, max_bytes, ...])   | Reclaim content-addressed blobs — the gigabytes (falaw#22).                    |
| [`prune_manifests`](_autosummary/falaw.prune.html.md#falaw.prune.prune_manifests)(\*[, older_than, max_bytes, ...]) | Reclaim cache entries — the fal responses themselves (falaw#22).               |

### Classes

| [`AreaUsage`](_autosummary/falaw.prune.html.md#falaw.prune.AreaUsage)(name, path, entries, bytes)             | Disk usage of one cache area.                                  |
|----------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| [`CacheUsage`](_autosummary/falaw.prune.html.md#falaw.prune.CacheUsage)(root, areas)                           | Where the falaw cache's disk is going, by area.                |
| [`PruneCandidate`](_autosummary/falaw.prune.html.md#falaw.prune.PruneCandidate)(key, path, bytes[, last_modified]) | One thing a prune would delete (or did).                       |
| [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)(area, dry_run[, candidates, ...])     | What a prune removed, or — with `dry_run=True` — would remove. |

### *class* falaw.prune.AreaUsage(name, path, entries, bytes)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Disk usage of one cache area.

`entries` counts the area’s own unit — cache entries for `manifests`,
blobs for `content`, files for `assets` and `url_index` — not files
on disk, which is why it is reported next to `bytes` rather than derived
from it.

`path` is `None` when the area has no directory of its own. That is the
case for `manifests` (scattered across two-character shard directories at
the cache root) and for `other`. It is deliberately not the cache root:
a caller reaching for `rmtree(usage.area(...).path)` would then destroy
the content store along with it.

### *class* falaw.prune.CacheUsage(root, areas)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where the falaw cache’s disk is going, by area.

The whole point of the breakdown: `size_bytes` alone cannot tell you
whether you are looking at gigabytes of irreplaceable blobs or gigabytes of
`assets/` copies that cost nothing to regenerate.

#### area(name)

The [`AreaUsage`](_autosummary/falaw.prune.html.md#falaw.prune.AreaUsage) named `name`.

* **Raises:**
  [**KeyError**](https://docs.python.org/3/builtins/exceptions.html#KeyError) – no such area. The valid names are `AREA_NAMES`.
* **Return type:**
  [`AreaUsage`](_autosummary/falaw.prune.html.md#falaw.prune.AreaUsage)

#### summary()

One human-readable line per area, largest first.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### *property* total_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Every byte under the cache root.

Equal to a full walk of the root, because `other` absorbs whatever the
named areas do not claim. That identity is the point: a capacity report
whose total is “the sum of the areas I thought to enumerate” silently
under-reports the moment falaw grows a directory nobody added here, and
under-reporting is the one direction a capacity tool cannot afford.

### *class* falaw.prune.PruneCandidate(key, path, bytes, last_modified=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One thing a prune would delete (or did).

`last_modified` is `None` when the age could not be determined — a
non-filesystem blob backend, or a manifest too corrupt to read. Such a
candidate is never selected by `older_than` (an unprovable age is not
evidence of staleness) and is evicted last under `max_bytes`.

### *class* falaw.prune.PruneReport(area, dry_run, candidates=(), deleted=(), kept_entries=0, kept_bytes=0, rebillable_entries=0, unreferenced_candidates=0, errors=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

What a prune removed, or — with `dry_run=True` — would remove.

`rebillable_entries` is the number this exists for: cache entries that
will **cost money again** because of this prune. Its meaning is exact per
area, and each is a different claim:

- `manifests` — every dropped entry re-bills, so it equals the candidate
  count.
- `content` — entries whose recorded response names an asset that resolves
  (through the `url -> hash` index) to a blob being dropped. Those become
  *unmaterializable* from cache; they re-download if fal still serves the
  URL and re-render if it does not, and falaw cannot tell which from here.
  It mirrors the predicate [`falaw.plan.execute()`](_autosummary/falaw.plan.html.md#falaw.plan.execute) applies to a hit from
  the **built-in** converter. It is therefore an *upper* bound, not an exact
  population: with a custom `artifact_converter=` the entry is never
  dropped (it returns a broken artifact instead), and with
  `fetch_bytes=False` dropping the blob changes nothing. Both errors are
  overcounts — the safe direction for a number you read before spending.
- `assets` — only the copies whose blob is *also* gone. While the blob
  survives, re-materializing is a local copy and costs nothing.

`unreferenced_candidates` is the other half of the story, and only
`content` populates it: blobs that **no manifest points at**. falaw cannot
tell whether such a blob is garbage or the last copy of something
irreplaceable — [`falaw.materialize_asset()`](_autosummary/falaw.html.md#falaw.materialize_asset) puts reference images and
locally-rendered `file://` media in the same store, and those never had a
fal response behind them. Counting them as `rebillable` would be wrong
(no cache entry re-bills), but reporting nothing would tell an operator the
prune is free when it may be destroying the only copy of a reference image.

#### *property* freed_bytes *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Bytes actually freed — or, under `dry_run`, that would be freed.

Sums `deleted`, **not** `candidates`. A deletion that failed (a
read-only volume, a permission error) leaves its bytes on disk, and a
capacity tool that reports them as reclaimed tells an operator staring
at a full disk that the problem is solved when it is not.

#### summary()

A human-readable line, phrased in the tense the run actually was.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.prune.cache_usage()

Disk usage of the falaw cache, broken down by area.

The structured form behind [`falaw.cache_stats()`](_autosummary/falaw.html.md#falaw.cache_stats). Read the module
docstring for what each area costs to reclaim.

* **Return type:**
  [`CacheUsage`](_autosummary/falaw.prune.html.md#falaw.prune.CacheUsage)

### falaw.prune.prune_assets(, older_than=None, max_bytes=None, dry_run=True, store=None)

Reclaim materialized asset copies — the cheapest disk in the cache (falaw#22).

`assets/` holds a **copy** of each blob rather than a hard link, which is
deliberate ([`falaw.content.write_blob_to_file()`](_autosummary/falaw.content.html.md#falaw.content.write_blob_to_file) explains why a link
would let a consumer corrupt the content store) but doubles the on-disk cost
of every materialized asset. That makes this the prune to reach for first:
while a blob survives, re-materializing its asset is a local copy and costs
nothing, so `rebillable_entries` counts only the copies whose blob is
*also* gone.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop copies last written more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
  * **store** – injected `lacing.ArtifactStore`, used only to ask whether
    each dropped copy still has a blob behind it; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store).
* **Returns:**
  with `area="assets"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given.

### falaw.prune.prune_content(, older_than=None, max_bytes=None, dry_run=True, store=None)

Reclaim content-addressed blobs — the gigabytes (falaw#22).

This is the **expensive** prune. A blob is the last copy of an asset once
fal has expired its URL (“expired files are permanently deleted and cannot
be recovered”), so dropping one can turn a free cache hit into a re-rendered
clip. The report says how many entries that applies to *before* you commit;
read `rebillable_entries`.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop blobs last written more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta).
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
  * **store** – injected `lacing.ArtifactStore`; defaults to
    [`falaw.content.default_content_store()`](_autosummary/falaw.content.html.md#falaw.content.default_content_store), whose deletes remove
    the file. An injected store keeps its own backend’s delete policy:
    a stock `ArtifactStore.from_directory` store moves each blob to
    the OS trash, which frees no space on that volume even though the
    report counts the bytes as freed.
* **Return type:**
  [*PruneReport*](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)

Blobs are listed and deleted through lacing’s contained
`iter_blobs`/`delete_blob` (falaw#66): nothing outside the blob root is
listed or removed, and an in-root symlink is unlinked, never its target.

* **Returns:**
  with `area="content"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given — see `_require_a_bound()`.

Both bounds may be combined; a blob selected by either is dropped.

```pycon
>>> report = prune_content(older_than=timedelta(days=90))
>>> report.dry_run
True
```

### falaw.prune.prune_manifests(, older_than=None, max_bytes=None, dry_run=True)

Reclaim cache entries — the fal responses themselves (falaw#22).

Manifests are kilobytes, so this is rarely where the disk is; it is here
because a stale *entry* is its own problem — it pins a model version and a
price you may no longer want served from cache.

Unlike [`prune_content()`](_autosummary/falaw.prune.html.md#falaw.prune.prune_content), the cost is unconditional: every dropped
entry re-bills its call on the next run, so `rebillable_entries` always
equals the candidate count.

* **Parameters:**
  * **older_than** (`Union`[[`float`](https://docs.python.org/3/builtins/functions.html#float), [`int`](https://docs.python.org/3/builtins/functions.html#int), [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta), [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – drop entries stored more than this ago — seconds, or a
    [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta). Read from the manifest’s own
    `stored_at`, falling back to file mtime.
  * **max_bytes** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`int`](https://docs.python.org/3/builtins/functions.html#int)]) – drop oldest-first until the area fits in this budget.
  * **dry_run** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – report without deleting. Default, deliberately.
* **Returns:**
  with `area="manifests"`.
* **Return type:**
  [`PruneReport`](_autosummary/falaw.prune.html.md#falaw.prune.PruneReport)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – neither bound was given.

Only the manifest is removed; blobs are shared by content hash across
entries and are never dropped from here — the same rule
[`falaw.drop_cache_entry()`](_autosummary/falaw.html.md#falaw.drop_cache_entry) follows.


# _autosummary/falaw.refresh.html.md

# falaw.refresh

Refresh fal.ai docs locally with conditional GETs.

Three layers, cheapest first:

* `is_stale(source)` — HEAD with `If-None-Match`; near-zero cost.
* `fetch_if_changed(source)` — conditional GET; saves and snapshots if changed.
* `refresh_llms()` / `refresh_full_docs()` — coordinate fetches across
  the doc set, save previous-version snapshots for diffing, journal results.

State (per-source ETag and Last-Modified) is kept under
`$FALAW_DATA_DIR/refresh/state.json` (default `~/.config/falaw/refresh/`);
previous-version snapshots live alongside as `<source>.prev` so a successful
refresh can be diffed against the prior content.

For scheduled / remote runs, set `FALAW_DATA_DIR` to a path *inside* the
repo (e.g. `misc/falaw_state`) so journal entries and refresh state are
committed back to the repo on each run.

### Functions

| `default_sources`(\*[, docs_dir])                                                               |                                                                      |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`diff_against_previous`](_autosummary/falaw.refresh.html.md#falaw.refresh.diff_against_previous)(source, \*[, n_context]) | Unified diff: previous snapshot vs current local file.               |
| [`fetch_if_changed`](_autosummary/falaw.refresh.html.md#falaw.refresh.fetch_if_changed)(source, \*[, save_snapshot])  | Conditional GET.                                                     |
| [`is_stale`](_autosummary/falaw.refresh.html.md#falaw.refresh.is_stale)(source, \*[, state])                  | Cheap HEAD: True iff the upstream ETag differs from saved state.     |
| [`refresh_full_docs`](_autosummary/falaw.refresh.html.md#falaw.refresh.refresh_full_docs)(\*[, docs_dir, ...])         | Re-crawl per-page docs and rebuild `fal_ai_docs_full.md`.            |
| [`refresh_llms`](_autosummary/falaw.refresh.html.md#falaw.refresh.refresh_llms)(\*[, docs_dir, journal])          | Refresh `llms.txt` and `llms-full.txt`; return a summary dict.       |
| [`refresh_state`](_autosummary/falaw.refresh.html.md#falaw.refresh.refresh_state)()                                | Return the saved per-source refresh state (etags, last fetch times). |

### Classes

| [`DocSource`](_autosummary/falaw.refresh.html.md#falaw.refresh.DocSource)(\*, name, url, local_path)   | A single document mirrored locally.   |
|-----------------------------------------------------------------------------------------|---------------------------------------|

### *class* falaw.refresh.DocSource(, name, url, local_path)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single document mirrored locally.

### falaw.refresh.diff_against_previous(source, , n_context=3)

Unified diff: previous snapshot vs current local file.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.refresh.fetch_if_changed(source, , save_snapshot=True)

Conditional GET. Updates the local file and saved state on a real change.

Returns `(changed, body)`: `changed` is False on 304 (or any non-200);
`body` is the new text only when changed.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`bool`](https://docs.python.org/3/builtins/functions.html#bool), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.refresh.is_stale(source, , state=None)

Cheap HEAD: True iff the upstream ETag differs from saved state.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### falaw.refresh.refresh_full_docs(, docs_dir=None, max_workers=16, force=False, journal=True)

Re-crawl per-page docs and rebuild `fal_ai_docs_full.md`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh.refresh_llms(, docs_dir=None, journal=True)

Refresh `llms.txt` and `llms-full.txt`; return a summary dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.refresh.refresh_state()

Return the saved per-source refresh state (etags, last fetch times).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# _autosummary/falaw.registry.html.md

# falaw.registry

Tool and model registries.

Two registries, both queryable by tags/categories:

* `ToolRegistry` — decorator-populated, holds the agent-facing surface.
* `ModelRegistry` — JSON-backed, catalog of fal models with quality tiers.

Bridges (skill / MCP / HTTP) read the tool registry. Operations call
`pick_model` to map a (category, quality) pair to a concrete fal model id.

### Module Attributes

| [`MODEL_CATALOGUE_TABLE`](_autosummary/falaw.registry.html.md#falaw.registry.MODEL_CATALOGUE_TABLE)   | Identity a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) records for a catalogue-priced call.   |
|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|

### Functions

| `get_model`(id)                                                                           |                                                                                                                                               |
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| `get_tool`(name)                                                                          |                                                                                                                                               |
| `list_models`(\*[, category, quality_tier])                                               |                                                                                                                                               |
| `list_tools`(\*[, tag])                                                                   |                                                                                                                                               |
| [`model_constraints`](_autosummary/falaw.registry.html.md#falaw.registry.model_constraints)(id)                    | The capability/limit fields for a model — the "static reminder of limitations" a shot-list builder surfaces.                                  |
| [`model_params`](_autosummary/falaw.registry.html.md#falaw.registry.model_params)(id)                         | Per-model parameters worth knowing before a call, `{name: spec}`.                                                                             |
| [`models_table_version`](_autosummary/falaw.registry.html.md#falaw.registry.models_table_version)()                   | The catalogue's version — see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version). |
| [`pick_model`](_autosummary/falaw.registry.html.md#falaw.registry.pick_model)(\*, category[, quality_tier]) | Pick a sensible fal model for a (category, quality) request.                                                                                  |
| [`register_tool`](_autosummary/falaw.registry.html.md#falaw.registry.register_tool)(\*\*spec_kwargs)           | Decorator: register the wrapped function as a falaw tool.                                                                                     |
| [`video_model_constraints`](_autosummary/falaw.registry.html.md#falaw.registry.video_model_constraints)()                | `model_constraints` for every video model in the catalog — the data a shot-list builder shows as its model-limits reference.                  |

### falaw.registry.MODEL_CATALOGUE_TABLE *= 'falaw/data/models.json'*

Identity a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) records for a catalogue-priced call.

### falaw.registry.model_constraints(id)

The capability/limit fields for a model — the “static reminder of
limitations” a shot-list builder surfaces. Resolves aliases.

Returns a JSON-able dict; `max_clip_seconds` etc. are `None` / empty
when unknown for that model.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.registry.model_params(id)

Per-model parameters worth knowing before a call, `{name: spec}`.

Each spec is `{"enum": [...], "default": ...}`; resolves aliases. For
`fal-ai/recraft/v3/text-to-image` this shows `style` defaults to
`realistic_image` — a photograph, whatever the prompt describes — and
lists the styles that actually give a woodcut or a linocut. Empty when the
catalogue records none (fal’s OpenAPI schema is the full source).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### falaw.registry.models_table_version()

The catalogue’s version — see [`falaw.base.data_table_version()`](_autosummary/falaw.base.html.md#falaw.base.data_table_version).

Cached per process, like the catalogue itself.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.registry.pick_model(, category, quality_tier='balanced')

Pick a sensible fal model for a (category, quality) request.

First-match semantics: when several models share a tier, the earlier
entry wins. Curated entries are written first in `data/models.json`,
so they take precedence over corpus-merged additions. If no model
has the exact tier, neighboring tiers are tried. KeyError only when
the category is empty.

* **Return type:**
  [`ModelRecord`](_autosummary/falaw.base.html.md#falaw.base.ModelRecord)

### falaw.registry.register_tool(\*\*spec_kwargs)

Decorator: register the wrapped function as a falaw tool.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)

```pycon
>>> @register_tool(name='echo', description='echo back', tags=('demo',))
... def _echo(x): return x
>>> get_tool('echo').name
'echo'
```

### falaw.registry.video_model_constraints()

`model_constraints` for every video model in the catalog — the data a
shot-list builder shows as its model-limits reference.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]


# _autosummary/falaw.reprice.html.md

# falaw.reprice

Re-quote a persisted [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan) at today’s rates (falaw#60).

A `CallPlan`’s `estimated_cost_usd` is frozen at plan time. That is right —
a quote is a statement about a moment — but rate tables move: falaw 0.0.46
re-quoted every premium LLM call **tenfold upward**, so a preview saved the day
before under-quotes the run it is being used to price. Under-quoting is the one
direction a cost gate must never err in.

Re-quoting cannot be done from `application` and `arguments` alone. The
quantity hints that priced a call — a clip’s duration, a prompt’s token bound —
are estimator-only and deliberately never enter the wire arguments, so they are
not on the serialized call. [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) is where a `plan_*`
records them, together with which rate table it consulted; this module is what
reads one back:

```pycon
>>> from falaw import plan_generate_image, Plan, reprice_plan
>>> plan = Plan(calls=(plan_generate_image("a tiger", consult_cache=False),))
>>> repriced = reprice_plan(plan)
>>> [c.status for c in repriced.calls]
['unchanged']
>>> repriced.plan.total_cost_usd == plan.total_cost_usd
True
```

Three rules, in order of importance:

- **Pure data.** Nothing here touches the network or a billing API, exactly
  like `plan_*`. Re-pricing reads the committed tables and does arithmetic.
- **A plan with no basis is never assumed current.** Such a call comes back as
  `"no_basis"` with its cost *cleared to* `None`. Keeping the frozen figure
  would be the bug this module exists to fix, dressed as a feature.
- **Unknown is unknown, never free.** A basis whose model has left the
  catalogue, whose pricer is not registered, or whose table no longer prices it
  re-prices to `None` — which lights up
  [`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs) and forces approval.

The per-call diff says which of those happened, and
[`CallRepricing.basis_changed`](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing.basis_changed) says whether the *table* moved underneath
the quote — the audit answer the frozen number could never give.

### Module Attributes

| [`RepriceStatus`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepriceStatus)    | What re-pricing was able to say about one call.                        |
|-------------------------------------------------------------------|------------------------------------------------------------------------|
| [`CATALOGUE_PRICER`](_autosummary/falaw.reprice.html.md#falaw.reprice.CATALOGUE_PRICER) | `CostBasis.pricer` for a call priced from a `models.json` record.      |
| [`LLM_RATES_PRICER`](_autosummary/falaw.reprice.html.md#falaw.reprice.LLM_RATES_PRICER) | `CostBasis.pricer` for a routed LLM call priced from `llm_rates.json`. |
| [`DFLT_PRICERS`](_autosummary/falaw.reprice.html.md#falaw.reprice.DFLT_PRICERS)     | The pricers falaw ships — one per rate table it owns.                  |

### Functions

| [`catalogue_cost_basis`](_autosummary/falaw.reprice.html.md#falaw.reprice.catalogue_cost_basis)(model_id, \*\*quantities)   | A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a call priced from `models.json`.             |
|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| [`llm_cost_basis`](_autosummary/falaw.reprice.html.md#falaw.reprice.llm_cost_basis)(routed_model, \*[, ...])          | A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a routed LLM call priced from the rate table. |
| [`reprice_plan`](_autosummary/falaw.reprice.html.md#falaw.reprice.reprice_plan)(plan, \*[, pricers])                | Re-quote every call in `plan` against today's rate tables.                                                                           |

### Classes

| [`CallRepricing`](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing)(\*, index, call, old_cost_usd, ...)   | The before/after for one call in a re-priced plan.                                                               |
|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| [`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer)(\*, quote, table[, version])                 | One pricing rule, and the table that backs it.                                                                   |
| [`RepricedPlan`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan)(\*, plan[, calls])                     | Result of [`reprice_plan()`](_autosummary/falaw.reprice.html.md#falaw.reprice.reprice_plan) — the new Plan plus the per-call diff. |

### falaw.reprice.CATALOGUE_PRICER *= 'model_catalogue'*

`CostBasis.pricer` for a call priced from a `models.json` record.

### *class* falaw.reprice.CallRepricing(, index, call, old_cost_usd, new_cost_usd, status, reason='', basis_changed=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The before/after for one call in a re-priced plan.

#### basis_changed *: [bool](https://docs.python.org/3/builtins/functions.html#bool)*

True when the rate table’s identity or version moved between the
persisted basis and today’s — the audit answer to “did the price change
because the *table* changed?”. Always `False` for `"no_basis"`, which
has nothing to compare against.

#### call *: [CallPlan](_autosummary/falaw.plan.html.md#falaw.plan.CallPlan)*

The re-priced call — the same call with today’s
`estimated_cost_usd` and a basis stamped with today’s table version.

#### *property* delta_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

`new - old`, or `None` when either side is unknown.

`None` rather than `0.0`: a call that lost or gained its price did
not move by zero, and a caller summing deltas must not be handed a
number that says it did.

#### index *: [int](https://docs.python.org/3/builtins/functions.html#int)*

Position in the original [`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan).

#### new_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

What today’s rates say, `None` for unknown or no-basis.

#### old_cost_usd *: [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)*

What the persisted plan said, `None` if it said unknown.

#### reason *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Why, when the status is `"unknown"` or `"no_basis"`. Empty otherwise.

#### status *: [Literal](https://docs.python.org/3/library/typing.html#typing.Literal)['unchanged', 'changed', 'unknown', 'no_basis']*

Which of the four cases this call fell into — see [`RepriceStatus`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepriceStatus).

### falaw.reprice.DFLT_PRICERS *: [Mapping](https://docs.python.org/3/library/typing.html#typing.Mapping)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [Pricer](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer)]* *= {'llm_rates': Pricer(quote=<function \_quote_from_llm_rates>, table='falaw/data/llm_rates.json', version=<functools._lru_cache_wrapper object>), 'model_catalogue': Pricer(quote=<function \_quote_from_catalogue>, table='falaw/data/models.json', version=<functools._lru_cache_wrapper object>)}*

The pricers falaw ships — one per rate table it owns.

Keyed by [`falaw.CostBasis.pricer`](_autosummary/falaw.html.md#falaw.CostBasis.pricer). A basis naming anything else
re-prices as `"unknown"`: an unrecognized rule is a reason to refuse a
number, not to keep the old one.

### falaw.reprice.LLM_RATES_PRICER *= 'llm_rates'*

`CostBasis.pricer` for a routed LLM call priced from `llm_rates.json`.

### *class* falaw.reprice.Pricer(\*, quote, table, version=<function Pricer.<lambda>>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One pricing rule, and the table that backs it.

The seam for a caller with their own numbers: build a `Pricer` closing
over a reconciled rate table and pass `pricers={\*\*DFLT_PRICERS,
LLM_RATES_PRICER: mine}` to [`reprice_plan()`](_autosummary/falaw.reprice.html.md#falaw.reprice.reprice_plan). The `table` /
`version` pair is stamped onto the re-priced call’s basis, so the next
audit can see which table produced which number.

`table` is also a **gate, not just a label**: a call whose basis names a
different table is reported as `unknown` rather than re-quoted, so a row
priced from your invoice is never silently re-quoted at falaw’s published
rate (or the reverse). Give your `Pricer` the same `table` string the
basis carries — [`falaw.llm_rates.CUSTOM_LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.CUSTOM_LLM_RATES_TABLE) for anything
a `plan_*` priced with `llm_rates=`.

#### quote *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[[CostBasis](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)], [float](https://docs.python.org/3/builtins/functions.html#float) | [None](https://docs.python.org/3/builtins/constants.html#None)]*

Price `basis` in USD, or return `None` for *unknown*. May raise; a
raise is reported as `"unknown"` with the exception text as the reason,
so one unpriceable row never aborts a whole plan’s re-quote.

#### table *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

Identity of the table this pricer reads, for the refreshed basis.

#### version *: [Callable](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

Today’s version of that table, for the refreshed basis.

### falaw.reprice.RepriceStatus

What re-pricing was able to say about one call.

- `unchanged`: re-quoted, and today’s rates give the same number.
- `changed`: re-quoted to a different number — the interesting case.
- `unknown`: a basis was present but could not be re-quoted — the model has
  left the catalogue, no pricer is registered under that name, the table no
  longer prices it, or the basis names a **different table** than the pricer
  reads (two sets of books do not re-quote each other). The new cost is
  `None`.
- `no_basis`: the call carries no [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) at all — it was
  planned before falaw#60, or by something that never recorded one. The new
  cost is `None`, because the honest answer to “what does this cost today?”
  is that nobody can tell.

alias of [`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[‘unchanged’, ‘changed’, ‘unknown’, ‘no_basis’]

### *class* falaw.reprice.RepricedPlan(, plan, calls=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Result of [`reprice_plan()`](_autosummary/falaw.reprice.html.md#falaw.reprice.reprice_plan) — the new Plan plus the per-call diff.

Read [`plan`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan.plan) for the re-quoted plan (it is an ordinary
[`falaw.Plan`](_autosummary/falaw.html.md#falaw.Plan), so `known_cost_usd` / `unknown_call_count` /
`has_unknown_costs` mean what they always mean), and [`calls`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan.calls) for
what changed and why.

#### calls *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

One entry per call of the input plan, in order.

#### *property* changed *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

Calls whose price moved — the ones a re-quote exists to surface.

#### *property* known_delta_usd *: [float](https://docs.python.org/3/builtins/functions.html#float)*

Sum of [`CallRepricing.delta_usd`](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing.delta_usd) over calls priced both times.

The movement you can actually see. It says nothing about the calls in
[`unpriced`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan.unpriced) — read that alongside it, never this alone.

#### plan *: [Plan](_autosummary/falaw.plan.html.md#falaw.plan.Plan)*

The re-priced plan. Structurally identical to the input — same calls,
same order, same arguments, so `plan_hash` is unmoved — with today’s
costs.

#### *property* unpriced *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[CallRepricing](_autosummary/falaw.reprice.html.md#falaw.reprice.CallRepricing), ...]*

`"unknown"` plus `"no_basis"`.

A budget gate refuses while this is non-empty, the same way it refuses
on [`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs) — the two are the same
judgement, one made at re-quote time.

* **Type:**
  Calls today’s rates cannot price

### falaw.reprice.catalogue_cost_basis(model_id, \*\*quantities)

A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a call priced from `models.json`.

`quantities` are [`falaw.estimate_call_cost()`](_autosummary/falaw.html.md#falaw.estimate_call_cost) keywords (`count`,
`seconds`, `megapixels`, `tokens`); `None` values are dropped, so a
hint that was never supplied is recorded as absent rather than as `null`
— the same thing the estimator saw.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

```pycon
>>> catalogue_cost_basis("fal-ai/flux/dev", seconds=None).quantities
{}
```

### falaw.reprice.llm_cost_basis(routed_model, , input_tokens=None, max_output_tokens=None, custom_rates=False)

A [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) for a routed LLM call priced from the rate table.

`custom_rates=True` records that the quote came from a caller-supplied
`rates=` table rather than the committed one: there is no file to digest,
so the version stays empty and the table names itself
[`falaw.llm_rates.CUSTOM_LLM_RATES_TABLE`](_autosummary/falaw.llm_rates.html.md#falaw.llm_rates.CUSTOM_LLM_RATES_TABLE). [`reprice_plan()`](_autosummary/falaw.reprice.html.md#falaw.reprice.reprice_plan) then
**refuses** to re-quote it with the committed table, reporting `unknown`
— a caller’s reconciled $0.50 row re-priced at falaw’s published $0.01 is a
50x under-quote, not a price drop. Re-quote it by passing a
[`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer) over your table that names that same identity.

* **Return type:**
  [`CostBasis`](_autosummary/falaw.plan.html.md#falaw.plan.CostBasis)

### falaw.reprice.reprice_plan(plan, \*, pricers={'llm_rates': Pricer(quote=<function \_quote_from_llm_rates>, table='falaw/data/llm_rates.json', version=<functools._lru_cache_wrapper object>), 'model_catalogue': Pricer(quote = <function \_quote_from_catalogue>, table='falaw/data/models.json', version=<functools._lru_cache_wrapper object>)})

Re-quote every call in `plan` against today’s rate tables.

Pure data: no network, no billing API, nothing executed. Returns a new
[`RepricedPlan`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan) — the input plan is untouched.

A call carrying a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) is re-quoted through the pricer
its basis names, with the exact quantity hints that produced the original
figure, and comes back with a basis re-stamped to today’s table version. A
call carrying no basis is reported as `"no_basis"` and its cost is
**cleared to** `None`: the frozen number is a fact about a past moment,
and silently re-presenting it as a current quote is the failure this
function exists to prevent. That clearing lights up
[`falaw.Plan.has_unknown_costs`](_autosummary/falaw.html.md#falaw.Plan.has_unknown_costs), which is exactly right — nobody can
say what that call costs today.

A basis is only re-quoted by a pricer reading the **same table** it names.
A mismatch is `"unknown"`, not a re-quote: two tables are two sets of
books, and pricing a caller’s reconciled $0.50 row at falaw’s published
$0.01 would be a 50x under-quote wearing the clothes of a price drop. Pass
a [`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer) naming that table to re-quote against the same books.

`cache_status` is carried through unchanged. Re-pricing answers “what
would this cost?”, not “is it still cached?”; peeking the cache here would
quietly turn a re-quote into an I/O operation and make a hit look like a
price drop. Re-plan the calls if you want a fresh cache reading.

* **Parameters:**
  * **plan** ([`Plan`](_autosummary/falaw.plan.html.md#falaw.plan.Plan)) – The plan to re-quote, typically just deserialized with
    [`falaw.plan_from_dict()`](_autosummary/falaw.html.md#falaw.plan_from_dict).
  * **pricers** ([`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer)]) – Pricing rules by [`falaw.CostBasis.pricer`](_autosummary/falaw.html.md#falaw.CostBasis.pricer) name. The seam
    for a caller who has reconciled real numbers against their own
    invoice — see [`Pricer`](_autosummary/falaw.reprice.html.md#falaw.reprice.Pricer).
* **Return type:**
  [`RepricedPlan`](_autosummary/falaw.reprice.html.md#falaw.reprice.RepricedPlan)

```pycon
>>> from falaw import Plan, CallPlan, reprice_plan
>>> stale = CallPlan(tool="generate_image", application="fal-ai/flux/dev",
...                  arguments={"prompt": "a tiger"}, output_kind="image",
...                  estimated_cost_usd=0.025)
>>> out = reprice_plan(Plan(calls=(stale,)))
>>> out.calls[0].status
'no_basis'
>>> out.plan.calls[0].estimated_cost_usd is None   # unknown, not $0.025
True
>>> out.plan.has_unknown_costs
True
```


# _autosummary/falaw.results.html.md

# falaw.results

Result wrapper: parse fal responses into typed assets, lazy download.

### Functions

| [`parse_response`](_autosummary/falaw.results.html.md#falaw.results.parse_response)(raw, \*, application, arguments)   | Best-effort parser over the common fal response shapes.      |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
| [`sniff_extension`](_autosummary/falaw.results.html.md#falaw.results.sniff_extension)(data)                             | Extension implied by the leading bytes ('' if unrecognised). |

### Classes

| [`Asset`](_autosummary/falaw.results.html.md#falaw.results.Asset)(url, kind[, content_type, width, ...])   | A single piece of generated media.                                  |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
| [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)([assets, raw, application, arguments])  | A fal call result with parsed assets and the original raw response. |

### *class* falaw.results.Asset(url, kind, content_type='', width=0, height=0, duration_s=0.0, metadata=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single piece of generated media.

Holds the URL plus minimal typed metadata. `download` materializes it.

#### download(, to=None, fix_suffix=False)

Download the asset to a file. Returns the local path.

The bytes are sniffed: some models return a different format than the
usual one (Recraft’s `vector_illustration/*` styles return SVG, not
PNG). If the suffix of `to` contradicts the content, a
`UserWarning` is issued; with `fix_suffix=True` the suffix is
corrected instead (so the returned path may differ from `to`).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### *class* falaw.results.Result(assets=<factory>, raw=<factory>, application='', arguments=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A fal call result with parsed assets and the original raw response.

The raw response is kept so callers can inspect provider-specific fields
(timings, seed, has_nsfw_concepts, …) that we do not normalize.

### falaw.results.parse_response(raw, , application, arguments)

Best-effort parser over the common fal response shapes.

fal models return a variety of layouts (lists, single objects, bare URLs).
We normalize each into Asset(url, kind, …). Unknown shapes pass through
as `raw` only — callers can read `result.raw` for anything we miss.

* **Return type:**
  [`Result`](_autosummary/falaw.results.html.md#falaw.results.Result)

### falaw.results.sniff_extension(data)

Extension implied by the leading bytes (’’ if unrecognised).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/falaw.scene.html.md

# falaw.scene

Scene IR: the editable structure that survives all the way to the pixels.

Design thesis (from the Directa Labs one-pager): a film stays in \*editable
structure\* from idea to final cut. The creator authors a Scene as data.
A directorial note becomes a single IR edit. The renderer picks it up
and re-renders only what changed.

All entities are frozen dataclasses with content-derived ids so:

* equality and hashing are structural — two beats with identical content
  share an id, and the cache can short-circuit re-rendering;
* edits create new entities (immutable); diffs against the previous
  version are obvious and surgical;
* serialization to JSON/YAML is mechanical (asdict).

The renderer (`falaw.operations.render`) consumes Scene; LLM tools
(`falaw.operations.llm`) produce or edit it.

### Functions

| [`beat_content_hash`](_autosummary/falaw.scene.html.md#falaw.scene.beat_content_hash)(beat, \*[, character])    | Hash everything that affects how the beat *renders*.      |
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| [`beat_id`](_autosummary/falaw.scene.html.md#falaw.scene.beat_id)(\*, speaker, line[, action, index]) | Deterministic, human-readable-ish beat id.                |
| `load_scene`(path)                                                                           |                                                           |
| `make_beat`(speaker[, line, action, emotion, ...])                                           |                                                           |
| `make_shot`(description, \*[, framing, ...])                                                 |                                                           |
| `save_scene`(scene, path)                                                                    |                                                           |
| [`scene_from_dict`](_autosummary/falaw.scene.html.md#falaw.scene.scene_from_dict)(d)                          | Inverse of asdict: reconstruct a Scene from a plain dict. |
| `scene_to_dict`(scene)                                                                       |                                                           |
| `shot_content_hash`(shot, \*[, environment])                                                 |                                                           |
| `shot_id`(\*, description[, framing, index])                                                 |                                                           |

### Classes

| [`Beat`](_autosummary/falaw.scene.html.md#falaw.scene.Beat)(\*, id[, speaker, line, action, ...])   | The atomic unit of a scene: who, says what, with what intent.   |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`Character`](_autosummary/falaw.scene.html.md#falaw.scene.Character)(\*, name[, description, ...])      | A reusable character: stable face, stable voice, stable style.  |
| [`Environment`](_autosummary/falaw.scene.html.md#falaw.scene.Environment)(\*, name[, description, ...])    | A reusable location/setting.                                    |
| [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)(\*, title[, style, characters, ...])   | The whole editable structure: cast, locations, shots, beats.    |
| [`Shot`](_autosummary/falaw.scene.html.md#falaw.scene.Shot)(\*, id[, description, framing, ...])    | A visual frame: framing + environment + characters in view.     |
| [`Voice`](_autosummary/falaw.scene.html.md#falaw.scene.Voice)(\*, name[, voice_id, ...])             | A character's voice spec.                                       |

### *class* falaw.scene.Beat(, id, speaker='', line='', action='', emotion='', shot_id='', duration_s=None, notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The atomic unit of a scene: who, says what, with what intent.

A Beat that has only `action` (no `line`) is a non-verbal beat.

### *class* falaw.scene.Character(, name, description='', reference_image_url='', voice=None, style_notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A reusable character: stable face, stable voice, stable style.

### *class* falaw.scene.Environment(, name, description='', reference_image_url='', time_of_day='', lighting='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A reusable location/setting.

### *class* falaw.scene.Scene(, title, style='', characters=(), environments=(), shots=(), beats=(), notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The whole editable structure: cast, locations, shots, beats.

#### with_beat(beat)

Return a new Scene with `beat` replacing any existing beat with the same id.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)

### *class* falaw.scene.Shot(, id, description='', framing='medium', environment='', characters=(), camera='', notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A visual frame: framing + environment + characters in view.

Beats anchor to a Shot via `shot_id`. The Shot itself has its own
rendered output (still or short clip used as the anchor for beat
lipsync renders).

### *class* falaw.scene.Voice(, name, voice_id='', reference_audio_url='', model_id='', style_notes='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A character’s voice spec.

Three modes, choose any:

* `voice_id` — model-side voice id (e.g. ElevenLabs voice).
* `reference_audio_url` — a few seconds of audio to clone.
* `model_id` — override the default TTS model for this voice.

Always provide `name` for stable, human-readable referencing.

### falaw.scene.beat_content_hash(beat, , character=None)

Hash everything that affects how the beat *renders*.

Includes the beat’s content + the character’s identity anchors
(face image, voice spec). Style/emotion changes invalidate the
cache; pure id renames do not.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.scene.beat_id(, speaker, line, action='', index=None)

Deterministic, human-readable-ish beat id.

Pattern: `{index?}-{speaker_slug}-{content_hash}`. Index makes
chronological sort cheap; the hash makes the id stable when content
is unchanged.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### falaw.scene.scene_from_dict(d)

Inverse of asdict: reconstruct a Scene from a plain dict.

* **Return type:**
  [`Scene`](_autosummary/falaw.scene.html.md#falaw.scene.Scene)


# _autosummary/falaw.schema_export.html.md

# falaw.schema_export

Export the Python SSOT as committed JSON for the TypeScript twin (`ts/`).

falaw ships twice: as this Python package and as the npm package `falaw-client`
(`ts/`), which *plans* in the browser and *executes* through a server relay.
The two must agree on the plan and result shapes, the model catalogue and its
prices, the cost rules, the model-picking rules, the response parser, and the
canonical byte-form every hash is taken over. None of that is re-authored on
the TypeScript side: this module writes it out as JSON, the TS build generates
its types from the schemas and reads the catalogue as data, and its parity tests
replay the fixtures below and assert the same output, hash for hash.

What lands in `<out_dir>`:

- `call-plan.schema.json`, `result.schema.json`, `model-record.schema.json`
  — the dataclasses as JSON Schema (via `pydantic.TypeAdapter`), the codegen
  inputs for the Zod types.
- `models.json` — the catalogue, **byte for byte**, so the TS side can compute
  the same `models_table_version` digest a [`falaw.CostBasis`](_autosummary/falaw.html.md#falaw.CostBasis) records.
- `constants.json` — the default backend, the plan dict schema tag, the
  catalogue pricer identity, the tier order, the response-kind keys, and the
  literal vocabularies (`OutputKind`, `CacheStatus`).
- `fixtures/plans.json` — planner inputs → the `CallPlan` dict this package
  builds (cost, basis and all) and its `plan_hash`; plus multi-call plans.
- `fixtures/responses.json` — raw fal responses → the `Result` this package
  parses them into.
- `fixtures/pick_model.json`, `fixtures/cost.json`, `fixtures/canonical.json`
  — model selection, cost estimation, and the canonical blob + SHA-256 for a set
  of payloads (the byte-form parity that makes `plan_hash` agree).

`tests/test_schema_export.py` pins the committed directory to a fresh export,
so a change here that forgets `python -m falaw export-schema` fails CI on this
side, before the TS side can drift.

```pycon
>>> from falaw.schema_export import PLAN_CASES
>>> PLAN_CASES[0]["tool"]
'generate_image'
```

### Functions

| [`export_schema`](_autosummary/falaw.schema_export.html.md#falaw.schema_export.export_schema)([out_dir])   | Write the JSON contract under `out_dir`; return the paths written.   |
|-----------------------------------------------------------------------------|----------------------------------------------------------------------|

### falaw.schema_export.export_schema(out_dir=None)

Write the JSON contract under `out_dir`; return the paths written.

`out_dir` defaults to the `schema/` directory of *this checkout*. This is
repository tooling: from an installed wheel (no `pyproject.toml` beside the
package) it refuses rather than writing a stray `schema/` wherever the shell
happens to be.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]


# _autosummary/falaw.session.html.md

# falaw.session

Session: optional stateful controller over a sequence of falaw operations.

Plain function calls work without a session. Sessions exist to make multi-step
flows ergonomic — a working directory, a result history, and a journal
binding that future-you (or another agent) can read.

### Classes

| [`Session`](_autosummary/falaw.session.html.md#falaw.session.Session)([output_dir, journal, history])   | A working session for a sequence of falaw operations.   |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------|

### *class* falaw.session.Session(output_dir=<factory>, journal=<factory>, history=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A working session for a sequence of falaw operations.

```pycon
>>> import tempfile
>>> s = Session(output_dir=tempfile.mkdtemp())
>>> s.history
[]
```


# _autosummary/falaw.testing.html.md

# falaw.testing

Make a suite that uses falaw genuinely offline — the fake asset transport.

falaw content-addresses every media result ([`falaw.content`](_autosummary/falaw.content.html.md#module-falaw.content)): a fal URL is
neither unique-per-content nor durable, so it cannot be an identity, and
[`falaw.execute_plan()`](_autosummary/falaw.html.md#falaw.execute_plan) therefore **reads the bytes** behind every result. A
downstream suite that stubs the fal *response* but not the *asset transport* is
consequently resolving its made-up URLs for real — one DNS lookup and one
connection attempt per test.

Such a suite passes anyway. That is the trap. falaw deliberately degrades a
failed asset fetch to a URL-only artifact with a [`UserWarning`](https://docs.python.org/3/builtins/exceptions.html#UserWarning) rather
than raising, so an upstream hiccup cannot throw away a render fal has already
billed for — which means reaching the network is *invisible* downstream. The
costs are real all the same: the suite is slower and behaves differently
without a network; it exercises the *degraded* path rather than the content
addressing it appears to exercise; and a stub URL whose host happens to answer
**200** pulls arbitrary internet content into the cache with the test still
green.

This module is falaw’s answer, so no consumer has to write one. Three autouse
fixtures, each re-exportable into a `conftest.py` in one line:

```pycon
>>> from falaw.testing import (
...     fake_assets,
...     isolated_falaw_cache,
...     no_outbound_network,
... )  # noqa: F401
```

[`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets)
: Serves falaw’s asset fetches from memory. An unregistered URL yields
  deterministic synthetic bytes derived from the URL, so two different stub
  URLs behave like two genuinely different renders;
  [`FakeAssets.serve()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.serve) pins explicit bytes — which is how a test makes
  two *different* URLs serve the *same* bytes, the case content addressing
  exists for; [`FakeAssets.fail()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.fail) makes a URL 404, as an expired fal
  asset does, which is how a test exercises the degrade path on purpose.

[`isolated_falaw_cache()`](_autosummary/falaw.testing.html.md#falaw.testing.isolated_falaw_cache)
: Points falaw’s manifest cache, content store and url-index at a throwaway
  directory. A precondition for the fake, not a nicety: once the fetches
  *succeed*, an un-isolated run durably records synthetic test bytes under
  real-looking URLs in the developer’s own `~/.config/falaw/cache`.

[`no_outbound_network()`](_autosummary/falaw.testing.html.md#falaw.testing.no_outbound_network)
: The backstop. The other two close the holes we know about; this one
  notices the next one. Every non-loopback DNS lookup or socket connect is
  refused **and recorded**, and the recording fails the test at teardown.

**Refusing is not reporting**, and that distinction is the whole design of the
guard. A guard that only raises is invisible here, because falaw’s fetch path
is a funnel of broad `except Exception` handlers that exist for good reasons:
`_fetch_into_store` turns any `Exception` into a `FalAssetFetchError`,
which `execute` then **degrades** to a URL-only artifact with a warning
(rather than discard a render fal has already billed); `remembered_ref`
treats any `Exception` as a cache miss; `make_call_plan`’s cache probe
falls back to `"unknown"`. An ordinary-`Exception` refusal is absorbed by
all three, and the suite stays green with the regression back in place.

Hence two measures, not one. [`OutboundNetworkAttempt`](_autosummary/falaw.testing.html.md#falaw.testing.OutboundNetworkAttempt) derives from
[`BaseException`](https://docs.python.org/3/builtins/exceptions.html#BaseException) so it escapes those funnels (`execute_isolated`
deliberately re-raises a non-`Exception`, since a run-level abort is not a
call failure) — and every attempt is **recorded** anyway, because that only
covers the funnels *inside falaw*. A consumer’s own `except BaseException`
re-absorbs it, and a subprocess is out of reach entirely. The record is the
part that cannot be swallowed.

## Why not `FALAW_FETCH_ARTIFACT_BYTES=0`

It silences the network too — by turning content addressing **off**. The suite
becomes hermetic and simultaneously stops testing the feature: every
`asset_id` becomes a response digest rather than a content hash, and no test
covers the path production takes. Faking the transport keeps content addressing
under test with bytes the test controls. See
[`falaw.plan.FETCH_BYTES_ENVVAR`](_autosummary/falaw.plan.html.md#falaw.plan.FETCH_BYTES_ENVVAR).

## Opting a test back into the real world

A test carrying any marker in [`DFLT_LIVE_MARKERS`](_autosummary/falaw.testing.html.md#falaw.testing.DFLT_LIVE_MARKERS) (`live_api`) gets the
real transport and no network guard, and keeps the cache isolation. Suites with
another such marker build their own fixtures from the factories:

```pycon
>>> gated = make_fake_assets_fixture(live_markers=("live_api", "live_capture"))
```

The classes and context managers here need no pytest; only the fixtures do.

### Module Attributes

| [`DFLT_LIVE_MARKERS`](_autosummary/falaw.testing.html.md#falaw.testing.DFLT_LIVE_MARKERS)                           | Pytest markers meaning "this test is allowed to reach the real world".   |
|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| [`DFLT_SYNTHETIC_PREFIX`](_autosummary/falaw.testing.html.md#falaw.testing.DFLT_SYNTHETIC_PREFIX)                       | Prefix of the stand-in bytes an unpinned URL serves.                     |
| [`LOCAL_HOSTNAMES`](_autosummary/falaw.testing.html.md#falaw.testing.LOCAL_HOSTNAMES)                             | Hostnames that mean "this machine" without a DNS round-trip.             |
| [`NON_NETWORK_SCHEMES`](_autosummary/falaw.testing.html.md#falaw.testing.NON_NETWORK_SCHEMES)                         | URL schemes that do not leave this machine, and so are not faked.        |
| [`fake_assets`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets)(request)                        | Autouse fixture serving falaw's asset fetches from memory.               |
| [`isolated_falaw_cache`](_autosummary/falaw.testing.html.md#falaw.testing.isolated_falaw_cache)(tmp_path, monkeypatch) | Autouse fixture pointing falaw's on-disk state at a throwaway directory. |
| [`no_outbound_network`](_autosummary/falaw.testing.html.md#falaw.testing.no_outbound_network)(request)                | Autouse fixture failing any test that reaches a non-loopback address.    |

### Functions

| [`blocked_outbound_network`](_autosummary/falaw.testing.html.md#falaw.testing.blocked_outbound_network)()                        | Refuse **and record** every non-local socket use; yield the record.                                          |
|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|
| [`fake_assets`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets)(request)                              | Autouse fixture serving falaw's asset fetches from memory.                                                   |
| [`is_network_url`](_autosummary/falaw.testing.html.md#falaw.testing.is_network_url)(url)                               | True when resolving `url` would leave this machine.                                                          |
| [`isolated_falaw_cache`](_autosummary/falaw.testing.html.md#falaw.testing.isolated_falaw_cache)(tmp_path, monkeypatch)       | Autouse fixture pointing falaw's on-disk state at a throwaway directory.                                     |
| [`make_fake_assets_fixture`](_autosummary/falaw.testing.html.md#falaw.testing.make_fake_assets_fixture)(\*[, live_markers, ...]) | Build a `fake_assets` fixture with non-default settings.                                                     |
| [`make_isolated_falaw_cache_fixture`](_autosummary/falaw.testing.html.md#falaw.testing.make_isolated_falaw_cache_fixture)(\*[, autouse])  | Build an `isolated_falaw_cache` fixture.                                                                     |
| [`make_no_outbound_network_fixture`](_autosummary/falaw.testing.html.md#falaw.testing.make_no_outbound_network_fixture)(\*[, ...])       | Build a `no_outbound_network` fixture.                                                                       |
| [`no_outbound_network`](_autosummary/falaw.testing.html.md#falaw.testing.no_outbound_network)(request)                      | Autouse fixture failing any test that reaches a non-loopback address.                                        |
| [`serving_fake_assets`](_autosummary/falaw.testing.html.md#falaw.testing.serving_fake_assets)([assets])                     | Install a [`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets) as falaw's transport for the duration. |
| [`synthetic_asset_bytes`](_autosummary/falaw.testing.html.md#falaw.testing.synthetic_asset_bytes)(url, \*[, prefix])          | Deterministic stand-in bytes for a URL nothing has pinned.                                                   |

### Classes

| [`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets)(\*[, synthetic_prefix, ...])   | An in-memory `url -> bytes` transport standing in for the network.   |
|--------------------------------------------------------------------------------------------|----------------------------------------------------------------------|

### Exceptions

| [`OutboundNetworkAttempt`](_autosummary/falaw.testing.html.md#falaw.testing.OutboundNetworkAttempt)   | An offline test tried to talk to a non-local host.   |
|---------------------------------------------------------------------------|------------------------------------------------------|

### falaw.testing.DFLT_LIVE_MARKERS *= ('live_api',)*

Pytest markers meaning “this test is allowed to reach the real world”.

falaw’s own convention. A suite using another name (`live_capture`, say)
passes its own tuple to the fixture factories rather than editing this.

### falaw.testing.DFLT_SYNTHETIC_PREFIX *= 'falaw-test-asset'*

Prefix of the stand-in bytes an unpinned URL serves.

Only ever seen while debugging — which is exactly why it is worth setting per
suite (`FakeAssets(synthetic_prefix="nw-test-asset")`): a hexdump in a
failing assertion then names the suite that invented the bytes.

### *class* falaw.testing.FakeAssets(, synthetic_prefix='falaw-test-asset', fake_non_network_urls=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

An in-memory `url -> bytes` transport standing in for the network.

Install it with [`falaw.content.using_url_fetcher()`](_autosummary/falaw.content.html.md#falaw.content.using_url_fetcher) (the
[`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets) fixture does), after which every falaw entry point that
reads asset bytes is served from here.

Three behaviours, each of which a downstream suite discovered it needed:

- an **unpinned** network URL yields [`synthetic_asset_bytes()`](_autosummary/falaw.testing.html.md#falaw.testing.synthetic_asset_bytes);
- [`serve()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.serve) **pins** explicit bytes, which is how a test makes two
  *different* URLs serve the *same* bytes — the case content addressing
  exists for — or aligns falaw’s view of an asset with bytes the test also
  wrote to disk itself;
- [`fail()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.fail) makes a URL **404**, as an expired fal asset does, which is
  how a test exercises falaw’s degrade-to-URL-only path on purpose.

Every fetch is **recorded** in `fetched`, and that is not a
convenience. falaw degrades a failed fetch to a URL-only artifact with a
warning rather than raising, so a fake that only *refused* an unexpected
URL would be swallowed and the test would pass. Assert on what was fetched,
not on an exception:

```pycon
>>> from lacing import ArtifactStore
>>> from falaw import content_ref_for_url
>>> from falaw.content import using_url_fetcher
>>> store, assets = ArtifactStore.in_memory(), FakeAssets()
>>> shared = assets.serve("https://fal.media/one.png", b"identical bytes")
>>> _ = assets.serve("https://fal.media/two.png", shared)
>>> with using_url_fetcher(assets.chunks):
...     one = content_ref_for_url("https://fal.media/one.png", store=store)
...     two = content_ref_for_url("https://fal.media/two.png", store=store)
>>> one.content_hash == two.content_hash  # two URLs, one content address
True
>>> assets.fetched
['https://fal.media/one.png', 'https://fal.media/two.png']
```

#### chunks(url, , chunk_size=65536)

Yield `url`’s bytes — the [`falaw.content.UrlFetcher`](_autosummary/falaw.content.html.md#falaw.content.UrlFetcher) to install.

* **Raises:**
  [**HTTPError**](https://docs.python.org/3/library/urllib.error.html#urllib.error.HTTPError) – `url` was marked with [`fail()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.fail).
* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]

#### conditional_fetch(url, validators, , chunk_size=65536)

Answer a revalidation — the capability falaw#23 asks a transport for.

Present so a downstream suite exercises the **cheap** path rather than
the fallback. Without it falaw can never confirm a mutable URL is
unchanged, so it re-fetches on every call: correct, but it would mean no
test in the ecosystem ever covers the `304` branch, and any suite
asserting “did not refetch” for a non-fal URL would fail for a reason
that has nothing to do with what it is testing.

Deferred to [`chunks()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.chunks) for a URL this fake does not handle, so a
real `file://` still reaches the real filesystem.

#### etag_for(url)

The ETag this fake serves for `url` — a digest of its current bytes.

A well-behaved origin’s ETag changes exactly when the body does, so
deriving it from the served bytes is the most faithful fake available:
[`serve()`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets.serve)-ing different bytes at the same URL flips it, and serving
the same bytes twice does not.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### fail(url)

Make `url` 404, the way an expired fal asset does.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### handles(url)

Whether this transport answers for `url` rather than deferring.

A pinned URL always. An unpinned one only when it is a network URL (or
`fake_non_network_urls` is set) — see [`NON_NETWORK_SCHEMES`](_autosummary/falaw.testing.html.md#falaw.testing.NON_NETWORK_SCHEMES).

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

#### revalidated *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

URLs answered with a cheap “unchanged” — no body transferred.

Separate from `fetched` because the two are the *point* of
falaw#23 and a suite must be able to tell them apart. Without this, “we
revalidated cheaply” and “we never asked at all” are indistinguishable,
and every cheap-path assertion in the suite is equally satisfied by the
un-fixed code that simply trusted its index.

#### serve(url, data)

Make `url` serve exactly `data`. Returns `data`, for chaining.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

#### synthetic(url)

The bytes an unpinned `url` serves.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

### falaw.testing.LOCAL_HOSTNAMES *= frozenset({'', 'ip6-localhost', 'ip6-loopback', 'localhost', 'localhost.localdomain'})*

Hostnames that mean “this machine” without a DNS round-trip.

### falaw.testing.NON_NETWORK_SCHEMES *= frozenset({'', 'file'})*

URL schemes that do not leave this machine, and so are not faked.

`file://` is a documented falaw input — downstream packages hand it locally
rendered media that way — so faking it would replace a real mp4 with synthetic
bytes and break the ffmpeg that reads it. Such URLs fall through to the real
fetcher unless a test pins them explicitly.

### *exception* falaw.testing.OutboundNetworkAttempt

Bases: [`BaseException`](https://docs.python.org/3/builtins/exceptions.html#BaseException)

An offline test tried to talk to a non-local host.

Derived from [`BaseException`](https://docs.python.org/3/builtins/exceptions.html#BaseException), not [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception), on purpose:
the fetch path is a funnel of `except Exception` handlers that turn a
failed fetch into a degraded-but-successful result (falaw’s URL-only
artifact fallback is the main one), so anything catchable would be caught.

That still is not enough on its own. It escapes *falaw’s* funnels, not a
consumer’s own `except BaseException` — and not a subprocess, which opens
its own sockets where nothing here has reach. Which is why every attempt is
also **recorded**; see [`blocked_outbound_network()`](_autosummary/falaw.testing.html.md#falaw.testing.blocked_outbound_network).

### falaw.testing.blocked_outbound_network()

Refuse **and record** every non-local socket use; yield the record.

The yielded list is the point. Raising alone is not a guard here: falaw
swallows a failed fetch by design, and a refusal that reaches a consumer’s
own broad `except` — or happens in a subprocess — vanishes without trace.
A caller asserts on the list afterwards; [`no_outbound_network()`](_autosummary/falaw.testing.html.md#falaw.testing.no_outbound_network) fails
the test at teardown when it is non-empty.

A test that *means* to provoke an attempt drains the list
(`attempts.clear()`) instead of failing.

Blind spot worth knowing: a subprocess opens its own sockets in its own
process, where this has no reach. ffmpeg is the one that matters.

(`.invalid` is reserved by RFC 2606 and can never resolve, so even a
broken guard cannot turn this example into real traffic.)

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]

```pycon
>>> import socket
>>> with blocked_outbound_network() as attempts:
...     try:
...         socket.getaddrinfo("nowhere.invalid", 443)
...     except OutboundNetworkAttempt:
...         pass
>>> attempts
["a DNS lookup for 'nowhere.invalid'"]
```

### falaw.testing.fake_assets(request)

Autouse fixture serving falaw’s asset fetches from memory.

Re-export it into a `conftest.py` and the whole suite is covered:

```default
from falaw.testing import fake_assets  # noqa: F401
```

Yields the [`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets) in force, so a test can pin bytes
(`fake_assets.serve(url, data)`), 404 a URL (`fake_assets.fail(url)`) or
assert on what was fetched (`fake_assets.fetched`). Yields `None` for a
test marked `live_api`, which gets the real transport.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets)]]

### falaw.testing.is_network_url(url)

True when resolving `url` would leave this machine.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_network_url("https://fal.media/x.png")
True
>>> is_network_url("file:///tmp/rendered.mp4"), is_network_url("/tmp/x.mp4")
(False, False)
```

### falaw.testing.isolated_falaw_cache(tmp_path, monkeypatch)

Autouse fixture pointing falaw’s on-disk state at a throwaway directory.

falaw’s manifest cache, content store and url-index all hang off
`$FALAW_CACHE_DIR` / `$FALAW_DATA_DIR`. Without this a run writes into the
developer’s real cache and inherits the previous run’s `url -> content hash`
index — and once [`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets) makes the fetches *succeed*, it durably
records synthetic test bytes under real-looking URLs there. Yields the root of
the throwaway directory.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### falaw.testing.make_fake_assets_fixture(, live_markers=('live_api',), synthetic_prefix='falaw-test-asset', fake_non_network_urls=False, autouse=True)

Build a `fake_assets` fixture with non-default settings.

[`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets) is `make_fake_assets_fixture()`. Use this when the
suite’s live-marker names differ, or to label the synthetic bytes with the
suite’s own name:

```pycon
>>> fake_assets = make_fake_assets_fixture(
...     live_markers=("live_api", "live_capture"),
...     synthetic_prefix="reelee-test-asset",
... )
```

### falaw.testing.make_isolated_falaw_cache_fixture(, autouse=True)

Build an `isolated_falaw_cache` fixture. See [`isolated_falaw_cache()`](_autosummary/falaw.testing.html.md#falaw.testing.isolated_falaw_cache).

### falaw.testing.make_no_outbound_network_fixture(, live_markers=('live_api',), autouse=True)

Build a `no_outbound_network` fixture. See [`no_outbound_network()`](_autosummary/falaw.testing.html.md#falaw.testing.no_outbound_network).

### falaw.testing.no_outbound_network(request)

Autouse fixture failing any test that reaches a non-loopback address.

The backstop behind [`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets): that one closes the holes we know
about, this one notices the next. Yields the list of recorded attempts, so a
test that means to provoke one can drain it (`no_outbound_network.clear()`)
instead of failing at teardown. Inert for a test marked `live_api`.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`list`](https://docs.python.org/3/builtins/stdtypes.html#list)]

### falaw.testing.serving_fake_assets(assets=None)

Install a [`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets) as falaw’s transport for the duration.

The pytest-free core of [`fake_assets()`](_autosummary/falaw.testing.html.md#falaw.testing.fake_assets), for a caller driving falaw
outside a pytest session (a script, a notebook, another test runner).

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/typing.html#typing.Iterator)[[`FakeAssets`](_autosummary/falaw.testing.html.md#falaw.testing.FakeAssets)]

```pycon
>>> from falaw import materialize_asset
>>> with serving_fake_assets() as assets:
...     _ = assets.serve("https://fal.media/x.png", b"pinned")
...     path = materialize_asset("https://fal.media/x.png")
>>> with open(path, "rb") as f:
...     f.read()
b'pinned'
```

### falaw.testing.synthetic_asset_bytes(url, , prefix='falaw-test-asset')

Deterministic stand-in bytes for a URL nothing has pinned.

Derived from the URL, so two different stub URLs behave like two genuinely
different renders — and the same stub URL hashes the same way on every run,
which is what lets a test assert on a content hash at all.

* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)

```pycon
>>> synthetic_asset_bytes("https://fal.media/x.png")
b'falaw-test-asset::https://fal.media/x.png'
>>> a = synthetic_asset_bytes("https://fal.media/one.png")
>>> b = synthetic_asset_bytes("https://fal.media/two.png")
>>> a == b
False
```


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-14 12:58 UTC** from commit <a href="https://github.com/reeleehq/falaw/commit/14eff54bf51e10c1d8ae695c1b7d4da3872edbc1"><code>14eff54</code></a> on branch <code>main</code>, for **falaw 0.0.62** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.0.62) is behind the latest release on PyPI (0.0.63): `pip install falaw` gives newer code than these docs describe.

## Source

|                     |                                                                                                                                                       |
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/reeleehq/falaw/commit/14eff54bf51e10c1d8ae695c1b7d4da3872edbc1"><code>14eff54bf51e10c1d8ae695c1b7d4da3872edbc1</code></a> |
| Branch              | <code>main</code>                                                                                                                                     |
| Tags at this commit | none                                                                                                                                                  |
| Working tree        | clean                                                                                                                                                 |
| Remote              | <code>https://github.com/reeleehq/falaw</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>reeleehq/falaw</code>                                                                |
| Run          | <a href="https://github.com/reeleehq/falaw/actions/runs/38567699368">38567699368</a>       |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>14eff54bf51e10c1d8ae695c1b7d4da3872edbc1</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#804a00</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/falaw/0.0.63/">0.0.63</a>, newer than the documented version (0.0.62).

## Reproduce

```bash
git clone https://github.com/reeleehq/falaw && cd falaw
git checkout 14eff54bf51e10c1d8ae695c1b7d4da3872edbc1
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`falaw` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `falaw`

Generate and manage AI media (images, video, audio) via fal.ai. Use this skill whenever the user wants to generate, edit, upscale, or compose media, or asks about fal.ai or fal-client. Also use it for *directorial* workflows: the user defines a Scene as data (characters, beats, shots) and falaw renders it with caching, so a single edit re-renders only what changed.

```bash
gh skill install thorwhalen/falaw falaw --agent claude-code
```

Source: [`falaw/data/skills/falaw`](https://github.com/thorwhalen/falaw/tree/HEAD/falaw/data/skills/falaw) (bundled with the pip package).

### `falaw`

Generate and manage AI media (images, video, audio) via fal.ai. Use this skill whenever the user wants to generate, edit, upscale, or compose media, or asks about fal.ai or fal-client. Also use it for *directorial* workflows: the user defines a Scene as data (characters, beats, shots) and falaw renders it with caching, so a single edit re-renders only what changed.

Source: [`.claude/skills/falaw`](https://github.com/thorwhalen/falaw/tree/HEAD/.claude/skills/falaw).

The bundled skills are also on disk after `pip install falaw`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/thorwhalen/falaw/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/falaw/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`falaw.md`](https://thorwhalen.github.io/falaw/falaw.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/falaw/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`falaw`](_autosummary/falaw.html.md#module-falaw)   | falaw: agent-friendly Python facade over fal.ai for AI media generation.   |
|-----------------------------------------------------------------------|----------------------------------------------------------------------------|


