Skip to content

Releases: bzdvdn/reactifact

0.15.2

Choose a tag to compare

@bzdvdn bzdvdn released this 11 Oct 07:44

Added

  • Produce-level tracing (AgentSpan.produces). A trace span now records
    which Produce(s) ran within the agent for that event, with how many effect
    operations each authored and its latency — so a multi-produce agent (e.g. one
    that shapes an answer and records a side effect) is no longer a black box in
    the trace. Captured via a per-task contextvar in Agent.execute, persisted in
    the SQLite/Postgres trace stores (auto-migrated column) and shown in the
    dashboard as a Consume → Produce flow on each span (trigger artifact →
    produce chips with +N authored operations, no-op produces dimmed); the
    agent header aggregates it. The external sinks carry it too: LangfuseTracer
    emits a child observation per produce (with its latency, so Langfuse's
    waterfall shows each Consume → Produce step) plus the
    langfuse.observation.metadata.produces list, and OTLPTracer emits a
    reactifact.produces attribute (Name(ops) list). A produce that raises is
    still recorded — with its partial operations — on the error span, so the
    failing step is visible instead of silently missing. Produce names are readable:
    Produce.name uses the class name, the @produce function's name, or — for a
    bare Produce(Model) used only to widen an agent's allowed Create types —
    Produce[Model] (an explicit name= still wins), so traces no longer show a
    wall of generic Produce.
  • AgentSpan.event_artifact_type — the artifact type whose change triggered
    the agent (its matched Consume), e.g. DraftAnswer vs FinalResponse for an
    agent that consumes both. event_type alone (created/updated) can't tell them
    apart. Persisted in both trace stores (auto-migrated) and shown on each span;
    Langfuse gets langfuse.observation.metadata.event_artifact_type and OTLP gets
    reactifact.event_artifact_type (both omitted when empty, so no blank key).

Fixed

  • Dashboard disclosure carets now point down when expanded. The ▸ on
    agent, span and LLM-call rows was styled with details[open]>.caret, but the
    caret lives inside <summary>, so the selector never matched and it stayed
    ▸; it now rotates to ▾ for every expanded row.

Full changelog: https://github.com/bzdvdn/reactifact/blob/v0.15.2/CHANGELOG.md

0.15.1

Choose a tag to compare

@bzdvdn bzdvdn released this 07 Oct 06:50

Added

  • create_chat_router(request_factory=…) accepts an async factory. The
    factory may now be async def (or otherwise return an awaitable); its result
    is awaited before the turn runs, e.g. to look up the authenticated user with a
    query. The sync form is unchanged.

Fixed

  • Runtime.astream left a pending queue.get()/done.wait() task. In the
    event/done wait loop the losing future was cancelled but never awaited (and
    done.wait() was not cancelled at all when an event won), so closing the
    stream early — a dropped SSE client, an app shutdown — left a task pending and
    printed Task was destroyed but it is pending!. Both futures are now
    cancelled and awaited every iteration, and the runner task is settled in the
    generator's finally.
  • Runtime.astream waited on a cancelled runner without bound. A misbehaving
    agent that swallows CancelledError could hang the caller's disconnect or
    shutdown path; the wait is now bounded (_CANCEL_GRACE_SECONDS).
  • A2A message/stream leaked its run on client disconnect. The server's
    stream generator create_taskd the run but had no finally, so a dropped
    client left the run executing (burning tokens) and pending at loop shutdown
    (Task was destroyed but it is pending!). The generator now cancels and
    awaits the run when closed early, and the in-process task store is bounded
    (_MAX_TASKS, evicting the oldest terminal task).
  • ChatAssistant.stream leaked per-turn resources when opening failed. A
    callable resources= builds a fresh instance per turn; if the store failed to
    open the session (or the runtime could not be built) it was never closed.
    _open/stream now close it on that failure path.
  • EventHub subscriber queues were unbounded. A subscriber that never drains
    could grow memory without limit; the queue is now bounded and drops the oldest
    status hint when full.

Full changelog: https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md

0.15.0

Choose a tag to compare

@bzdvdn bzdvdn released this 06 Oct 15:08

Added

  • create_chat_router(request_factory=…). The chat router can now map the
    incoming FastAPI Request into the per-turn request= mapping that
    ChatAssistant.stream accepts (e.g. the authenticated user for
    ACL/identity-aware runs). Without it the turn runs with request=None, as
    before — existing callers are unaffected.

Full changelog: https://github.com/bzdvdn/reactifact/blob/master/CHANGELOG.md

0.14.2

Choose a tag to compare

@bzdvdn bzdvdn released this 05 Oct 16:32

0.14.2

Fixed

  • Trace dashboard respects its mount prefix. create_trace_router's pages
    (/traces, /traces/{id}, /sessions) embedded root-absolute URLs
    (/api/traces, /traces, /sessions, /traces/assets/app.css), so a router
    mounted with include_router(prefix=…) (or run behind an ASGI root_path)
    served a UI whose links and fetch calls escaped the prefix. The templates
    now use a __BASE__ placeholder filled per request from the mount prefix, so
    the dashboard works at any mount point (and is unchanged at the root).

0.14.1

Choose a tag to compare

@bzdvdn bzdvdn released this 01 Oct 16:00

0.14.1

Added

  • ChatAssistant(dispatcher=...). The canonical chat layer now passes
    dispatcher=/on_dispatch_error= through to the Runtime it builds for each
    turn, so an agent that records effects.act(...) intents has them delivered
    from a chat turn (once per stable id, after the commit) instead of left
    pending with only the no-dispatcher warning. quick.chat_agent(...) gains
    the same two arguments (reactifact.runtime.DispatchErrorHandler is the
    callback type).

0.14.0

Choose a tag to compare

@bzdvdn bzdvdn released this 30 Sep 23:00

0.14.0

Added

  • A2A (Agent2Agent) client and server. reactifact.a2a speaks the A2A
    JSON-RPC binding (v0.3 shape) in both directions, with no new dependency (the
    client uses httpx; the server, FastAPI from the web extra, is imported
    lazily). Client: A2AClient (Agent Card, message/send, message/stream,
    tasks/get, tasks/cancel) and A2AAgentTool/a2a_tool — a reactifact
    Tool so an LLMAgent can delegate to a remote agent. remote_agent(...) /
    A2ARemoteProduce — a remote agent as a first-class scheduled node (the
    analogue of LangGraph's A2ARemoteGraph): it consumes a local artifact, calls
    message/send, produces a local artifact, and maps a remote input-required
    to a local PendingQuestion (resume continues the same remote task). Server:
    create_a2a_router(agents, ...) builds an Agent Card (skills from each
    agent's produces) and a JSON-RPC endpoint where a Task is one conversation
    — a message/send seeds via create_message, runs the agents to a fixpoint,
    and replies via reply; a paused HITL run maps to input-required and the
    next message with the same taskId resumes it; message/stream emits the
    working task and a status update per context.announce() progress event over
    SSE. Docs in docs/en|ru/a2a.md. Verified against the official a2a-sdk
    client
    (the SDK resolves our Agent Card and gets a completed task from
    message/send); a2a-sdk is a test-only dependency (its own a2a group),
    the runtime stays on httpx + FastAPI. The examples/a2a app demonstrates it
    end to end (serve, call directly, schedule as a node, HITL) with no LLM or
    network.
  • Outbox for outbound side effects. A produce no longer performs external
    I/O (a notification, a webhook, a deploy) directly:
    effects.act(kind=..., key=..., payload=...) records a PendingAction
    intent under a stable id (action:{key}), and Runtime(dispatcher=...)
    performs every committed-but-undispatched action after the commit — once
    per stable id, recording dispatched (or failed, then re-raising) as state.
    flush_pending_actions() drains it without a generation (e.g. after
    merge()); the runtime keeps a turn alive until the outbox is empty. Replay
    reconstructs the record without running the runtime, so it never re-sends; a
    retried produce re-derives the same id and creates no second intent; two
    branches that reach the same action share the id and merge to one — dispatch
    bookkeeping (status/dispatched_at/error) is excluded from the three-way
    merge signature, so a mixed dispatched/pending pair converges instead of
    raising MergeConflict — and the dispatched side wins whichever branch is
    the merge target, so a merge never re-sends an already-sent action — while a
    changed kind/key/payload still conflicts. PendingAction and Context.pending_actions()/
    mark_dispatched()/mark_failed()/retry_action() are exported;
    replay_summary and python -m reactifact replay report
    dispatched/pending/failed action counts. Delivery remains at-least-once
    (a crash between the send and the dispatched commit re-runs it), so the
    dispatcher must honor idempotency_key — stated, not hidden. The offline
    examples/outbox demo walks seven cases: commit→dispatch, re-derivation,
    same-generation duplicate, merged branches, replay, failure→retry, and an
    app-owned retry wrapper around the dispatcher. A failed dispatch bumps
    PendingAction.attempts and fires Runtime(on_dispatch_error=action, exc)
    before re-raising; retry_action resets attempts. The framework runs no
    background relay
    — the next arun() or an explicit
    flush_pending_actions() drains the outbox, and retry/backoff policy stays
    with the application (deliberately not a workflow engine).
  • Correlated structured logging. reactifact.logging (exported as
    configure_logging/get_logger) turns on structured logs under the
    reactifact.* namespace — silent until configured (a NullHandler by
    default, never basicConfig), human-readable or json=True, with
    configure_logging(level=..., stream=...). Every record carries the turn's
    correlation fields (run_id/session_id/generation/agent, plus the
    request's request_id) so a line ties back to the exact run/agent; app code
    adds fields with bind(...). The runtime emits run started/run finished
    and action dispatched at INFO, generation/agent completion at DEBUG,
    and failing agents/dispatches at WARNING. Logs are metadata only — no
    artifact data or prompt contents. examples/starter_app shows it in a real
    lifespan (LOG_LEVEL=…, LOG_JSON=1) alongside a GET /api/ready readiness
    probe backed by active_runs().
  • Hard turn deadline, graceful shutdown, in-flight registry.
    Budget.max_seconds is now a hard per-turn deadline: the runtime cancels the
    generation in flight (a hung LLM call is actually stopped) instead of only
    checking between generations, leaves the cancelled trigger batch unconsumed
    for a resume, and ends with RunOutcome.BUDGET_TIME_EXCEEDED.
    Runtime.request_stop() stops at the next generation boundary
    (RunOutcome.STOPPED; the current generation still commits), and
    Runtime.ashutdown(timeout=…) waits for the turn, force-cancelling after the
    timeout. Shared RuntimeResources are closed by the host app, never by the
    runtime. reactifact.runtime.active_runs() / cancel_run(run_id) expose the
    process's in-flight turns read-only (readiness/ops; in-process only), and
    Runtime.in_flight reports whether it is running.

Changed

  • Budget.max_seconds is enforced hard (behavior-visible, non-breaking): the
    runtime now cancels the generation in flight when the deadline passes, instead
    of only checking between generations, and ends the turn with
    RunOutcome.BUDGET_TIME_EXCEEDED. A run that previously overshot a hung LLM
    call now stops on time. See release.md.
  • Logging stays silent by default (a library NullHandler); nothing is
    emitted until configure_logging() is called.

Docs

  • patterns.md (EN + RU) gains an Outbox: external side effects recipe,
    and effects.md/durability.md/replay.md/api.md (EN + RU) document
    effects.act/PendingAction/Runtime(dispatcher=...); the
    reactifact/reactifact-agents/reactifact-observability skills are
    updated too. durability.md (EN + RU) also gains an Outbox in production
    checklist (idempotent dispatcher, periodic drain, app-owned retry,
    observability), stating that the framework ships no relay by design. The example count is now stated once (twenty-three) across
    examples.md, index.md and quickstart.md (EN + RU) and the constitution's
    demo list, replacing the disagreeing subset counts (14/15/16/17).
  • durability.md (EN + RU) states plainly that artifact types do not need
    registering — the persisted id is the qualified name by default, resolved by
    import on load. It lists the only three reasons to register_type (rename/
    move, aliases, migration), notes the TYPE_ID classvar shortcut, and calls
    out the one caveat: a model that is not importable in a new process
    (__main__, a notebook, <locals>, pydantic.create_model) needs an
    explicit type_id.

0.13.0

Choose a tag to compare

@bzdvdn bzdvdn released this 28 Sep 12:24

0.13.0

Added

  • Online evaluation over live traces. reactifact.eval.online scores
    finished runs sampled from a TraceStore with the same Evaluators as
    offline evaluation, writing the verdicts back as tags (pass → eval, fail →
    eval:failed) for the dashboard, plus metrics
    (reactifact_online_eval_runs_total, reactifact_online_eval_score).
    OnlineEvalConfig controls sampling (sample_rate/strategy/seed),
    filters, tags and reference_fn; online_evaluate(store, config) runs one
    batch and OnlineEvaluator schedules it (run_once, run_forever,
    on_report). Because a trace is truncated, the source decides what is
    scoreable: trace_source() scores the RunTrace itself (typed path via
    trajectory_match, output_present, no_errors; unusable metrics are
    skipped, never a false zero), while context_source(run_fn) rehydrates the
    full Context for exact text/core_metrics/judge scoring. Ships a FastAPI
    router (create_online_eval_router: run/report/summary, lazy import) and the
    CLI python -m reactifact eval <traces.db>. Docs in docs/en|ru/eval.md and
    the reactifact-eval skill. The devops example demonstrates it:
    examples/devops/online_eval.py scores served runs with trace-level
    routed/routed_to evaluators and web.py mounts
    create_online_eval_router next to the trace dashboard.
  • Trust & safety: guardrails, authorization and quota. Three opt-in,
    library-level layers on RuntimeResources, acting on typed artifacts and
    resource access (deterministic, testable) rather than around model messages.
    reactifact.guardrails.GuardrailPolicy runs ordered checks on every
    agent-produced Create/Update: PIIGuardrail (redacts via any Redactor),
    InjectionGuardrail, DenyListGuardrail, SizeGuardrail, the generic
    PatternGuardrail, with on_violation="block" (raises GuardrailViolation)
    or "flag" (records reactifact_guardrail_triggered_total). reactifact. authz adds a Principal + capability-based PermissionPolicy, enforced by
    the runtime on run/create/update/delete, by the tool loop on
    execute, and available to produces via resources.require_authorized(...);
    denial raises AuthorizationError. reactifact.quota adds cross-turn,
    per-principal usage limits (Quota/QuotaTracker/QuotaLLM) — checked at
    turn start (RunOutcome.QUOTA_EXCEEDED) and counted through the turn, with
    cache hits free and a pluggable QuotaStore (in-process by default). All off
    by default; docs in docs/en/safety.md and the reactifact skill's
    references/trust-and-safety.md. The devops example demonstrates it:
    examples/devops/guardrails.py defines a custom guardrail (a destructive
    production change must cite a CHG- ticket) plus secret redaction, wired
    onto RuntimeResources and screening the raw input in web.py; run
    examples/devops/safety.py for an end-to-end, no-LLM demo.
  • Agent skills for building with reactifact. The package ships official
    Agent Skills in reactifact/skills/<name>/SKILL.md (version-matched, since
    they live in the package), installed into a project with
    python -m reactifact skills install [--target agents|claude|both] (also
    skills list / skills show). Eight skills cover the paradigm end to end:
    reactifact (mental model, invariants, working loop, routing, mistakes),
    reactifact-agents (produce/consume/effects/provenance), reactifact-llm
    (structured output, LLMAgent/HITL, tools, budgets), reactifact-rag
    (sources, fan_out_sources, recipes), reactifact-testing,
    reactifact-eval, reactifact-observability, and
    reactifact-from-langchain. Each has a workflow plus references/ for
    lookup; every runnable code block is executed in CI by tests/test_skills.py
    (# not-run marks illustrative fragments), so the examples cannot rot. A
    root AGENTS.md points library contributors at the skills and the repo
    commands.
  • Prometheus metrics, dependency-free. reactifact.metrics adds a tiny
    in-process collector (Metrics) and a MetricsTracer that fills it from the
    tracing hook, plus create_metrics_router serving GET /metrics (the web
    extra). Counters: run/budget outcomes, agent runs, LLM calls/tokens/errors
    and (with MetricsTracer(metrics, pricer=…)) cost; histograms: agent/LLM
    latency. Metrics.render() emits the Prometheus text exposition directly —
    no prometheus_client dependency — and MetricsTracer composes with the
    existing sinks (tracer=[TraceStore(…), MetricsTracer(metrics)]). Span
    reads/writes/edges also give domain-adjacent counters
    (reactifact_artifacts_written_total{type,op},
    reactifact_artifacts_read_total{type},
    reactifact_relations_total{relation}), and RuntimeResources.metrics is a
    no-op-until-configured sink so a produce can record app-specific signals
    (call.context.resources.metrics.increment("route_total", route=…)).
  • Dataset evaluation and LLM-as-judge. reactifact.eval grows from the
    deterministic metric core (§56) into a full offline-evaluation layer, with no
    hosted store. Dataset/Example hold inputs + optional
    reference_outputs/metadata (content-hashed version, .json/.jsonl
    loading); evaluate(dataset, target, evaluators, summary=…) runs a target
    over every example — the target may return a Context, a
    reactifact.testing.ScenarioResult (read via .context/.trace), an
    outputs mapping or a RunResult — and scores it. An Evaluator is any
    callable EvalInput -> Feedback | bool | float | {key: score} | list | None
    (None skips, never a silent zero); from_metric wraps the scoring metrics,
    trajectory_match(mode, steps=…) matches a run's typed path
    (strict/unordered/subset/superset over agents/events/reads/
    writes). llm_judge (plus judge_correctness/judge_relevance/
    judge_faithfulness) turns any LLMProvider into a judge with tolerant JSON
    parsing, continuous scores, choices snapping and reference-based grading —
    composable with CachingLLM/budget/metrics and faked with FakeLLM.
    EvalReport gains aggregate()/passed()/assert_passed() (the CI gate,
    EvalFailure), summary evaluators (summary_pass_rate, summary_mean) and
    assert_eval. reactifact.eval is now a package; the existing
    EvalCase/run_suite metrics API is unchanged.
  • Provider resilience: exact LLM cache and failover. reactifact.cache. CachingLLM keys every complete() by the whole request (model, messages,
    temperature, …) and answers a repeat without calling the provider — no
    tokens, no latency; an optional KVCache over any KVBackend persists it.
    A hit is marked (raw={"reactifact_cache": "hit"}) so BudgetLLM does not
    charge it. Deliberately not semantic (an approximate match would answer a
    different question) and stream() is uncached. reactifact.routing. RouterLLM tries providers in order and falls back on failure
    (should_fallback= — e.g. retryable_only — and on_fallback=); only
    complete() fails over. Both are drop-in provider wrappers, composable:
    resources.llm = CachingLLM(RouterLLM([openrouter_llm(), groq_llm()])).
  • Stable artifact type identity (schema evolution). reactifact.types
    decouples the persisted type string from Python's module layout:
    register_type(Model, type_id=…, aliases=[…], migrate=…) makes a model
    rename/move — or a field rename — no longer break loading old sessions,
    commits, events or recordings. type_id_of / resolve / migrate_payload;
    the default id stays the qualified name and pre-registry payloads keep
    loading, so it is fully backward compatible. Applies to Artifact.data_type,
    Create/Update.data_type and Event.artifact_type.
  • Multi-turn conversations on typed artifacts. reactifact.recipes. conversation adds Conversation — generic over your message model (any
    model with role/text; ConversationMessage is a ready default) — with
    create_turn/add_message/transcript/turn_count/conversation_state,
    so apps stop hand-rolling a chat log and a prompt builder. For the common
    case of separate question/answer types, Transcript + MessageSpec
    render several typed turns (with per-type role and an optional renderer for a
    non-text assistant payload) into one Message list, leaving the artifacts
    untouched. ChatMemory on
    ChatAssistant wires bounded memory: keep (prune the raw thread via
    WindowPruner), summarize+summary_type (a growing digest via
    RollingDigestSummarizer), and compact_commits (Context.compact — bounds
    history without touching artifacts, safe for correlated/typed messages). The
    repair/devops/knowledge examples use compact_commits to bound
    long-lived sessions. Docs: a new Multi-turn & chat page
    (EN + RU) mapping the LangGraph pieces to reactifact, plus a minimal offline
    examples/chat demo of the single-model recipe.
  • Context compaction. Context.compact(keep_commits=…, keep_versions=…)
    bounds a long-lived session: it collapses commits older than the last
    keep_commits into a baseline snapshot (and optionally trims each artifact's
    retained version history). The absolute version/head_id are preserved, so
    context_hash, commit version numbers and pending-trigger resume are
    unchanged. It is irreversible — checkout/diff below the baseline raise
    (Context.compacted_at reports it) — and it does not evict artifacts (the
    memory recipes still own that). Artifact gained an absolute version
    counter decoupled from len(history) so trimming history never changes
    context_hash; the baseline is persisted in Context.to_dict/from_dict,
    copied by clone/branch, and CommitLog replays correctly across it.
  • Token and cost budgets. Budget gains max_tokens (total prompt +
    completion over...
Read more

v0.12.0 — configurable artifact columns + ScenarioReport

Choose a tag to compare

@bzdvdn bzdvdn released this 24 Sep 22:48

The tracing release: the traces table now takes configurable, artifact-derived
columns
— create_trace_router(store, columns=[TraceColumn(...)]) pulls a
field out of a run's artifacts (agent/type/direction/field/scope), shown in the
list (with a column chooser) and as a Fields card on the run page; scope="session"
resolves as of the row's run, so a chat's question and latest answer show across
HITL-split runs. Also ScenarioReport (token/latency/tool totals via
result.report / scenario.report). Backwards-compatible.

Added

  • Configurable artifact columns in the traces table. create_trace_router( store, columns=[TraceColumn(...)]) adds columns pulled from a run's
    artifacts: each TraceColumn names an agent, artifact type (class or
    name), direction (read/write/any) and a dotted field (a list step takes
    its first element unless an explicit index is given). Values are computed
    server-side per run and returned in each /api/traces item as fields
    (GET /api/columns lists the labels); the dashboard renders them with a
    per-browser column chooser. scope="session" resolves a column as of the
    row's run (every run up to it, chronological; read order reversed so the
    triggering artifact is the most recent and index=-1 means "latest so far").
    A chat is many runs and a HITL clarify splits one exchange into two (ask
    turn, then resume), so Question is the message that started the current
    exchange (shown on the resume row too) and Answer the latest reply so far.
    The run page also shows the configured columns as a Fields card
    (/api/traces/{id} carries fields), so the question — and a HITL clarify,
    via a PendingQuestion column — are visible when opening a run, not just in
    the list.
    RuntimeResources(trace_truncate=…) now controls how much artifact/response
    JSON a trace keeps (None = no clip), so deep fields stay resolvable. The
    devops example shows the question, the pending clarify question and the
    final answer; repair (which now mounts the trace dashboard) shows the
    question, the flow stage, the pending approval and the final answer.
  • ScenarioResult.report / Scenario.report return a ScenarioReport: token
    totals (prompt_tokens/completion_tokens/total_tokens), llm_calls,
    tool_calls, spans, agents, errors and outcomes for a turn — or
    aggregated across every turn of a multi-turn scenario() — with to_dict()
    and a readable render(). result.llm gained prompt_tokens /
    completion_tokens alongside tokens.

v0.11.2 — PostgreSQLKVBackend self-heals a poisoned connection

Choose a tag to compare

@bzdvdn bzdvdn released this 23 Sep 22:48

Patch release. PostgreSQLKVBackend could be wedged for good by a single
cancelled query: a client dropping a streaming turn cancels the session save
mid-flight, leaving the shared libpq connection busy/aborted, and every later
session read/write failed. Each statement now drops the connection and retries
once on any psycopg error, so the store self-heals. No API change.

Fixed

  • PostgreSQLKVBackend could be wedged for good by a single cancelled query:
    a client dropping a streaming turn cancels the session save mid-flight, which
    leaves the shared libpq connection busy/aborted, and every later session
    read/write then failed (InFailedSqlTransaction, or "another command is
    already in progress" — surfacing as a 500 on DELETE /api/runs/{id}). Each
    statement now drops the connection and retries once on any psycopg error,
    so the store self-heals instead of staying poisoned.

v0.11.1 — OpenAPI schema fix for the trace export route

Choose a tag to compare

@bzdvdn bzdvdn released this 23 Sep 22:26

Patch release. The trace router's /api/traces/export handler broke OpenAPI
schema generation — its -> JSONResponse return annotation is an unresolvable
forward ref, so /openapi.json and /docs raised PydanticUserError. The
route now declares response_model=None. No behavior change.

Fixed

  • The trace router's /api/traces/export handler broke OpenAPI schema
    generation: its -> JSONResponse return annotation is an unresolvable forward
    ref (the module uses from __future__ import annotations and imports fastapi
    lazily inside create_trace_router), so /openapi.json and /docs raised
    PydanticUserError. The route now declares response_model=None.