Skip to content

API Reference

Generated in part from source docstrings via mkdocstrings.

Registration

expose_as_tool

Register a viewset's actions as tools.

Parameters:

Name Type Description Default
actions Sequence[str]

Which actions to expose. Standard actions ("list", "create", "retrieve", "update", "partial_update", "destroy") are recognized automatically; custom @action-decorated method names are also supported, introspected via their .mapping/.detail attributes.

('list', 'retrieve', 'create', 'update', 'partial_update', 'destroy')
name_prefix str | None

Base name used to build each tool's name, as f"{name_prefix}{NAME_SEPARATOR}{action}". Defaults to a snake_case version of the viewset's class name with any trailing ViewSet/APIView/View suffix stripped (e.g. ArticleViewSet -> article).

None
descriptions Mapping[str, str] | None

Optional per-action description overrides. Actions without an override get a generated default description.

None
serializer_classes Mapping[str, type[BaseSerializer[Any]]] | None

Optional per-action serializer class overrides. Defaults to the viewset's serializer_class attribute for every action.

None
examples Mapping[str, Sequence[Mapping[str, Any]]] | None

Optional per-action example argument payloads, surfaced in the generated schema.

None
lookup_field str | None

URL kwarg name for detail actions. Defaults to the viewset's lookup_url_kwarg or lookup_field attribute, or "pk".

None
registry ToolRegistry | None

The :class:ToolRegistry to register into. Defaults to :data:default_registry.

None

Returns:

Type Description
Callable[[_V], _V]

A class decorator that registers the tools as a side effect and

Callable[[_V], _V]

returns the viewset class unchanged.

Raises:

Type Description
ToolRegistrationError

If a resulting tool name collides with an already-registered tool, or if an action name isn't a standard action and isn't found as an @action-decorated method on the viewset.

Example

.. code-block:: python

@expose_as_tool(actions=["list", "retrieve"])
class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
Source code in src/drf_llm_gateway/registry.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def expose_as_tool(
    *,
    actions: Sequence[str] = ("list", "retrieve", "create", "update", "partial_update", "destroy"),
    name_prefix: str | None = None,
    descriptions: Mapping[str, str] | None = None,
    serializer_classes: Mapping[str, type[BaseSerializer[Any]]] | None = None,
    examples: Mapping[str, Sequence[Mapping[str, Any]]] | None = None,
    lookup_field: str | None = None,
    registry: ToolRegistry | None = None,
) -> Callable[[_V], _V]:
    """Register a viewset's actions as tools.

    Args:
        actions: Which actions to expose. Standard actions
            (``"list"``, ``"create"``, ``"retrieve"``, ``"update"``,
            ``"partial_update"``, ``"destroy"``) are recognized
            automatically; custom ``@action``-decorated method names are
            also supported, introspected via their ``.mapping``/``.detail``
            attributes.
        name_prefix: Base name used to build each tool's name, as
            ``f"{name_prefix}{NAME_SEPARATOR}{action}"``. Defaults to a
            snake_case version of the viewset's class name with any
            trailing ``ViewSet``/``APIView``/``View`` suffix stripped
            (e.g. ``ArticleViewSet`` -> ``article``).
        descriptions: Optional per-action description overrides. Actions
            without an override get a generated default description.
        serializer_classes: Optional per-action serializer class
            overrides. Defaults to the viewset's ``serializer_class``
            attribute for every action.
        examples: Optional per-action example argument payloads, surfaced
            in the generated schema.
        lookup_field: URL kwarg name for detail actions. Defaults to the
            viewset's ``lookup_url_kwarg`` or ``lookup_field`` attribute,
            or ``"pk"``.
        registry: The :class:`ToolRegistry` to register into. Defaults to
            :data:`default_registry`.

    Returns:
        A class decorator that registers the tools as a side effect and
        returns the viewset class unchanged.

    Raises:
        drf_llm_gateway.exceptions.ToolRegistrationError: If a resulting
            tool name collides with an already-registered tool, or if an
            action name isn't a standard action and isn't found as an
            ``@action``-decorated method on the viewset.

    Example:
        .. code-block:: python

            @expose_as_tool(actions=["list", "retrieve"])
            class ArticleViewSet(viewsets.ReadOnlyModelViewSet):
                queryset = Article.objects.all()
                serializer_class = ArticleSerializer
    """
    target_registry = registry if registry is not None else default_registry
    descriptions = descriptions or {}
    serializer_classes = serializer_classes or {}
    examples = examples or {}

    def decorator(viewset_class: _V) -> _V:
        base_name = name_prefix or viewset_base_name(viewset_class)
        separator = get_setting("NAME_SEPARATOR")
        resolved_lookup_field = (
            lookup_field
            or getattr(viewset_class, "lookup_url_kwarg", None)
            or getattr(viewset_class, "lookup_field", None)
            or "pk"
        )
        for action_name in actions:
            http_method, detail = _resolve_action(viewset_class, action_name)
            tool = ToolDefinition(
                name=f"{base_name}{separator}{action_name}",
                description=descriptions.get(action_name)
                or _default_description(viewset_class, action_name),
                viewset_class=viewset_class,
                action=action_name,
                http_method=http_method,
                detail=detail,
                lookup_field=resolved_lookup_field,
                serializer_class=serializer_classes.get(
                    action_name, getattr(viewset_class, "serializer_class", None)
                ),
                permission_classes=tuple(getattr(viewset_class, "permission_classes", ())),
                authentication_classes=tuple(getattr(viewset_class, "authentication_classes", ())),
                examples=tuple(examples.get(action_name, ())),
            )
            target_registry.register(tool)
        return viewset_class

    return decorator

ToolRegistry

An ordered collection of :class:ToolDefinition objects.

Most projects only interact with the module-level :data:default_registry (via :func:expose_as_tool), but a custom registry is useful for isolating tool sets in tests or for exposing different tool sets to different agent identities.

Source code in src/drf_llm_gateway/registry.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class ToolRegistry:
    """An ordered collection of :class:`ToolDefinition` objects.

    Most projects only interact with the module-level
    :data:`default_registry` (via :func:`expose_as_tool`), but a custom
    registry is useful for isolating tool sets in tests or for exposing
    different tool sets to different agent identities.
    """

    def __init__(self) -> None:
        self._tools: dict[str, ToolDefinition] = {}

    def register(self, tool: ToolDefinition) -> None:
        """Add a tool definition to the registry.

        Args:
            tool: The tool to register.

        Raises:
            drf_llm_gateway.exceptions.ToolRegistrationError: If a tool
                with the same name is already registered.
        """
        if tool.name in self._tools:
            raise ToolRegistrationError(
                f"A tool named {tool.name!r} is already registered "
                f"(existing: {self._tools[tool.name].viewset_class.__name__}, "
                f"new: {tool.viewset_class.__name__})."
            )
        self._tools[tool.name] = tool

    def unregister(self, name: str) -> None:
        """Remove a tool definition by name, if present.

        Args:
            name: The tool name to remove. A no-op if not registered.
        """
        self._tools.pop(name, None)

    def get(self, name: str) -> ToolDefinition | None:
        """Look up a tool definition by name.

        Args:
            name: The tool name.

        Returns:
            The matching :class:`ToolDefinition`, or ``None``.
        """
        return self._tools.get(name)

    def __iter__(self) -> Iterator[ToolDefinition]:
        return iter(self._tools.values())

    def __len__(self) -> int:
        return len(self._tools)

    def __contains__(self, name: object) -> bool:
        return name in self._tools

    def clear(self) -> None:
        """Remove every registered tool. Primarily useful in tests."""
        self._tools.clear()

clear()

Remove every registered tool. Primarily useful in tests.

Source code in src/drf_llm_gateway/registry.py
211
212
213
def clear(self) -> None:
    """Remove every registered tool. Primarily useful in tests."""
    self._tools.clear()

get(name)

Look up a tool definition by name.

Parameters:

Name Type Description Default
name str

The tool name.

required

Returns:

Type Description
ToolDefinition | None

The matching :class:ToolDefinition, or None.

Source code in src/drf_llm_gateway/registry.py
191
192
193
194
195
196
197
198
199
200
def get(self, name: str) -> ToolDefinition | None:
    """Look up a tool definition by name.

    Args:
        name: The tool name.

    Returns:
        The matching :class:`ToolDefinition`, or ``None``.
    """
    return self._tools.get(name)

register(tool)

Add a tool definition to the registry.

Parameters:

Name Type Description Default
tool ToolDefinition

The tool to register.

required

Raises:

Type Description
ToolRegistrationError

If a tool with the same name is already registered.

Source code in src/drf_llm_gateway/registry.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def register(self, tool: ToolDefinition) -> None:
    """Add a tool definition to the registry.

    Args:
        tool: The tool to register.

    Raises:
        drf_llm_gateway.exceptions.ToolRegistrationError: If a tool
            with the same name is already registered.
    """
    if tool.name in self._tools:
        raise ToolRegistrationError(
            f"A tool named {tool.name!r} is already registered "
            f"(existing: {self._tools[tool.name].viewset_class.__name__}, "
            f"new: {tool.viewset_class.__name__})."
        )
    self._tools[tool.name] = tool

unregister(name)

Remove a tool definition by name, if present.

Parameters:

Name Type Description Default
name str

The tool name to remove. A no-op if not registered.

required
Source code in src/drf_llm_gateway/registry.py
183
184
185
186
187
188
189
def unregister(self, name: str) -> None:
    """Remove a tool definition by name, if present.

    Args:
        name: The tool name to remove. A no-op if not registered.
    """
    self._tools.pop(name, None)

ToolDefinition

A single callable tool derived from one viewset action.

Attributes:

Name Type Description
name str

The unique tool name (see :func:expose_as_tool).

description str

Human-readable description shown to the LLM.

viewset_class type

The DRF viewset class this tool dispatches to.

action str

The action name ("list", "retrieve", a custom @action name, ...).

http_method str

The HTTP method used to dispatch this action.

detail bool

Whether this action operates on a single object (True) or a collection (False).

lookup_field str

The URL kwarg name identifying the target object for detail actions (ignored for non-detail actions).

serializer_class type[BaseSerializer[Any]] | None

The serializer class describing this action's request body, or None for actions with no body (list, retrieve, destroy).

permission_classes tuple[type[BasePermission], ...]

Permission classes enforced by :func:~drf_llm_gateway.executor.execute_tool.

authentication_classes tuple[type[BaseAuthentication], ...]

Authentication classes used to resolve the calling identity in :func:~drf_llm_gateway.executor.execute_tool.

examples tuple[Mapping[str, Any], ...]

Example argument payloads, surfaced in generated schemas when :ref:INCLUDE_EXAMPLES_IN_SCHEMA <settings-include-examples-in-schema> is enabled.

version str

Developer-supplied semantic version string (defaults to "1.0.0"). Independent of :attr:schema_hash, which tracks actual schema changes automatically.

Source code in src/drf_llm_gateway/registry.py
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@dataclass(frozen=True, slots=True)
class ToolDefinition:
    """A single callable tool derived from one viewset action.

    Attributes:
        name: The unique tool name (see :func:`expose_as_tool`).
        description: Human-readable description shown to the LLM.
        viewset_class: The DRF viewset class this tool dispatches to.
        action: The action name (``"list"``, ``"retrieve"``, a custom
            ``@action`` name, ...).
        http_method: The HTTP method used to dispatch this action.
        detail: Whether this action operates on a single object (``True``)
            or a collection (``False``).
        lookup_field: The URL kwarg name identifying the target object for
            detail actions (ignored for non-detail actions).
        serializer_class: The serializer class describing this action's
            request body, or ``None`` for actions with no body (``list``,
            ``retrieve``, ``destroy``).
        permission_classes: Permission classes enforced by
            :func:`~drf_llm_gateway.executor.execute_tool`.
        authentication_classes: Authentication classes used to resolve the
            calling identity in :func:`~drf_llm_gateway.executor.execute_tool`.
        examples: Example argument payloads, surfaced in generated schemas
            when :ref:`INCLUDE_EXAMPLES_IN_SCHEMA <settings-include-examples-in-schema>`
            is enabled.
        version: Developer-supplied semantic version string (defaults to
            ``"1.0.0"``). Independent of :attr:`schema_hash`, which tracks
            *actual* schema changes automatically.
    """

    name: str
    description: str
    viewset_class: type
    action: str
    http_method: str
    detail: bool
    lookup_field: str
    serializer_class: type[BaseSerializer[Any]] | None
    permission_classes: tuple[type[BasePermission], ...] = ()
    authentication_classes: tuple[type[BaseAuthentication], ...] = ()
    examples: tuple[Mapping[str, Any], ...] = ()
    version: str = "1.0.0"

    def input_json_schema(self) -> JSONSchema:
        """Build the JSON Schema for this tool's callable arguments.

        Returns:
            An object schema combining the detail lookup parameter (for
            detail actions) with the serializer's writable fields (for
            actions with a request body). ``partial_update`` never marks
            fields as required, matching HTTP ``PATCH`` semantics.
        """
        properties: dict[str, JSONSchema] = {}
        required: list[str] = []

        if self.detail:
            properties[self.lookup_field] = self._lookup_field_schema()
            required.append(self.lookup_field)

        if self.serializer_class is not None and self.action in (
            "create",
            "update",
            "partial_update",
        ):
            body_schema = serializer_to_json_schema(self.serializer_class, mode="input")
            properties.update(body_schema.get("properties", {}))
            if self.action != "partial_update":
                required.extend(body_schema.get("required", []))

        schema: JSONSchema = {"type": "object", "properties": properties}
        if required:
            schema["required"] = sorted(set(required))
        if self.examples and get_setting("INCLUDE_EXAMPLES_IN_SCHEMA"):
            schema["examples"] = list(self.examples)
        return schema

    @property
    def schema_hash(self) -> str:
        """A content hash of :meth:`input_json_schema`, for drift detection."""
        return compute_schema_hash(self.input_json_schema())

    def _lookup_field_schema(self) -> JSONSchema:
        """Infer the JSON Schema type of this tool's detail lookup field.

        Inspects the viewset's ``queryset.model`` for a field matching
        :attr:`lookup_field` (typically the primary key, but any model
        field works for a custom ``lookup_field``) to decide between
        ``integer``, ``string`` (UUID), and plain ``string``. Falls back
        to ``integer`` if the model or field can't be determined (e.g. the
        viewset builds its queryset dynamically in ``get_queryset()``).
        """
        model = getattr(getattr(self.viewset_class, "queryset", None), "model", None)
        if model is None:
            return {"type": "integer"}
        try:
            model_field = model._meta.get_field(self.lookup_field)
        except FieldDoesNotExist:
            return {"type": "integer"}
        internal_type = model_field.get_internal_type()
        if internal_type in {"AutoField", "BigAutoField", "SmallAutoField", "IntegerField"}:
            return {"type": "integer"}
        if internal_type == "UUIDField":
            return {"type": "string", "format": "uuid"}
        return {"type": "string"}

schema_hash property

A content hash of :meth:input_json_schema, for drift detection.

input_json_schema()

Build the JSON Schema for this tool's callable arguments.

Returns:

Type Description
JSONSchema

An object schema combining the detail lookup parameter (for

JSONSchema

detail actions) with the serializer's writable fields (for

JSONSchema

actions with a request body). partial_update never marks

JSONSchema

fields as required, matching HTTP PATCH semantics.

Source code in src/drf_llm_gateway/registry.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def input_json_schema(self) -> JSONSchema:
    """Build the JSON Schema for this tool's callable arguments.

    Returns:
        An object schema combining the detail lookup parameter (for
        detail actions) with the serializer's writable fields (for
        actions with a request body). ``partial_update`` never marks
        fields as required, matching HTTP ``PATCH`` semantics.
    """
    properties: dict[str, JSONSchema] = {}
    required: list[str] = []

    if self.detail:
        properties[self.lookup_field] = self._lookup_field_schema()
        required.append(self.lookup_field)

    if self.serializer_class is not None and self.action in (
        "create",
        "update",
        "partial_update",
    ):
        body_schema = serializer_to_json_schema(self.serializer_class, mode="input")
        properties.update(body_schema.get("properties", {}))
        if self.action != "partial_update":
            required.extend(body_schema.get("required", []))

    schema: JSONSchema = {"type": "object", "properties": properties}
    if required:
        schema["required"] = sorted(set(required))
    if self.examples and get_setting("INCLUDE_EXAMPLES_IN_SCHEMA"):
        schema["examples"] = list(self.examples)
    return schema

default_registry

The module-level ToolRegistry instance expose_as_tool registers into unless a different registry= is passed explicitly.

STANDARD_ACTIONS

Mapping of standard ModelViewSet action names to (http_method, detail) tuples, used internally to resolve HTTP method and detail-ness for actions that aren't custom @action-decorated methods.

Schema generation

serializer_to_json_schema

Convert a DRF serializer to a JSON Schema object schema.

Parameters:

Name Type Description Default
serializer type[BaseSerializer[Any]] | BaseSerializer[Any]

A serializer class or instance. Classes are instantiated internally (with no arguments) purely for field introspection; no validation or I/O occurs.

required
mode str

"input" (default) includes only writable fields — the arguments a caller may supply — and is what :mod:drf_llm_gateway.openai_tools and :mod:drf_llm_gateway.mcp use for tool parameter schemas. "output" includes every field (read-only fields are marked "readOnly": true), useful for describing what a tool returns.

'input'

Returns:

Type Description
JSONSchema

A JSON-Schema-compatible object schema: a top-level type of

JSONSchema

object, a mapping of field name to field schema, and (in

JSONSchema

"input" mode) a list of the required field names.

Raises:

Type Description
SchemaGenerationError

If mode is invalid, or if nested serializers exceed the MAX_SCHEMA_DEPTH setting.

Example

A serializer with a required name field and an optional email field produces a schema whose required list contains only "name".

Source code in src/drf_llm_gateway/schema.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def serializer_to_json_schema(
    serializer: type[BaseSerializer[Any]] | BaseSerializer[Any],
    *,
    mode: str = "input",
) -> JSONSchema:
    """Convert a DRF serializer to a JSON Schema ``object`` schema.

    Args:
        serializer: A serializer class or instance. Classes are
            instantiated internally (with no arguments) purely for field
            introspection; no validation or I/O occurs.
        mode: ``"input"`` (default) includes only writable fields — the
            arguments a caller may supply — and is what
            :mod:`drf_llm_gateway.openai_tools` and :mod:`drf_llm_gateway.mcp`
            use for tool parameter schemas. ``"output"`` includes every
            field (read-only fields are marked ``"readOnly": true``),
            useful for describing what a tool *returns*.

    Returns:
        A JSON-Schema-compatible object schema: a top-level type of
        object, a mapping of field name to field schema, and (in
        ``"input"`` mode) a list of the required field names.

    Raises:
        drf_llm_gateway.exceptions.SchemaGenerationError: If ``mode`` is
            invalid, or if nested serializers exceed the
            ``MAX_SCHEMA_DEPTH`` setting.

    Example:
        A serializer with a required ``name`` field and an optional
        ``email`` field produces a schema whose ``required`` list contains
        only ``"name"``.
    """
    if mode not in ("input", "output"):
        raise SchemaGenerationError(f"mode must be 'input' or 'output', got {mode!r}.")
    instance = serializer() if isinstance(serializer, type) else serializer
    return _serializer_to_schema(instance, mode=mode, depth=1)

Export formats

to_openai_tool

Convert a single :class:ToolDefinition to an OpenAI tool object.

Parameters:

Name Type Description Default
tool ToolDefinition

The tool definition to convert.

required

Returns:

Type Description
dict[str, Any]

A dict shaped like the following, ready to pass directly in an

dict[str, Any]

OpenAI API call's tools argument:

dict[str, Any]

```python

dict[str, Any]

{"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}

dict[str, Any]

```

Source code in src/drf_llm_gateway/openai_tools.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def to_openai_tool(tool: ToolDefinition) -> dict[str, Any]:
    """Convert a single :class:`ToolDefinition` to an OpenAI tool object.

    Args:
        tool: The tool definition to convert.

    Returns:
        A dict shaped like the following, ready to pass directly in an
        OpenAI API call's ``tools`` argument:

        ```python
        {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
        ```
    """
    return {
        "type": "function",
        "function": {
            "name": tool.name,
            "description": tool.description,
            "parameters": tool.input_json_schema(),
        },
    }

to_openai_tools

Convert every tool in a registry to OpenAI tool objects.

Parameters:

Name Type Description Default
registry ToolRegistry | None

The registry to export. Defaults to :data:drf_llm_gateway.registry.default_registry.

None

Returns:

Type Description
list[dict[str, Any]]

A list of OpenAI tool objects, in registration order.

Source code in src/drf_llm_gateway/openai_tools.py
40
41
42
43
44
45
46
47
48
49
50
51
def to_openai_tools(registry: ToolRegistry | None = None) -> list[dict[str, Any]]:
    """Convert every tool in a registry to OpenAI tool objects.

    Args:
        registry: The registry to export. Defaults to
            :data:`drf_llm_gateway.registry.default_registry`.

    Returns:
        A list of OpenAI tool objects, in registration order.
    """
    source = registry if registry is not None else default_registry
    return [to_openai_tool(tool) for tool in source]

to_mcp_tool

Convert a single :class:ToolDefinition to an MCP tool object.

Parameters:

Name Type Description Default
tool ToolDefinition

The tool definition to convert.

required

Returns:

Type Description
dict[str, Any]

A dict of the form ``{"name": ..., "description": ...,

dict[str, Any]

"inputSchema": ...}``, matching the shape an MCP server returns

dict[str, Any]

from tools/list and expects to receive lookups against from

dict[str, Any]

tools/call.

Source code in src/drf_llm_gateway/mcp.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def to_mcp_tool(tool: ToolDefinition) -> dict[str, Any]:
    """Convert a single :class:`ToolDefinition` to an MCP tool object.

    Args:
        tool: The tool definition to convert.

    Returns:
        A ``dict`` of the form ``{"name": ..., "description": ...,
        "inputSchema": ...}``, matching the shape an MCP server returns
        from ``tools/list`` and expects to receive lookups against from
        ``tools/call``.
    """
    return {
        "name": tool.name,
        "description": tool.description,
        "inputSchema": tool.input_json_schema(),
    }

to_mcp_tools

Convert every tool in a registry to MCP tool objects.

Parameters:

Name Type Description Default
registry ToolRegistry | None

The registry to export. Defaults to :data:drf_llm_gateway.registry.default_registry.

None

Returns:

Type Description
list[dict[str, Any]]

A list of MCP tool objects, in registration order — directly

list[dict[str, Any]]

usable as the tools array of a tools/list response.

Source code in src/drf_llm_gateway/mcp.py
34
35
36
37
38
39
40
41
42
43
44
45
46
def to_mcp_tools(registry: ToolRegistry | None = None) -> list[dict[str, Any]]:
    """Convert every tool in a registry to MCP tool objects.

    Args:
        registry: The registry to export. Defaults to
            :data:`drf_llm_gateway.registry.default_registry`.

    Returns:
        A list of MCP tool objects, in registration order — directly
        usable as the ``tools`` array of a ``tools/list`` response.
    """
    source = registry if registry is not None else default_registry
    return [to_mcp_tool(tool) for tool in source]

Execution

execute_tool

Execute a registered tool with the given arguments.

Parameters:

Name Type Description Default
name str

The tool name, as produced by :func:drf_llm_gateway.registry.expose_as_tool.

required
arguments dict[str, Any]

The tool call arguments. For detail actions, must include the key named by the tool's lookup_field. For create/update/partial_update, the remaining keys are validated against the tool's serializer exactly as a real request body would be.

required
user AbstractBaseUser | AnonymousUser | None

The identity to dispatch as. Defaults to :class:~django.contrib.auth.models.AnonymousUser — pass a real user instance to exercise permission classes that require authentication.

None
registry ToolRegistry | None

The registry to look name up in. Defaults to :data:drf_llm_gateway.registry.default_registry.

None

Returns:

Name Type Description
A ToolResult

class:ToolResult with the response status code and body.

Raises:

Type Description
ToolNotFoundError

If name isn't registered.

ToolValidationError

If the underlying view returned 400 Bad Request (typically a serializer validation failure).

ToolPermissionDeniedError

If the underlying view returned 401 or 403.

Source code in src/drf_llm_gateway/executor.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def execute_tool(
    name: str,
    arguments: dict[str, Any],
    *,
    user: AbstractBaseUser | AnonymousUser | None = None,
    registry: ToolRegistry | None = None,
) -> ToolResult:
    """Execute a registered tool with the given arguments.

    Args:
        name: The tool name, as produced by
            :func:`drf_llm_gateway.registry.expose_as_tool`.
        arguments: The tool call arguments. For detail actions, must
            include the key named by the tool's ``lookup_field``. For
            ``create``/``update``/``partial_update``, the remaining keys
            are validated against the tool's serializer exactly as a real
            request body would be.
        user: The identity to dispatch as. Defaults to
            :class:`~django.contrib.auth.models.AnonymousUser` — pass a
            real user instance to exercise permission classes that require
            authentication.
        registry: The registry to look ``name`` up in. Defaults to
            :data:`drf_llm_gateway.registry.default_registry`.

    Returns:
        A :class:`ToolResult` with the response status code and body.

    Raises:
        drf_llm_gateway.exceptions.ToolNotFoundError: If ``name`` isn't
            registered.
        drf_llm_gateway.exceptions.ToolValidationError: If the underlying
            view returned ``400 Bad Request`` (typically a serializer
            validation failure).
        drf_llm_gateway.exceptions.ToolPermissionDeniedError: If the
            underlying view returned ``401`` or ``403``.
    """
    source = registry if registry is not None else default_registry
    tool = source.get(name)
    if tool is None:
        raise ToolNotFoundError(name)
    if tool.detail and tool.lookup_field not in arguments:
        raise ToolValidationError({tool.lookup_field: ["This field is required."]})

    response = _dispatch(tool, arguments, user=user)
    if response.status_code in (401, 403):
        raise ToolPermissionDeniedError(name)
    if response.status_code == 400:
        raise ToolValidationError(response.data)
    return ToolResult(status_code=response.status_code, data=response.data)

ToolResult

The outcome of successfully executing a tool.

Attributes:

Name Type Description
status_code int

The HTTP status code the underlying view produced.

data Any

The response body as plain Python data (what response.data contained before rendering) — safe to json.dumps directly.

Source code in src/drf_llm_gateway/executor.py
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass(frozen=True, slots=True)
class ToolResult:
    """The outcome of successfully executing a tool.

    Attributes:
        status_code: The HTTP status code the underlying view produced.
        data: The response body as plain Python data (what
            ``response.data`` contained before rendering) — safe to
            ``json.dumps`` directly.
    """

    status_code: int
    data: Any

Versioning

compute_schema_hash

Compute a stable, deterministic hash of a JSON Schema.

Parameters:

Name Type Description Default
schema dict[str, Any]

A JSON-Schema-compatible dict, as produced by :func:drf_llm_gateway.schema.serializer_to_json_schema or :meth:drf_llm_gateway.registry.ToolDefinition.input_json_schema.

required

Returns:

Type Description
str

A 16-character hexadecimal digest prefix. Dict key order does not

str

affect the result (json.dumps(..., sort_keys=True) is used

str

before hashing), so semantically identical schemas always hash

str

identically.

Example

compute_schema_hash({"type": "object", "properties": {}}) '...' # doctest: +SKIP

Source code in src/drf_llm_gateway/versioning.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def compute_schema_hash(schema: dict[str, Any]) -> str:
    """Compute a stable, deterministic hash of a JSON Schema.

    Args:
        schema: A JSON-Schema-compatible ``dict``, as produced by
            :func:`drf_llm_gateway.schema.serializer_to_json_schema` or
            :meth:`drf_llm_gateway.registry.ToolDefinition.input_json_schema`.

    Returns:
        A 16-character hexadecimal digest prefix. Dict key order does not
        affect the result (``json.dumps(..., sort_keys=True)`` is used
        before hashing), so semantically identical schemas always hash
        identically.

    Example:
        >>> compute_schema_hash({"type": "object", "properties": {}})
        '...'  # doctest: +SKIP
    """
    canonical = json.dumps(schema, sort_keys=True, separators=(",", ":"), default=str)
    digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    return digest[:16]

Exceptions

LLMGatewayError

Bases: Exception

Base class for all errors raised by this package.

Source code in src/drf_llm_gateway/exceptions.py
18
19
class LLMGatewayError(Exception):
    """Base class for all errors raised by this package."""

SchemaGenerationError

Bases: LLMGatewayError, ValueError

Raised when a serializer cannot be converted to JSON Schema.

The exception message describes what could not be converted and why (e.g. an invalid mode, or nesting beyond MAX_SCHEMA_DEPTH).

Source code in src/drf_llm_gateway/exceptions.py
22
23
24
25
26
27
class SchemaGenerationError(LLMGatewayError, ValueError):
    """Raised when a serializer cannot be converted to JSON Schema.

    The exception message describes what could not be converted and why
    (e.g. an invalid ``mode``, or nesting beyond ``MAX_SCHEMA_DEPTH``).
    """

ToolRegistrationError

Bases: LLMGatewayError, ValueError

Raised when :func:~drf_llm_gateway.registry.expose_as_tool or :meth:~drf_llm_gateway.registry.ToolRegistry.register is used incorrectly — e.g. duplicate tool names, or an unsupported action.

The exception message names the specific registration problem encountered.

Source code in src/drf_llm_gateway/exceptions.py
30
31
32
33
34
35
36
37
class ToolRegistrationError(LLMGatewayError, ValueError):
    """Raised when :func:`~drf_llm_gateway.registry.expose_as_tool` or
    :meth:`~drf_llm_gateway.registry.ToolRegistry.register` is used
    incorrectly — e.g. duplicate tool names, or an unsupported action.

    The exception message names the specific registration problem
    encountered.
    """

ToolExecutionError

Bases: LLMGatewayError, APIException

Base class for errors raised while executing a registered tool.

Source code in src/drf_llm_gateway/exceptions.py
40
41
42
43
class ToolExecutionError(LLMGatewayError, APIException):
    """Base class for errors raised while executing a registered tool."""

    default_code = "tool_execution_error"

ToolNotFoundError

Bases: ToolExecutionError

Raised when :func:~drf_llm_gateway.executor.execute_tool is asked to run a tool name that isn't registered.

Parameters:

Name Type Description Default
name str

The unrecognized tool name.

required
Source code in src/drf_llm_gateway/exceptions.py
46
47
48
49
50
51
52
53
54
55
56
57
58
class ToolNotFoundError(ToolExecutionError):
    """Raised when :func:`~drf_llm_gateway.executor.execute_tool` is asked
    to run a tool name that isn't registered.

    Args:
        name: The unrecognized tool name.
    """

    status_code = 404
    default_code = "tool_not_found"

    def __init__(self, name: str) -> None:
        super().__init__(detail=f"No tool named {name!r} is registered.", code=self.default_code)

ToolValidationError

Bases: ToolExecutionError, ValidationError

Raised when the arguments passed to a tool fail serializer validation.

The underlying :attr:detail mirrors the structure DRF's own :class:~rest_framework.exceptions.ValidationError produces, so field-level error messages are preserved.

Source code in src/drf_llm_gateway/exceptions.py
61
62
63
64
65
66
67
68
69
class ToolValidationError(ToolExecutionError, _DRFValidationError):
    """Raised when the arguments passed to a tool fail serializer validation.

    The underlying :attr:`detail` mirrors the structure DRF's own
    :class:`~rest_framework.exceptions.ValidationError` produces, so
    field-level error messages are preserved.
    """

    default_code = "tool_validation_error"

ToolPermissionDeniedError

Bases: ToolExecutionError, PermissionDenied

Raised when the calling identity fails a tool's permission checks.

Parameters:

Name Type Description Default
name str

The tool name that was denied.

required
Source code in src/drf_llm_gateway/exceptions.py
72
73
74
75
76
77
78
79
80
81
82
class ToolPermissionDeniedError(ToolExecutionError, _DRFPermissionDenied):
    """Raised when the calling identity fails a tool's permission checks.

    Args:
        name: The tool name that was denied.
    """

    default_code = "tool_permission_denied"

    def __init__(self, name: str) -> None:
        super().__init__(detail=f"Permission denied for tool {name!r}.", code=self.default_code)

Settings

get_setting

Return the effective value of a single package setting.

Parameters:

Name Type Description Default
key str

One of the keys documented in :data:DEFAULTS.

required

Returns:

Type Description
Any

The user-configured value if present in the LLM_GATEWAY Django

Any

setting, otherwise the default.

Raises:

Type Description
KeyError

If key is not a recognized setting name.

ImproperlyConfigured

If the LLM_GATEWAY setting is malformed.

Source code in src/drf_llm_gateway/settings.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def get_setting(key: str) -> Any:
    """Return the effective value of a single package setting.

    Args:
        key: One of the keys documented in :data:`DEFAULTS`.

    Returns:
        The user-configured value if present in the ``LLM_GATEWAY`` Django
        setting, otherwise the default.

    Raises:
        KeyError: If ``key`` is not a recognized setting name.
        django.core.exceptions.ImproperlyConfigured: If the ``LLM_GATEWAY``
            setting is malformed.
    """
    return app_settings[key]

See Settings for the full list of recognized keys.

Management command

generate_llm_tools

python manage.py generate_llm_tools [--format {openai,mcp,jsonschema}] [--indent N] [--urlconf PATH]

See Quick Start.