Imperal Docs
SDK Reference

@chat.function reference

Every kwarg, every behavior, every action_type — full surface for the LLM-callable handler decorator

@chat.function is the most-used decorator in the SDK. It registers a handler that the LLM can call by name with typed parameters. Every registered function appears as a tool in the LLM's tool-use schema, gates through action authorization, and writes an audit row on completion.

Where it lives

@chat.function is a method on a ChatExtension instance — not a global decorator and not a method on Extension directly. The pattern used in every production extension is:

from imperal_sdk import Extension
from imperal_sdk.chat import ChatExtension, ActionResult
from pydantic import BaseModel, Field

ext = Extension(
    "my-app",
    version="1.0.0",
    display_name="My App",
    description="My App — AI-powered tool for managing resources.",
    icon="icon.svg",
    actions_explicit=True,
)

chat = ChatExtension(
    ext=ext,
    tool_name="my-app",
    description="My App assistant — ask anything about your resources.",
)

chat is then available at module scope. Handler files import it from app.py:

# handlers_resources.py
from app import chat, ActionResult

Functions are registered in handler files by decorating async functions:

@chat.function(
    "function_name",
    description="What this function does for the user.",
    action_type="read",
)
async def fn_function_name(ctx, params: MyParams) -> ActionResult:
    ...

Signature

def function(
    self,
    name: str,
    description: str,
    params: dict | None = None,
    action_type: str = "read",
    event: str = "",
    event_schema: type | None = None,
    chain_callable: bool | None = None,
    effects: list[str] | None = None,
    id_projection: str | None = None,
    background: bool = False,        # v4.2.13+ — LONGRUN-V1 sugar
    long_running: bool = False,      # v4.2.13+ — federal cap 180s/1800s
) -> Callable:
    ...

The decorator is called with keyword or positional arguments and returns a decorator that wraps the handler function. The handler function is stored unchanged in _functions[name].


Kwargs reference

name

Required. The function identifier used in the LLM's tool-use calls. This name appears in the manifest, in audit logs, and in chain dispatch calls.

Prop

Type

Rules:

  • Must be unique within the ChatExtension instance.
  • Use verb_noun style (create_note, list_tasks, delete_saved_query).
  • For compound multi-word targets where the verb-prefix heuristic would yield the wrong field, also set id_projection (see below).

description

Required. The human-readable description shown to the LLM when it decides which tool to call.

Prop

Type

Best practices:

  • Always name the entity explicitly: "Create a new notes folder." not "Create a new folder." — the LLM has many extensions and needs to know you mean notes folders.
  • Include pagination behavior in read tools: "Returns up to limit rows. If has_more is true, call again with offset=offset+limit."
  • Do not include "I will" or "You should" phrasing — this is metadata, not a prompt.
  • V16 enforces ≥20 characters at ERROR severity. "Create note" (11 chars) fails validation.

Production example from notes/handlers_notes.py:

"List notes (paginated). Returns up to `limit` rows per call. If `has_more` is true, call again with `offset=offset+limit` to fetch the next page."

params

Optional explicit parameter schema. If omitted, the SDK auto-derives it from the Pydantic BaseModel annotation on the handler's second parameter.

Prop

Type

Preferred pattern (auto-derive):

from pydantic import BaseModel, Field

class CreateNoteParams(BaseModel):
    title: str = Field(description="Note title")
    content_text: str = Field("", description="Note body in plain text")
    tags: list[str] = Field(default_factory=list, description="Tag labels to apply")
    folder_id: str | None = Field(None, description="Folder UUID or name, or null for root")

@chat.function(
    "create_note",
    description="Create a new note with title, content, tags, and optional folder.",
    action_type="write",
    chain_callable=True,
    effects=["create:note"],
    event="created",
)
async def fn_create_note(ctx, params: CreateNoteParams) -> ActionResult:
    ...

Critical: module-scope model requirement (V17). The Pydantic model MUST be defined at module scope, not inside the function body. The SDK detects the model via func.__globals__; a function-local class is invisible to this lookup and triggers a V17 ERROR.

# CORRECT: module scope
class CreateNoteParams(BaseModel):
    title: str

@chat.function(...)
async def fn_create_note(ctx, params: CreateNoteParams) -> ActionResult:
    ...

# WRONG: function-local — V17 ERROR, model not detected
@chat.function(...)
async def fn_create_note(ctx, params) -> ActionResult:
    class CreateNoteParams(BaseModel):  # invisible to SDK
        title: str
    ...

No-arg functions (V17): Functions that take no user parameters (e.g. list_trash, empty_trash) still require a Pydantic param. Use a sentinel:

class NoParams(BaseModel):
    pass

@chat.function(
    "list_trash",
    description="List all notes currently in the trash.",
    action_type="read",
)
async def fn_list_trash(ctx, params: NoParams) -> ActionResult:
    ...

Field descriptions matter. The SDK serializes Field(description=...) directly into the tool-use schema. The LLM reads these descriptions when filling in parameter values. Short or missing descriptions produce poor argument quality.


action_type

Controls the authorization gate, confirmation behavior, and audit classification for this function.

Prop

Type

Semantic table:

ValueSide effectsKAV confirmationAudit rowchain_callable default
"read"NoneNeverYes (read)True (v4.2.10+)
"write"Creates or modifies dataWhen user has confirmations enabledYes (write)True
"destructive"Deletes, permanently removes, or cannot be undoneAlways — unconditionalYes (destructive)True

KAV (Pre-Authorized Action Execution) is the 2-step confirmation flow. When a write function is called and the user has confirmations enabled, the web-kernel presents a confirmation card before executing. destructive functions always present a confirmation card regardless of user settings.

Choose action_type carefully:

  • "read" — fetches data, no mutation: list_notes, get_task, search_notes, query_database
  • "write" — creates, updates, moves, or sends: create_note, update_task, send_email, move_note
  • "destructive" — deletes, permanently removes, empties, archives with no recovery: delete_note, permanent_delete_note, empty_trash, delete_task

When in doubt between "write" and "destructive": if the action cannot easily be undone, use "destructive".


event

The event suffix emitted after a successful write or destructive execution.

Prop

Type

V10 warns (and will become ERROR in v5.0.0) when a write or destructive function has no event. Events are consumed by:

  • Other extensions that subscribe via @ext.on_event
  • The audit ledger
  • SSE streams for real-time panel refresh

Production examples from notes/handlers_notes.py: event="created", event="updated", event="deleted", event="permanently_deleted", event="bulk_deleted".

The web-kernel automatically prepends the extension's app_id, so event="created" becomes "notes.created" in the stream.


event_schema

Optional Pydantic model for typed event data validation.

Prop

Type

Most extensions leave this None and pass raw dict in ActionResult.success(data=...). Set it when you publish structured events that other extensions consume via @ext.on_event.


chain_callable

Controls whether the chain orchestrator can call this function as a typed step in a multi-extension chain.

Prop

Type

Default behavior: When chain_callable=None (omitted), the SDK sets it to True for every action_type since v4.2.10. Before v4.2.10 the default for reads was False — meaning read tools were typed-callable in chains only when authors set the flag explicitly.

Why this matters: In a multi-step chain (e.g. "find my pending tasks and email them to my manager"), the web-kernel needs to call each step with the exact structured data from the previous step. With chain_callable=True, it issues a deterministic typed app/func(args) dispatch — Pydantic-validated, no wrapper-LLM round inside your extension. With chain_callable=False, the web-kernel falls back to wrapper-LLM routing, which can paraphrase parameters and lose structured-data fidelity between steps.

Read intents in chains (v4.2.10+): the default flip means typed @chat.function reads such as list_notes, search_*, get_* now type-dispatch in chains as first-step data providers — no extra setup needed. Reads in conversational turns (open questions like "what's in my inbox?") continue to route via the wrapper-LLM chat path; the classifier distinguishes the two.

When to set chain_callable=False explicitly: for catch-all conversational handlers (a case_chat-style tool that takes the raw user message and decides what to do internally). Such handlers depend on the wrapper-LLM seeing the user's prose, so direct typed dispatch would bypass their design.

V19 enforces chain_callable=True on every write/destructive function at ERROR severity when actions_explicit=True.


effects

Side-effect declarations for the audit ledger and chain narrator.

Prop

Type

Format: "verb:resource" strings where verb is one of create, update, delete, trash, send, archive, move, and resource is the entity name.

Production examples:

  • ["create:note"] — creates a single note
  • ["update:note"] — updates note fields
  • ["trash:note"] — moves to trash (soft delete)
  • ["delete:note"] — permanent deletion
  • ["trash:note", "delete:note"] — bulk delete with optional permanent flag
  • ["create:task"] — creates a task
  • ["update:task"] — updates task fields, marks complete, etc.
  • ["delete:task"] — deletes a task permanently

The chain narrator reads effects to describe what changed in multi-step responses. The audit ledger stores them verbatim. V20 warns when absent on write/destructive — will be ERROR in SDK v5.0.0.


id_projection

Declares which Pydantic params field carries the resolved target ID when this function runs as a chain step.

Prop

Type

When to use it: The web-kernel uses a verb-prefix heuristic to locate the target ID field. For delete_note, it derives note_id. For complete_task, it derives task_id. This works for simple verb_noun names.

For compound names, the heuristic fails:

  • delete_notes_from_folder → heuristic yields notes_from_folder_id (wrong) → set id_projection="folder_id"
  • toggle_checklist_item → heuristic yields checklist_item_id (wrong) → set id_projection="task_id"
  • create_task → when the chain needs to know which project to create in → set id_projection="project_id"
  • permanent_delete_note → heuristic yields permanent_delete_note_id (wrong) → set id_projection="note_id"

Production examples from notes/handlers_notes.py and tasks/handlers_crud.py:

@chat.function(
    "permanent_delete_note",
    action_type="destructive",
    chain_callable=True,
    id_projection="note_id",   # heuristic would fail for "permanent_delete"
    effects=["delete:note"],
    event="permanently_deleted",
    description="Permanently delete a note. Cannot be undone.",
)
async def fn_permanent_delete_note(ctx, params: NoteIdParams) -> ActionResult:
    ...

@chat.function(
    "create_task",
    action_type="write",
    chain_callable=True,
    id_projection="project_id",  # chain step needs project context
    effects=["create:task"],
    event="task.created",
    description="Create a new task in a project. Returns task_id + full task details.",
)
async def create_task(ctx, params: CreateTaskParams) -> ActionResult:
    ...

When id_projection is set, the chain executor reads params[id_projection] to thread the resolved ID from a previous step. This closes the class of bugs where a chain step receives a placeholder instead of the actual resource ID from an earlier step.


background and long_running — LONGRUN-V1 sugar (v4.2.13+)

Declarative wrapper over ctx.background_task(). When background=True, the SDK auto-wraps the function call in ctx.background_task() instead of running the handler synchronously.

Prop

Type

Usage:

@chat.function(
    "refine_output",
    description="Refine the given text via AI completion.",
    action_type="write",
    event="text_refined",
    background=True,        # ← auto-wrap in ctx.background_task
    long_running=False,     # default cap 180s; True → 1800s
)
async def refine_output(ctx, params: RefineParams) -> ActionResult:
    # The body runs DETACHED via ctx.background_task() under the hood.
    # No inner _work() coro, no manual task_id wiring.
    await ctx.progress(50, "Generating with AI")
    resp = await ctx.http.post(openai_url, json={...}, timeout=120)
    return ActionResult.success(
        summary="Refined output ready!",
        data={"text": resp.body["text"]},
    )

What the user sees in chat:

  1. Immediate — auto-generated ack: "Started 'refine_output' in background — the result will be sent to chat when it finishes." Carries the task_id in data.
  2. Later — your handler's returned ActionResult.success(...) rendered as a fresh bot turn when the work completes.

Sugar vs. explicit ctx.background_task(coro) — when to use which:

  • Sugar (background=True) — your handler body is entirely the long work; the auto-ack summary is acceptable.
  • Explicit (ctx.background_task(coro)) — you need a custom acknowledgement summary, want to choose background_task() conditionally at runtime, or run mixed sync + background work in the same handler.

Federal contract: same five I-LONGRUN-* invariants apply (see Federal contract). Note in particular I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT — the handler MUST return ActionResult.


action_type in depth

"read" — side-effect-free queries

Use for any function that only fetches data. The web-kernel skips the confirmation gate entirely. There is no audit write-action row (a read access row is still written).

chain_callable defaults to True for reads since v4.2.10 — typed read handlers (list_*, search_*, get_*) participate in chains as first-step data providers without any extra setup. Set it explicitly to False only for catch-all conversational handlers that rely on the wrapper-LLM seeing the raw user prose.

@chat.function(
    "list_notes",
    action_type="read",
    description=(
        "List notes (paginated). Returns up to `limit` rows per call. "
        "If `has_more` is true, call again with `offset=offset+limit`."
    ),
)
async def fn_list_notes(ctx, params: ListNotesParams) -> ActionResult:
    ...

"write" — mutations with optional confirmation

Use for create, update, move, send, or any reversible mutation. The confirmation gate fires only when the user has enabled confirmations in their settings.

V10 warns when event is absent. V19 requires chain_callable=True when actions_explicit=True.

@chat.function(
    "create_note",
    action_type="write",
    chain_callable=True,
    effects=["create:note"],
    event="created",
    description="Create a new note with title, content, tags, and optional folder.",
)
async def fn_create_note(ctx, params: CreateNoteParams) -> ActionResult:
    ...

"destructive" — always-confirmed permanent actions

Use for permanent deletion, irreversible state changes, or any action the user should be forced to confirm. The confirmation card is always shown regardless of user settings. This is the strongest signal — do not use it for reversible operations.

@chat.function(
    "delete_task",
    action_type="destructive",
    chain_callable=True,
    effects=["delete:task"],
    event="task.deleted",
    description="Permanently delete a task. Cannot be undone.",
)
async def delete_task(ctx, params: DeleteTaskParams) -> ActionResult:
    ...

Pydantic params

The Pydantic params model is the primary contract between the LLM and your handler.

Model definition rules

  1. Module scope (V17): Define the model at module scope, not inside the function.
  2. Typed fields: Every field must have a type annotation. str, int, bool, list[str], str | None, and nested models are all supported.
  3. Field descriptions: Use Field(description="...") on every field. The SDK serializes these into the LLM schema — they directly affect argument quality.
  4. Required vs optional: Fields without a default are required in the LLM call. Fields with Field(None, ...) or Field(default_factory=list, ...) are optional.
  5. Validators: Pydantic validators work normally. @field_validator, @model_validator, etc. are all supported.
from pydantic import BaseModel, Field, field_validator

class UpdateNoteParams(BaseModel):
    note_id: str = Field(description="UUID4 of the note to update")
    title: str | None = Field(None, description="New title, or null to keep current")
    content_text: str | None = Field(None, description="New body text, or null to keep current")
    tags: list[str] | None = Field(None, description="New tag list, or null to keep current")
    is_pinned: bool | None = Field(None, description="Pin or unpin the note, or null to keep current")

    @field_validator("note_id")
    @classmethod
    def validate_uuid(cls, v: str) -> str:
        import re
        if not re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v, re.I):
            raise ValueError("note_id must be a UUID4")
        return v

Pydantic feedback loop (v4.1.0+)

When the LLM passes arguments that fail Pydantic validation, the SDK automatically retries the LLM call up to 2 times (I-PYDANTIC-RETRY-BUDGET) with structured feedback:

  • The validation error is formatted into human-readable prose.
  • The feedback is appended to the conversation as a structured correction.
  • The LLM retries with the corrected parameters.
  • After 2 failed retries, the error surfaces to the user.

This means well-described Pydantic models with clear error messages dramatically improve the user experience. The validation error message is sent to the LLM as feedback — make it actionable.

Federal invariants (I-PYDANTIC-RETRY-BUDGET/SCOPE/FEEDBACK-STRUCTURED/FC-SINGLE-APPEND/WIRE-FROZEN): The retry budget is hard-capped at 2 regardless of configuration. The scope of retries is limited to Pydantic validation errors only — not network errors or handler exceptions. The feedback message is structured prose, not a raw Pydantic traceback.


Return types

All @chat.function handlers must return ActionResult. The return type annotation -> ActionResult is required (V18 ERROR).

ActionResult.success

ActionResult.success(
    data: dict | BaseModel = {},
    summary: str = "",
    *,
    ui: UINode | None = None,
    refresh_panels: list[str] | None = None,
) -> ActionResult

summary is always displayed to the user in chat. Make it conversational and informative:

  • "Note created: My Shopping List"
  • "3 tasks completed."
  • "Email sent to alex@example.com."

data is the structured payload passed to downstream chain steps and stored in the audit ledger. For chain steps, the data dict is what the next step reads via prior_step_results.

refresh_panels controls which panels refresh after the action:

  • None (default): all panels refresh.
  • []: no panels refresh.
  • ["sidebar"], ["sidebar", "editor"]: specific named panels.

ui attaches an inline UINode rendered directly in the chat response (not in a panel). Use for confirmation receipts, summaries, or quick-view cards.

return ActionResult.success(
    data={"note_id": note["id"], "title": note["title"]},
    summary=f"Note created: {note['title']}",
    refresh_panels=["sidebar"],
)

ActionResult.error

ActionResult.error(
    error: str,
    retryable: bool = False,
) -> ActionResult

error is shown to the user. Keep it user-friendly — no raw exception strings, no internal paths, no class names. The SDK's Magic UX layer (I-MAGIC-UX-1/2) intercepts raw exceptions but your explicit error messages bypass it.

retryable=True shows a retry button in the UI.

return ActionResult.error(
    "Folder not found. Use list_folders() to see available folders.",
    retryable=False,
)

Examples

Example 1: Read tool

A paginated list with no side effects. Uses action_type="read", no event, no effects, no chain_callable.

from imperal_sdk import Extension
from imperal_sdk.chat import ChatExtension, ActionResult
from pydantic import BaseModel, Field

ext = Extension(
    "notes",
    version="1.0.0",
    display_name="Notes",
    description="Notes extension — create, organize, and search your notes with AI.",
    icon="icon.svg",
    actions_explicit=True,
)

chat = ChatExtension(ext, tool_name="notes", description="Notes assistant")


class ListNotesParams(BaseModel):
    folder_id: str | None = Field(None, description="Filter by folder UUID or name, or null for all notes")
    limit: int = Field(20, description="Number of notes per page (max 50)")
    offset: int = Field(0, description="Page offset for pagination")


@chat.function(  # type: ignore[attr-defined]
    "list_notes",
    action_type="read",
    description=(
        "List notes (paginated). Returns up to `limit` rows per call. "
        "If `has_more` is true, call again with `offset=offset+limit`."
    ),
)
async def fn_list_notes(ctx, params: ListNotesParams) -> ActionResult:
    notes = await ctx.store.query(
        "notes",
        where={"folder_id": params.folder_id} if params.folder_id else None,
        limit=params.limit,
    )
    return ActionResult.success(
        data={
            "notes": [{"note_id": n.id, "title": n.data["title"]} for n in notes.data],
            "has_more": notes.has_more,
            "next_offset": params.offset + len(notes.data) if notes.has_more else None,
        },
        summary=f"{len(notes.data)} note(s) found.",
    )

Example 2: Write tool

Creates a resource. Uses action_type="write", chain_callable=True, effects, and event.

from imperal_sdk import Extension
from imperal_sdk.chat import ChatExtension, ActionResult
from pydantic import BaseModel, Field

ext = Extension(
    "notes",
    version="1.0.0",
    display_name="Notes",
    description="Notes extension — create, organize, and search your notes with AI.",
    icon="icon.svg",
    actions_explicit=True,
)

chat = ChatExtension(ext, tool_name="notes", description="Notes assistant")


class CreateNoteParams(BaseModel):
    title: str = Field(description="Note title")
    content_text: str = Field("", description="Note body in plain text")
    folder_id: str | None = Field(None, description="Folder UUID or name, or null for root")


@chat.function(  # type: ignore[attr-defined]
    "create_note",
    action_type="write",
    chain_callable=True,
    effects=["create:note"],
    event="created",
    description="Create a new note with title, content, and optional folder.",
)
async def fn_create_note(ctx, params: CreateNoteParams) -> ActionResult:
    if not params.title.strip() and not params.content_text.strip():
        return ActionResult.error(
            "Note must have a title or content. Pass title and/or content_text."
        )
    doc = await ctx.store.create("notes", {
        "title": params.title.strip(),
        "content_text": params.content_text,
        "folder_id": params.folder_id,
        "user_id": ctx.user.imperal_id,
    })
    return ActionResult.success(
        data={"note_id": doc.id, "title": doc.data["title"]},
        summary=f"Note created: {doc.data['title']}",
        refresh_panels=["sidebar"],
    )

Example 3: Destructive tool with id_projection

Permanently deletes a resource. Always shows a confirmation card. Uses id_projection because the function name has a compound prefix.

from imperal_sdk import Extension
from imperal_sdk.chat import ChatExtension, ActionResult
from pydantic import BaseModel, Field

ext = Extension(
    "notes",
    version="1.0.0",
    display_name="Notes",
    description="Notes extension — create, organize, and search your notes with AI.",
    icon="icon.svg",
    actions_explicit=True,
)

chat = ChatExtension(ext, tool_name="notes", description="Notes assistant")


class DeleteNotesFromFolderParams(BaseModel):
    folder_id: str = Field(description="Folder UUID or name — all notes in this folder will be deleted")
    permanent: bool = Field(False, description="True to permanently delete; False to move to trash")


@chat.function(  # type: ignore[attr-defined]
    "delete_notes_from_folder",
    action_type="destructive",
    chain_callable=True,
    id_projection="folder_id",  # verb-prefix heuristic would yield wrong field name
    effects=["trash:note", "delete:note"],
    event="bulk_deleted",
    description=(
        "Delete ALL notes in a folder (bulk). By default moves them to trash; "
        "pass permanent=true to permanently delete. Cannot be undone when permanent=true."
    ),
)
async def fn_delete_notes_from_folder(
    ctx, params: DeleteNotesFromFolderParams
) -> ActionResult:
    notes = await ctx.store.query("notes", where={"folder_id": params.folder_id})
    if not notes.data:
        return ActionResult.success(
            data={"deleted_count": 0, "folder_id": params.folder_id},
            summary="No notes in folder — nothing to delete.",
        )
    deleted = 0
    for note in notes.data:
        await ctx.store.delete("notes", note.id)
        deleted += 1
    action = "permanently deleted" if params.permanent else "moved to trash"
    return ActionResult.success(
        data={"deleted_count": deleted, "folder_id": params.folder_id, "permanent": params.permanent},
        summary=f"{deleted} note(s) {action}.",
        refresh_panels=["sidebar"],
    )

Common pitfalls

V14–V22+V24+V31 validator failures

ValidatorWhat failsFix
V16description shorter than 20 charsExpand to ≥20 chars; name the entity
V17No Pydantic BaseModel paramDefine a BaseModel class at module scope; use it as the second param
V17Model defined inside function bodyMove class definition to module scope
V18Missing -> ActionResult return annotationAdd return annotation
V19write/destructive without chain_callable=TrueAdd chain_callable=True
V20write/destructive without effectsAdd effects=["verb:resource"]
V10write/destructive without eventAdd event="past_tense_verb"
V24ctx.skeleton.get() in a handler bodyRemove; use ctx.store, ctx.http, or ctx.cache instead

Pydantic retry budget exhausted

If the LLM consistently fails Pydantic validation after 2 retries, the error surfaces to the user. Common causes:

  • Field descriptions are absent or too terse for the LLM to understand the expected format.
  • Required fields are ambiguous — the LLM cannot infer what to pass.
  • Complex nested models without clear field documentation.

Fix: improve Field(description=...) on every required field, especially IDs and enums.

action_type mismatch

Using "write" for an action that is irreversible leads to a poor user experience — the confirmation card may not appear when the user expects it. Audit the semantics of every function:

  • "Move to trash" where recovery is available = "write"
  • "Delete permanently" = "destructive"
  • "Archive" where the user can unarchive = "write"
  • "Purge all history" with no recovery = "destructive"

Missing id_projection for compound tool names

If a chain step passes a placeholder instead of the resolved ID, check whether the tool name has a compound prefix that confuses the verb-prefix heuristic. Add id_projection="field_name" to declare the correct field explicitly.

ctx.skeleton access in handlers (V24)

ctx.skeleton.get() inside a @chat.function handler raises SkeletonAccessForbidden. Skeleton data is classifier input only — it is not a data source for runtime handlers. Use ctx.store, ctx.http, or ctx.cache to fetch live data inside handlers.

ChatExtension(model=...) deprecated

The model= constructor parameter is deprecated since SDK 3.3.0 and emits a once-per-process warning. LLM model resolution is handled by the web-kernel's context injection layer. Remove model= from all ChatExtension(...) calls.


Cross-references

On this page