Source code for oumi.core.types.tool_call

# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Data structures for function/tool calling in LLM APIs.

These models use ``extra="allow"`` so unknown keys round-trip through
serialization unchanged. This matters for two reasons: (1) OpenAI may
add new fields to the tool wire format over time, and (2)
``transformers.utils.chat_template_utils.get_json_schema`` produces a
richer schema (e.g., a top-level ``return`` block on
``FunctionDefinition``) than the OpenAI Chat Completions API itself
defines, and chat templates may consume those extras. Dropping unknown
keys at validation time would silently lose information that
downstream code relies on.
"""

import json
from enum import Enum
from typing import Any, Literal

import pydantic
from pydantic import BaseModel, ConfigDict, JsonValue

# The seven primitive types defined by JSON Schema.
JSONSchemaType = Literal[
    "object", "string", "number", "integer", "boolean", "array", "null"
]


[docs] class JSONSchema(BaseModel): """A JSON Schema object describing the shape of a value. Models the subset of JSON Schema commonly used in LLM tool definitions. ``extra="allow"`` lets less-common keywords (``$ref``, ``$defs``, ``additionalProperties``, ``anyOf``, numeric constraints, etc.) round-trip unchanged, matching the rest of this module — see the module docstring for why round-tripping matters. """ model_config = ConfigDict(frozen=True, extra="allow") type: JSONSchemaType | list[JSONSchemaType] | None = None """JSON type(s) of this value. A list expresses a union (e.g., ``["string", "null"]`` for a nullable string). """ description: str | None = None """Human-readable description, used by the model to choose values.""" title: str | None = None """Short human-readable label.""" properties: dict[str, "JSONSchema"] | None = None """For ``type="object"``: schema for each named property.""" required: list[str] | None = None """For ``type="object"``: names of properties that must be present.""" items: "JSONSchema | None" = None """For ``type="array"``: schema for array elements.""" enum: list[Any] | None = None """Restricts the value to a fixed set of allowed values.""" default: Any = None """Default value used when the field is omitted.""" format: str | None = None """Semantic format hint (e.g., ``"date-time"``, ``"email"``)."""
[docs] class ToolType(str, Enum): """Type of tool available to the model.""" FUNCTION = "function" """A callable function that the model can invoke."""
[docs] def __str__(self) -> str: """Return the string representation of the ToolType enum.""" return self.value
[docs] class FunctionDefinition(BaseModel): """Definition of a function that can be called by the model.""" model_config = ConfigDict(frozen=True, extra="allow") name: str """The name of the function to be called. Must be a-z, A-Z, 0-9, underscores and dashes, with a maximum length of 64. """ description: str | None = None """A description of what the function does. Used by the model to choose when and how to call the function. """ parameters: JSONSchema | None = None """The parameters the function accepts, as a JSON Schema object. See https://json-schema.org/understanding-json-schema/ for the format. To describe a function that accepts no parameters, provide ``{"type": "object", "properties": {}}``. """ strict: bool | None = None """Whether to enable strict schema adherence in the generated call. If true, the model will follow the exact schema provided in ``parameters``. Only supported by OpenAI gpt-4o and later; ignored by other providers. """
[docs] class ToolDefinition(BaseModel): """Definition of a tool available to the model.""" model_config = ConfigDict(frozen=True, extra="allow") type: ToolType = ToolType.FUNCTION """The type of the tool. Currently only ``function`` is supported. Defaulted for ergonomics — when a second tool type lands, drop the default to force callers to be explicit. """ function: FunctionDefinition """The function definition."""
class _TemplateFieldAccess: """Dict-style field access, for chat templates that subscript and call ``.get()``. Gemma 4's template reads its inputs as mappings (``tool_call.get('function')``, ``function['arguments']``) rather than as objects, so a Pydantic model reaches it as something with no ``get`` and no ``__getitem__`` and rendering fails outright. ``Message`` already carries its own ``get`` for this reason; these are the same accommodation for the tool-call types. Only declared fields are exposed, so a key that collides with a method name returns the field or the default rather than the bound method. """ def __getitem__(self, key: str) -> Any: if key in type(self).model_fields: # type: ignore[attr-defined] return getattr(self, key) raise KeyError(key) def get(self, key: str, default: Any = None) -> Any: """Returns a field by name, dict-style, or ``default`` if absent.""" try: return self[key] except KeyError: return default
[docs] class FunctionCall(_TemplateFieldAccess, BaseModel): """A function call made by the model.""" model_config = ConfigDict(frozen=True, extra="allow") name: str """The name of the function being called.""" arguments: str """The arguments to call the function with, as a JSON string. OpenAI wire format keeps this as an unparsed JSON-encoded string (NOT a dict). Some providers return malformed JSON; downstream code is responsible for parsing and handling errors. """ @pydantic.field_validator("arguments", mode="before") @classmethod def _coerce_arguments_to_str(cls, raw: Any) -> str: """Normalize dict/list arguments to a JSON string. Runs in ``mode="before"`` so we can intercept non-string inputs (e.g., dicts from JSONL files) and serialize them before Pydantic's core validator enforces the ``str`` type. """ if isinstance(raw, str): return raw if isinstance(raw, (dict, list)): return json.dumps(raw) raise ValueError( f"arguments must be a str, dict, or list; got {type(raw).__name__}." )
[docs] def __getitem__(self, key: str) -> Any: """As `_TemplateFieldAccess`, except that ``arguments`` decodes to its mapping. Gemma 4's template reads ``function['arguments']`` and raises unless it gets a mapping, so the JSON string stored here fails to render at all. Decoding on subscript hands the template the object it asks for while ``.arguments`` keeps the OpenAI wire string as the storage contract, and a dataset written with mappings still normalizes to that string on the way in. A template that subscripts ``arguments`` expecting the raw JSON string sees a dict instead. Piping it through ``tojson`` produces the same text, and printing it bare would produce a Python repr. Attribute access is unchanged either way. """ if key == "arguments": return self.get_arguments_dict() return super().__getitem__(key)
[docs] def get_arguments_dict(self) -> dict[str, Any]: """Returns ``arguments`` decoded from its JSON string form. The string is the storage contract; every consumer that needs a dict decodes it here rather than reimplementing the parse and its error handling. """ if not self.arguments: return {} try: parsed = json.loads(self.arguments) except json.JSONDecodeError as e: raise ValueError( f"Function '{self.name}' has invalid JSON arguments: {e}" ) from e if not isinstance(parsed, dict): raise ValueError( f"Function '{self.name}' arguments must be a JSON object, " f"got {type(parsed).__name__}." ) return parsed
[docs] class ToolCall(_TemplateFieldAccess, BaseModel): """A tool call emitted by the model.""" model_config = ConfigDict(frozen=True, extra="allow") id: str """The ID of the tool call. Used to match a tool response message back to the call that requested it (via ``Message.tool_call_id``). """ type: ToolType = ToolType.FUNCTION """The type of tool call. Defaulted; see ``ToolDefinition.type``.""" function: FunctionCall """The function the model called."""
[docs] class ToolResult(BaseModel): """Result returned by an environment ``step()``. Runtime value (not an OpenAI wire-format type) — projected by the synthesizer into ``Message(role=TOOL, content=...)`` before output. ``output`` may be any JSON value — a string is used as-is, everything else is json-encoded at the message boundary. """ output: JsonValue updated_state: dict[str, Any] | None = None