Files
2026-07-13 13:22:34 +08:00

341 lines
10 KiB
Python

from __future__ import annotations
import warnings
from typing import Annotated, Any, Literal
from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field, model_serializer
class TextContentPart(BaseModel):
type: Literal["text"]
text: str
class ImageUrl(BaseModel):
"""
Represents an image URL.
Attributes:
url: Either a URL of an image or base64 encoded data.
https://platform.openai.com/docs/guides/vision?lang=curl#uploading-base64-encoded-images
detail: The level of resolution for the image when the model receives it.
For example, when set to "low", the model will see a image resized to
512x512 pixels, which consumes fewer tokens. In OpenAI, this is optional
and defaults to "auto".
https://platform.openai.com/docs/guides/vision?lang=curl#low-or-high-fidelity-image-understanding
"""
url: str
detail: Literal["auto", "low", "high"] | None = None
class ImageContentPart(BaseModel):
type: Literal["image_url"]
image_url: ImageUrl
class InputAudio(BaseModel):
data: str # base64 encoded data
format: Literal["wav", "mp3"]
class AudioContentPart(BaseModel):
type: Literal["input_audio"]
input_audio: InputAudio
ContentPartsList = list[
Annotated[TextContentPart | ImageContentPart | AudioContentPart, Field(discriminator="type")]
]
ContentType = Annotated[str | ContentPartsList, Field(union_mode="left_to_right")]
class Function(BaseModel):
name: str | None = None
arguments: str | None = None
def to_tool_call(self, id=None) -> ToolCall:
if id is None:
id = str(uuid4())
return ToolCall(id=id, type="function", function=self)
class ToolCall(BaseModel):
id: str
type: str = Field(default="function")
function: Function
# Gemini thinking-mode models return a thought_signature with each
# function call that must be echoed back in subsequent turns.
# https://ai.google.dev/gemini-api/docs/thought-signatures
thought_signature: str | None = Field(default=None)
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if data.get("thought_signature") is None:
data.pop("thought_signature", None)
return data
class ChatMessage(BaseModel):
"""
A chat request. ``content`` can be a string, or an array of content parts.
A content part is one of the following:
- :py:class:`TextContentPart <mlflow.types.chat.TextContentPart>`
- :py:class:`ImageContentPart <mlflow.types.chat.ImageContentPart>`
- :py:class:`AudioContentPart <mlflow.types.chat.AudioContentPart>`
"""
role: str
content: ContentType | None = None
# NB: In the actual OpenAI chat completion API spec, these fields only
# present in either the request or response message (tool_call_id is only in
# the request, while the other two are only in the response).
# Strictly speaking, we should separate the request and response message types
# to match OpenAI's API spec. However, we don't want to do that because we the
# request and response message types are not distinguished in many parts of the
# codebase, and also we don't want to ask users to use two different classes.
# Therefore, we include all fields in this class, while marking them as optional.
# TODO: Define a sub classes for different type of messages (request/response, and
# system/user/assistant/tool, etc), and create a factory function to allow users
# to create them without worrying about the details.
tool_calls: list[ToolCall] | None = None
refusal: str | None = None
tool_call_id: str | None = None
AllowedType = Literal["string", "number", "integer", "object", "array", "boolean", "null"]
class ParamType(BaseModel):
type: AllowedType | list[AllowedType] | None = None
class ParamProperty(ParamType):
"""
OpenAI uses JSON Schema (https://json-schema.org/) for function parameters.
See OpenAI function calling reference:
https://platform.openai.com/docs/guides/function-calling?&api-mode=responses#defining-functions
JSON Schema enum supports any JSON type (str, int, float, bool, null, arrays, objects),
but we restrict to basic scalar types for practical use cases and API safety.
"""
description: str | None = None
enum: list[str | int | float | bool] | None = None
# Recursive type so nested arrays (e.g. list[list[str]]) preserve their inner
# `items` schema through Pydantic round-trips. If this were `ParamType`, the
# inner `items` field would be silently stripped and downstream providers
# would reject the schema with "array schema missing items".
items: ParamProperty | None = None
class FunctionParams(BaseModel):
properties: dict[str, ParamProperty]
type: Literal["object"] = "object"
required: list[str] | None = None
additionalProperties: bool | None = None
class FunctionToolDefinition(BaseModel):
name: str
description: str | None = None
parameters: FunctionParams | None = None
strict: bool | None = None
class ChatTool(BaseModel):
"""
A tool definition passed to the chat completion API.
Ref: https://platform.openai.com/docs/guides/function-calling
"""
type: Literal["function"]
function: FunctionToolDefinition | None = None
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message='Field name "schema" in "JsonSchemaSpec" shadows an attribute in parent '
'"BaseModel"',
category=UserWarning,
)
class JsonSchemaSpec(BaseModel):
"""
OpenAI-compatible JSON Schema envelope for structured outputs.
Attributes:
name: The schema name.
schema: A JSON Schema definition.
strict: Whether model output should strictly follow the schema.
"""
model_config = ConfigDict(extra="allow")
name: str
schema: dict[str, Any] = Field(...)
strict: bool = True
class ResponseFormat(BaseModel):
"""
Response format configuration for structured outputs. Compatible with
OpenAI's Chat Completion API.
Supported formats:
- {"type": "text"}
- {"type": "json_object"}
- {"type": "json_schema", "json_schema": {"name": ..., "schema": {...}, "strict": true}}
"""
type: Literal["text", "json_object", "json_schema"]
json_schema: JsonSchemaSpec | None = None
class ToolChoiceFunction(BaseModel):
"""Specifies a tool the model should use."""
name: str
class ToolChoice(BaseModel):
"""
Specifies a particular tool to use.
OpenAI format: {"type": "function", "function": {"name": "my_function"}}
"""
type: Literal["function"]
function: ToolChoiceFunction
class BaseRequestPayload(BaseModel):
"""Common parameters used for chat completions and completion endpoints."""
n: int = Field(1, ge=1)
stop: list[str] | None = Field(None, min_length=1)
max_tokens: int | None = Field(None, ge=1)
max_completion_tokens: int | None = Field(None, ge=1)
stream: bool | None = None
stream_options: dict[str, Any] | None = None
model: str | None = None
response_format: ResponseFormat | None = None
temperature: float | None = Field(None, ge=0, le=2)
top_p: float | None = Field(None, ge=0, le=1)
presence_penalty: float | None = Field(None, ge=-2, le=2)
frequency_penalty: float | None = Field(None, ge=-2, le=2)
top_k: int | None = Field(None, ge=1)
# NB: For interface constructs that rely on other BaseModel implementations, in
# pydantic 1 the **order** in which classes are defined in this module is absolutely
# critical to prevent ForwardRef errors. Pydantic 2 does not have this limitation.
# To maintain compatibility with Pydantic 1, ensure that all classes that are defined in
# this file have dependencies defined higher than the line of usage.
class ChatChoice(BaseModel):
index: int
message: ChatMessage
finish_reason: str | None = None
class PromptTokensDetails(BaseModel):
model_config = {"extra": "allow"}
cached_tokens: int | None = None
class ChatUsage(BaseModel):
model_config = {"extra": "allow"}
prompt_tokens: int | None = None
completion_tokens: int | None = None
total_tokens: int | None = None
prompt_tokens_details: PromptTokensDetails | None = None
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if data.get("prompt_tokens_details") is None:
data.pop("prompt_tokens_details", None)
return data
class ToolCallDelta(BaseModel):
index: int
id: str | None = None
type: str | None = None
function: Function
# Gemini thinking-mode models return a thought_signature with each
# function call that must be echoed back in subsequent turns.
# https://ai.google.dev/gemini-api/docs/thought-signatures
thought_signature: str | None = Field(default=None)
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if data.get("thought_signature") is None:
data.pop("thought_signature", None)
return data
class ChatChoiceDelta(BaseModel):
role: str | None = None
content: str | None = None
tool_calls: list[ToolCallDelta] | None = None
class ChatChunkChoice(BaseModel):
index: int
finish_reason: str | None = None
delta: ChatChoiceDelta
class ChatCompletionChunk(BaseModel):
"""A chunk of a chat completion stream response."""
id: str | None = None
object: str = "chat.completion.chunk"
created: int
model: str
choices: list[ChatChunkChoice]
usage: ChatUsage | None = None
class ChatCompletionRequest(BaseRequestPayload):
"""
A request to the chat completion API.
Must be compatible with OpenAI's Chat Completion API.
https://platform.openai.com/docs/api-reference/chat
"""
messages: list[ChatMessage] = Field(..., min_length=1)
tools: list[ChatTool] | None = Field(None, min_length=1)
tool_choice: Literal["none", "auto", "required"] | ToolChoice | None = None
class ChatCompletionResponse(BaseModel):
"""
A response from the chat completion API.
Must be compatible with OpenAI's Chat Completion API.
https://platform.openai.com/docs/api-reference/chat
"""
id: str | None = None
object: str = "chat.completion"
created: int
model: str
choices: list[ChatChoice]
usage: ChatUsage