94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
1795 lines
59 KiB
Python
1795 lines
59 KiB
Python
# Copyright 2023-2024 SGLang Team
|
||
# 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.
|
||
# ==============================================================================
|
||
"""Pydantic models for OpenAI API protocol"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
import uuid
|
||
from dataclasses import dataclass
|
||
from typing import (
|
||
Any,
|
||
Dict,
|
||
List,
|
||
NamedTuple,
|
||
Optional,
|
||
Protocol,
|
||
Tuple,
|
||
TypeAlias,
|
||
Union,
|
||
get_args,
|
||
runtime_checkable,
|
||
)
|
||
|
||
from openai.types.responses import (
|
||
ResponseFunctionToolCall,
|
||
ResponseInputItemParam,
|
||
ResponseOutputItem,
|
||
ResponseOutputMessage,
|
||
ResponseOutputText,
|
||
ResponseReasoningItem,
|
||
)
|
||
from openai.types.responses.response import ToolChoice
|
||
from openai.types.responses.tool import Tool
|
||
from pydantic import (
|
||
BaseModel,
|
||
ConfigDict,
|
||
Field,
|
||
field_validator,
|
||
model_serializer,
|
||
model_validator,
|
||
)
|
||
from typing_extensions import Literal
|
||
|
||
try:
|
||
from xgrammar import StructuralTag
|
||
except:
|
||
StructuralTag = Any
|
||
|
||
from sglang.utils import convert_json_schema_to_str
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
DEFAULT_MODEL_NAME = "default"
|
||
|
||
|
||
class ModelCard(BaseModel):
|
||
"""Model cards."""
|
||
|
||
id: str
|
||
object: str = "model"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
owned_by: str = "sglang"
|
||
root: Optional[str] = None
|
||
parent: Optional[str] = None
|
||
max_model_len: Optional[int] = None
|
||
|
||
|
||
class ModelList(BaseModel):
|
||
"""Model list consists of model cards."""
|
||
|
||
object: str = "list"
|
||
data: List[ModelCard] = Field(default_factory=list)
|
||
|
||
|
||
class ErrorResponse(BaseModel):
|
||
object: str = "error"
|
||
message: str
|
||
type: str
|
||
param: Optional[str] = None
|
||
code: int
|
||
|
||
|
||
@runtime_checkable
|
||
class ParsedResponseFields(Protocol):
|
||
"""Protocol for parsed response fields from custom renderers."""
|
||
|
||
content: Optional[str]
|
||
tool_calls: Optional[List[Dict]]
|
||
reasoning_content: Optional[str]
|
||
|
||
|
||
class ResponseParserProtocol(Protocol):
|
||
"""Protocol for custom response parsers.
|
||
|
||
Implementations parse model output tokens into structured OpenAI response fields.
|
||
"""
|
||
|
||
def parse_response(
|
||
self, output_ids: List[int]
|
||
) -> Union[ParsedResponseFields, ErrorResponse]:
|
||
"""Parse complete response from output token IDs."""
|
||
...
|
||
|
||
def build_streaming_sse_chunks(
|
||
self,
|
||
output_ids: List[int],
|
||
index: int,
|
||
chunk_id: str,
|
||
model: str,
|
||
usage: Optional[Dict],
|
||
) -> Tuple[List[str], bool, Optional[str]]:
|
||
"""Parse streaming tokens and build SSE chunks.
|
||
|
||
Returns: (sse_chunks, has_tool_calls, error_message)
|
||
"""
|
||
...
|
||
|
||
|
||
class LogProbs(BaseModel):
|
||
text_offset: List[int] = Field(default_factory=list)
|
||
token_logprobs: List[Optional[float]] = Field(default_factory=list)
|
||
tokens: List[str] = Field(default_factory=list)
|
||
top_logprobs: List[Optional[Dict[str, float]]] = Field(default_factory=list)
|
||
|
||
|
||
class TopLogprob(BaseModel):
|
||
token: str
|
||
bytes: List[int]
|
||
logprob: float
|
||
|
||
|
||
class ChatCompletionTokenLogprob(BaseModel):
|
||
token: str
|
||
bytes: List[int]
|
||
logprob: float
|
||
top_logprobs: List[TopLogprob]
|
||
|
||
|
||
class ChoiceLogprobs(BaseModel):
|
||
# build for v1/chat/completions response
|
||
content: List[ChatCompletionTokenLogprob]
|
||
|
||
|
||
class CachedTokensDetails(BaseModel):
|
||
"""Detailed breakdown of cached tokens by cache source."""
|
||
|
||
device: int = 0 # Tokens from device cache (GPU)
|
||
host: int = 0 # Tokens from host cache (CPU memory)
|
||
# L3 storage fields are only present when storage backend is enabled
|
||
storage: Optional[int] = None # Tokens from L3 storage backend
|
||
storage_backend: Optional[str] = None # Type of storage backend used
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
# Remove None fields so they don't appear in response when L3 is disabled
|
||
if self.storage is None:
|
||
data.pop("storage", None)
|
||
if self.storage_backend is None:
|
||
data.pop("storage_backend", None)
|
||
return data
|
||
|
||
|
||
class PromptTokensDetails(BaseModel):
|
||
"""Details about prompt tokens."""
|
||
|
||
cached_tokens: int = 0
|
||
# Multimodal prompt token counts (only populated when present in the prompt)
|
||
image_tokens: Optional[int] = None
|
||
audio_tokens: Optional[int] = None
|
||
video_tokens: Optional[int] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
# Drop multimodal fields when absent so text-only/cache-only responses
|
||
# keep the original {"cached_tokens": N} shape.
|
||
for key in ("image_tokens", "audio_tokens", "video_tokens"):
|
||
if data.get(key) is None:
|
||
data.pop(key, None)
|
||
return data
|
||
|
||
|
||
class UsageInfo(BaseModel):
|
||
prompt_tokens: int = 0
|
||
total_tokens: int = 0
|
||
completion_tokens: Optional[int] = 0
|
||
# Used to return cached tokens info when --enable-cache-report is set
|
||
prompt_tokens_details: Optional[PromptTokensDetails] = None
|
||
reasoning_tokens: Optional[int] = 0
|
||
|
||
|
||
class StreamOptions(BaseModel):
|
||
include_usage: Optional[bool] = False
|
||
continuous_usage_stats: Optional[bool] = False
|
||
|
||
|
||
class JsonSchemaResponseFormat(BaseModel):
|
||
name: str
|
||
description: Optional[str] = None
|
||
# use alias to workaround pydantic conflict
|
||
schema_: Optional[Dict[str, object]] = Field(alias="schema", default=None)
|
||
strict: Optional[bool] = False
|
||
|
||
|
||
class ResponseFormat(BaseModel):
|
||
type: Literal["text", "json_object", "json_schema"]
|
||
json_schema: Optional[JsonSchemaResponseFormat] = None
|
||
|
||
|
||
class StructuresResponseFormat(BaseModel):
|
||
begin: str
|
||
schema_: Optional[Dict[str, object]] = Field(alias="schema", default=None)
|
||
end: str
|
||
|
||
|
||
# NOTE(dark): keep this for backward compatibility
|
||
class LegacyStructuralTagResponseFormat(BaseModel):
|
||
type: Literal["structural_tag"]
|
||
structures: List[StructuresResponseFormat]
|
||
triggers: List[str]
|
||
at_least_one: bool = False
|
||
|
||
|
||
StructuralTagResponseFormat: TypeAlias = Union[
|
||
LegacyStructuralTagResponseFormat, StructuralTag
|
||
]
|
||
|
||
ToolCallConstraint: TypeAlias = Union[
|
||
Tuple[Literal["structural_tag"], StructuralTagResponseFormat],
|
||
Tuple[Literal["json_schema"], Any], # json_schema can be dict/str/None
|
||
]
|
||
|
||
|
||
class FileRequest(BaseModel):
|
||
# https://platform.openai.com/docs/api-reference/files/create
|
||
file: bytes # The File object (not file name) to be uploaded
|
||
purpose: str = (
|
||
"batch" # The intended purpose of the uploaded file, default is "batch"
|
||
)
|
||
|
||
|
||
class FileResponse(BaseModel):
|
||
id: str
|
||
object: str = "file"
|
||
bytes: int
|
||
created_at: int
|
||
filename: str
|
||
purpose: str
|
||
|
||
|
||
class FileDeleteResponse(BaseModel):
|
||
id: str
|
||
object: str = "file"
|
||
deleted: bool
|
||
|
||
|
||
class BatchRequest(BaseModel):
|
||
input_file_id: (
|
||
str # The ID of an uploaded file that contains requests for the new batch
|
||
)
|
||
endpoint: str # The endpoint to be used for all requests in the batch
|
||
completion_window: str # The time frame within which the batch should be processed
|
||
metadata: Optional[dict] = None # Optional custom metadata for the batch
|
||
|
||
|
||
class BatchResponse(BaseModel):
|
||
id: str
|
||
object: str = "batch"
|
||
endpoint: str
|
||
errors: Optional[dict] = None
|
||
input_file_id: str
|
||
completion_window: str
|
||
status: str = "validating"
|
||
output_file_id: Optional[str] = None
|
||
error_file_id: Optional[str] = None
|
||
created_at: int
|
||
in_progress_at: Optional[int] = None
|
||
expires_at: Optional[int] = None
|
||
finalizing_at: Optional[int] = None
|
||
completed_at: Optional[int] = None
|
||
failed_at: Optional[int] = None
|
||
expired_at: Optional[int] = None
|
||
cancelling_at: Optional[int] = None
|
||
cancelled_at: Optional[int] = None
|
||
request_counts: Optional[dict] = None
|
||
metadata: Optional[dict] = None
|
||
|
||
|
||
def _migrate_deprecated_dp_rank(values: dict) -> dict:
|
||
if isinstance(values, dict) and values.get("data_parallel_rank") is not None:
|
||
import warnings
|
||
|
||
warnings.warn(
|
||
"'data_parallel_rank' is deprecated, use 'routed_dp_rank' instead.",
|
||
DeprecationWarning,
|
||
stacklevel=2,
|
||
)
|
||
if values.get("routed_dp_rank") is None:
|
||
values["routed_dp_rank"] = values["data_parallel_rank"]
|
||
return values
|
||
|
||
|
||
class CompletionRequest(BaseModel):
|
||
# Ordered by official OpenAI API documentation
|
||
# https://platform.openai.com/docs/api-reference/completions/create
|
||
model: str = Field(
|
||
default=DEFAULT_MODEL_NAME,
|
||
description="Model name. Supports LoRA adapters via 'base-model:adapter-name' syntax.",
|
||
)
|
||
prompt: Union[List[int], List[List[int]], str, List[str]]
|
||
best_of: Optional[int] = None
|
||
echo: bool = False
|
||
frequency_penalty: float = 0.0
|
||
logit_bias: Optional[Dict[str, float]] = None
|
||
logprobs: Optional[int] = None
|
||
max_tokens: int = 16
|
||
n: int = 1
|
||
presence_penalty: float = 0.0
|
||
seed: Optional[int] = None
|
||
stop: Optional[Union[str, List[str]]] = None
|
||
stream: bool = False
|
||
stream_options: Optional[StreamOptions] = None
|
||
suffix: Optional[str] = None
|
||
temperature: float = 1.0
|
||
top_p: float = 1.0
|
||
user: Optional[str] = None
|
||
return_hidden_states: bool = False
|
||
return_routed_experts: bool = False
|
||
routed_experts_start_len: int = 0
|
||
return_cached_tokens_details: bool = False
|
||
|
||
# Extra parameters for SRT backend only and will be ignored by OpenAI models.
|
||
top_k: int = -1
|
||
min_p: float = 0.0
|
||
min_tokens: int = 0
|
||
json_schema: Optional[str] = None
|
||
regex: Optional[str] = None
|
||
ebnf: Optional[str] = None
|
||
repetition_penalty: float = 1.0
|
||
stop_token_ids: Optional[List[int]] = None
|
||
stop_regex: Optional[Union[str, List[str]]] = None
|
||
no_stop_trim: bool = False
|
||
ignore_eos: bool = False
|
||
skip_special_tokens: bool = True
|
||
lora_path: Optional[Union[List[Optional[str]], Optional[str]]] = None
|
||
session_id: Optional[str] = None
|
||
session_params: Optional[Dict] = None
|
||
response_format: Optional[Union[ResponseFormat, StructuralTagResponseFormat]] = None
|
||
custom_params: Optional[Dict] = None
|
||
custom_logit_processor: Optional[str] = None
|
||
|
||
images_config: Optional[Dict] = None
|
||
|
||
# For PD disaggregation
|
||
bootstrap_host: Optional[Union[List[str], str]] = None
|
||
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
||
bootstrap_room: Optional[Union[List[int], int]] = None
|
||
|
||
# For DP routing — external router assigns a specific DP worker
|
||
routed_dp_rank: Optional[int] = None
|
||
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||
disagg_prefill_dp_rank: Optional[int] = None
|
||
# Deprecated: use routed_dp_rank instead
|
||
data_parallel_rank: Optional[int] = None
|
||
|
||
# For request id
|
||
rid: Optional[Union[List[str], str]] = None
|
||
# Extra key for classifying the request (e.g. cache_salt)
|
||
extra_key: Optional[Union[List[str], str]] = None
|
||
# Cache salt for request caching
|
||
cache_salt: Optional[Union[List[str], str]] = None
|
||
# Priority for the request
|
||
priority: Optional[int] = None
|
||
|
||
# For custom metric labels
|
||
custom_labels: Optional[Dict[str, str]] = None
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def _handle_deprecated_dp_rank(cls, values):
|
||
return _migrate_deprecated_dp_rank(values)
|
||
|
||
@field_validator("max_tokens")
|
||
@classmethod
|
||
def validate_max_tokens_positive(cls, v):
|
||
if v is not None and v <= 0:
|
||
raise ValueError("max_tokens must be positive")
|
||
return v
|
||
|
||
|
||
class SglExt(BaseModel):
|
||
"""SGLang extension fields for OpenAI-compatible responses.
|
||
|
||
Future SGLang-specific extensions to OpenAI-compatible response objects
|
||
should be added as fields here rather than directly on the choice object.
|
||
"""
|
||
|
||
routed_experts: Optional[str] = None
|
||
cached_tokens_details: Optional[CachedTokensDetails] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
# Remove None fields to keep response clean
|
||
return {k: v for k, v in data.items() if v is not None}
|
||
|
||
|
||
class CompletionResponseChoice(BaseModel):
|
||
index: int
|
||
text: str
|
||
logprobs: Optional[LogProbs] = None
|
||
finish_reason: Optional[Literal["stop", "length", "content_filter", "abort"]] = None
|
||
matched_stop: Union[None, int, str] = None
|
||
hidden_states: Optional[object] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.hidden_states is None:
|
||
data.pop("hidden_states", None)
|
||
return data
|
||
|
||
|
||
class CompletionResponse(BaseModel):
|
||
id: str
|
||
object: str = "text_completion"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
choices: List[CompletionResponseChoice]
|
||
usage: UsageInfo
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
sglext: Optional[SglExt] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.sglext is None:
|
||
data.pop("sglext", None)
|
||
return data
|
||
|
||
|
||
class CompletionResponseStreamChoice(BaseModel):
|
||
index: int
|
||
text: str
|
||
logprobs: Optional[LogProbs] = None
|
||
finish_reason: Optional[Literal["stop", "length", "content_filter", "abort"]] = None
|
||
matched_stop: Union[None, int, str] = None
|
||
hidden_states: Optional[object] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.hidden_states is None:
|
||
data.pop("hidden_states", None)
|
||
return data
|
||
|
||
|
||
class CompletionStreamResponse(BaseModel):
|
||
id: str
|
||
object: str = "text_completion"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
choices: List[CompletionResponseStreamChoice]
|
||
usage: Optional[UsageInfo] = None
|
||
sglext: Optional[SglExt] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.sglext is None:
|
||
data.pop("sglext", None)
|
||
return data
|
||
|
||
|
||
class ChatCompletionMessageContentTextPart(BaseModel):
|
||
type: Literal["text"]
|
||
text: str
|
||
|
||
|
||
class ChatCompletionMessageContentImageURL(BaseModel):
|
||
url: str
|
||
detail: Optional[Literal["auto", "low", "high"]] = "auto"
|
||
max_dynamic_patch: Optional[int] = None
|
||
min_dynamic_patch: Optional[int] = None
|
||
|
||
|
||
class ChatCompletionMessageContentVideoURL(BaseModel):
|
||
url: str
|
||
max_dynamic_patch: Optional[int] = None
|
||
min_dynamic_patch: Optional[int] = None
|
||
|
||
|
||
class ChatCompletionMessageContentAudioURL(BaseModel):
|
||
url: str
|
||
|
||
|
||
class ChatCompletionMessageContentImagePart(BaseModel):
|
||
type: Literal["image_url"]
|
||
image_url: ChatCompletionMessageContentImageURL
|
||
modalities: Optional[Literal["image", "multi-images", "video"]] = "image"
|
||
|
||
|
||
class ChatCompletionMessageContentVideoPart(BaseModel):
|
||
type: Literal["video_url"]
|
||
video_url: ChatCompletionMessageContentVideoURL
|
||
|
||
|
||
class ChatCompletionMessageContentAudioPart(BaseModel):
|
||
type: Literal["audio_url"]
|
||
audio_url: ChatCompletionMessageContentAudioURL
|
||
|
||
|
||
class ChatCompletionMessageContentToolReferenceBlock(BaseModel):
|
||
# GLM-specific extension used alongside `defer_loading` tools. The chat
|
||
# template looks up `tools[*].function.name == tr.name` and renders the
|
||
# referenced tool schemas inline for the current turn. Not part of any
|
||
# OpenAI API; included here so Pydantic accepts the content through the
|
||
# Chat Completions path (the Anthropic endpoint translates its
|
||
# `tool_name` field to `name` before forwarding).
|
||
type: Literal["tool_reference"]
|
||
name: str
|
||
|
||
|
||
ChatCompletionMessageContentPart = Union[
|
||
ChatCompletionMessageContentTextPart,
|
||
ChatCompletionMessageContentImagePart,
|
||
ChatCompletionMessageContentVideoPart,
|
||
ChatCompletionMessageContentAudioPart,
|
||
ChatCompletionMessageContentToolReferenceBlock,
|
||
]
|
||
|
||
# Rerank content types for multimodal reranking (e.g., Qwen3-VL-Reranker)
|
||
# Can be a simple string (text-only) or a list of multimodal content parts
|
||
RerankContentPart = Union[
|
||
ChatCompletionMessageContentTextPart,
|
||
ChatCompletionMessageContentImagePart,
|
||
ChatCompletionMessageContentVideoPart,
|
||
]
|
||
RerankContent = Union[str, List[RerankContentPart]]
|
||
|
||
|
||
class FunctionResponse(BaseModel):
|
||
"""Function response."""
|
||
|
||
name: Optional[str] = None
|
||
arguments: Optional[str | Dict[str, Any]] = None
|
||
|
||
|
||
class ToolCall(BaseModel):
|
||
"""Tool call response."""
|
||
|
||
id: Optional[str] = None
|
||
index: Optional[int] = None
|
||
type: Literal["function"] = "function"
|
||
function: FunctionResponse
|
||
|
||
|
||
_GenericMessageRole = Literal[
|
||
"system", "assistant", "tool", "function", "developer", "latest_reminder"
|
||
]
|
||
_GENERIC_MESSAGE_ROLES: Tuple[str, ...] = get_args(_GenericMessageRole)
|
||
|
||
|
||
class ChatCompletionMessageGenericParam(BaseModel):
|
||
role: _GenericMessageRole
|
||
content: Union[str, List[ChatCompletionMessageContentPart], None] = Field(
|
||
default=None
|
||
)
|
||
tool_call_id: Optional[str] = None
|
||
name: Optional[str] = None
|
||
reasoning_content: Optional[str] = None
|
||
tool_calls: Optional[List[ToolCall]] = Field(default=None, examples=[None])
|
||
tools: Optional[List[Tool]] = Field(default=None, examples=[None])
|
||
|
||
@field_validator("role", mode="before")
|
||
@classmethod
|
||
def _normalize_role(cls, v):
|
||
if isinstance(v, str):
|
||
v_lower = v.lower()
|
||
if v_lower not in _GENERIC_MESSAGE_ROLES:
|
||
allowed = ", ".join(repr(r) for r in _GENERIC_MESSAGE_ROLES)
|
||
raise ValueError(f"'role' must be one of {allowed} (case-insensitive).")
|
||
return v_lower
|
||
raise ValueError("'role' must be a string")
|
||
|
||
|
||
class ChatCompletionMessageUserParam(BaseModel):
|
||
role: Literal["user"]
|
||
content: Union[str, List[ChatCompletionMessageContentPart]]
|
||
|
||
|
||
ChatCompletionMessageParam = Union[
|
||
ChatCompletionMessageGenericParam, ChatCompletionMessageUserParam
|
||
]
|
||
|
||
|
||
class Function(BaseModel):
|
||
"""Function descriptions."""
|
||
|
||
description: Optional[str] = Field(default=None, examples=[None])
|
||
name: str
|
||
parameters: Optional[object] = None
|
||
strict: bool = False
|
||
defer_loading: Optional[bool] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.defer_loading is None:
|
||
data.pop("defer_loading", None)
|
||
return data
|
||
|
||
|
||
class Tool(BaseModel):
|
||
"""Function wrapper."""
|
||
|
||
type: str = Field(default="function", examples=["function"])
|
||
function: Function
|
||
defer_loading: Optional[bool] = None
|
||
|
||
@model_validator(mode="after")
|
||
def _propagate_defer_loading(self) -> Tool:
|
||
if self.defer_loading is not None and self.function.defer_loading is None:
|
||
self.function.defer_loading = self.defer_loading
|
||
return self
|
||
|
||
|
||
class ToolChoiceFuncName(BaseModel):
|
||
"""The name of tool choice function."""
|
||
|
||
name: Optional[str] = None
|
||
|
||
|
||
class ToolChoice(BaseModel):
|
||
"""The tool choice definition."""
|
||
|
||
function: ToolChoiceFuncName
|
||
type: Literal["function"] = Field(default="function", examples=["function"])
|
||
|
||
|
||
class ChatCompletionRequest(BaseModel):
|
||
# Ordered by official OpenAI API documentation
|
||
# https://platform.openai.com/docs/api-reference/chat/create
|
||
messages: List[ChatCompletionMessageParam]
|
||
model: str = Field(
|
||
default=DEFAULT_MODEL_NAME,
|
||
description="Model name. Supports LoRA adapters via 'base-model:adapter-name' syntax.",
|
||
)
|
||
frequency_penalty: float = 0.0
|
||
logit_bias: Optional[Dict[str, float]] = None
|
||
logprobs: bool = False
|
||
top_logprobs: Optional[int] = None
|
||
max_tokens: Optional[int] = Field(
|
||
default=None,
|
||
deprecated="max_tokens is deprecated in favor of the max_completion_tokens field",
|
||
description="The maximum number of tokens that can be generated in the chat completion. ",
|
||
)
|
||
max_completion_tokens: Optional[int] = Field(
|
||
default=None,
|
||
description="The maximum number of completion tokens for a chat completion request, "
|
||
"including visible output tokens and reasoning tokens. Input tokens are not included. ",
|
||
)
|
||
n: int = 1
|
||
presence_penalty: float = 0.0
|
||
response_format: Optional[Union[ResponseFormat, StructuralTagResponseFormat]] = None
|
||
seed: Optional[int] = None
|
||
stop: Optional[Union[str, List[str]]] = None
|
||
stream: bool = False
|
||
stream_options: Optional[StreamOptions] = None
|
||
temperature: Optional[float] = None
|
||
top_p: Optional[float] = None
|
||
user: Optional[str] = None
|
||
tools: Optional[List[Tool]] = Field(default=None, examples=[None])
|
||
tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]] = Field(
|
||
default="auto", examples=["none"]
|
||
) # noqa
|
||
parallel_tool_calls: bool = True
|
||
return_hidden_states: bool = False
|
||
return_routed_experts: bool = False
|
||
routed_experts_start_len: int = 0
|
||
return_cached_tokens_details: bool = False
|
||
return_prompt_token_ids: bool = False
|
||
return_meta_info: bool = False
|
||
reasoning_effort: Optional[Literal["none", "low", "medium", "high", "max"]] = Field(
|
||
default=None,
|
||
description="Constrains effort on reasoning for reasoning models. "
|
||
"'none' disables reasoning entirely, 'low' is the least effort, 'high' is the most effort. "
|
||
"Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning "
|
||
"in a response. 'none' defaults thinking and enable_thinking to false in "
|
||
"chat_template_kwargs (unless explicitly overridden). Not supported in the harmony path."
|
||
"'max' is an sglang extension to the OpenAI schema for "
|
||
"models that expose a maximum-effort tier above 'high'; models that don't "
|
||
"support it treat it the same as 'high'.",
|
||
)
|
||
task: Optional[
|
||
Literal["action", "query", "authority", "domain", "title", "read_url"]
|
||
] = Field(
|
||
default=None,
|
||
description="DeepSeek-V4 quick instruction task. When set, the last "
|
||
"user/developer message is treated as a single-shot classification prompt "
|
||
"and the corresponding task special token (e.g. `<|domain|>`) is appended "
|
||
"before generation. Only honored by the dsv4 chat encoder; ignored otherwise.",
|
||
)
|
||
|
||
# Extra parameters for SRT backend only and will be ignored by OpenAI models.
|
||
top_k: Optional[int] = None
|
||
min_p: Optional[float] = None
|
||
min_tokens: int = 0
|
||
regex: Optional[str] = None
|
||
ebnf: Optional[str] = None
|
||
repetition_penalty: Optional[float] = None
|
||
stop_token_ids: Optional[List[int]] = None
|
||
stop_regex: Optional[Union[str, List[str]]] = None
|
||
no_stop_trim: bool = False
|
||
ignore_eos: bool = False
|
||
continue_final_message: bool = False
|
||
skip_special_tokens: bool = True
|
||
lora_path: Optional[Union[List[Optional[str]], Optional[str]]] = None
|
||
session_id: Optional[str] = None
|
||
session_params: Optional[Dict] = None
|
||
separate_reasoning: bool = True
|
||
stream_reasoning: bool = True
|
||
chat_template_kwargs: Optional[Dict] = None
|
||
|
||
# SGLang multimodal controls (extensions)
|
||
max_dynamic_patch: Optional[int] = None
|
||
min_dynamic_patch: Optional[int] = None
|
||
use_audio_in_video: bool = False
|
||
|
||
images_config: Optional[Dict] = None
|
||
|
||
# Custom logit processor for advanced sampling control
|
||
custom_logit_processor: Optional[Union[List[Optional[str]], str]] = None
|
||
custom_params: Optional[Dict] = None
|
||
|
||
# Pre-computed prompt token IDs: when provided, bypasses chat template
|
||
# tokenization entirely. Messages are still used to derive stop tokens
|
||
# and tool_call_constraint.
|
||
input_ids: Optional[List[int]] = None
|
||
|
||
# For request id
|
||
rid: Optional[Union[List[str], str]] = None
|
||
# Extra key for classifying the request (e.g. cache_salt)
|
||
extra_key: Optional[Union[List[str], str]] = None
|
||
# Cache salt for request caching
|
||
cache_salt: Optional[Union[List[str], str]] = None
|
||
# Priority for the request
|
||
priority: Optional[int] = None
|
||
|
||
# For PD disaggregation
|
||
bootstrap_host: Optional[Union[List[str], str]] = None
|
||
bootstrap_port: Optional[Union[List[Optional[int]], int]] = None
|
||
bootstrap_room: Optional[Union[List[int], int]] = None
|
||
|
||
# For DP routing — external router assigns a specific DP worker
|
||
routed_dp_rank: Optional[int] = None
|
||
# For PD disagg — hint telling decode which prefill DP worker has the KV cache
|
||
disagg_prefill_dp_rank: Optional[int] = None
|
||
# Deprecated: use routed_dp_rank instead
|
||
data_parallel_rank: Optional[int] = None
|
||
|
||
# OpenAI/SGLang default sampling parameters
|
||
_DEFAULT_SAMPLING_PARAMS = {
|
||
"temperature": 1.0,
|
||
"top_p": 1.0,
|
||
"top_k": -1,
|
||
"min_p": 0.0,
|
||
"repetition_penalty": 1.0,
|
||
}
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def _handle_deprecated_dp_rank(cls, values):
|
||
return _migrate_deprecated_dp_rank(values)
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def set_tool_choice_default(cls, values):
|
||
if values.get("tool_choice") is None:
|
||
if values.get("tools") is None:
|
||
values["tool_choice"] = "none"
|
||
else:
|
||
values["tool_choice"] = "auto"
|
||
return values
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def normalize_reasoning_inputs(cls, values: Dict):
|
||
r = values.get("reasoning")
|
||
thinking = None
|
||
|
||
if r is not None and isinstance(r, dict):
|
||
effort = r.get("effort") or r.get("reasoning_effort")
|
||
if effort in {"none", "low", "medium", "high"}:
|
||
values["reasoning_effort"] = effort
|
||
|
||
enabled = (
|
||
r.get("enabled")
|
||
if r.get("enabled") is not None
|
||
else r.get("enable", False)
|
||
)
|
||
if isinstance(enabled, str):
|
||
enabled = enabled.strip().lower() in {"1", "true", "yes", "y", "on"}
|
||
if enabled:
|
||
thinking = True
|
||
|
||
effort = values.get("reasoning_effort")
|
||
if effort is not None:
|
||
thinking = effort != "none"
|
||
|
||
if thinking is not None:
|
||
ctk = values.get("chat_template_kwargs")
|
||
if not isinstance(ctk, dict):
|
||
ctk = {}
|
||
# different models check different keys:
|
||
# - "thinking" for deepseek-v3, kimi_k2
|
||
# - "enable_thinking" for qwen3, glm45, nemotron_3, interns1
|
||
ctk.setdefault("thinking", thinking)
|
||
ctk.setdefault("enable_thinking", thinking)
|
||
values["chat_template_kwargs"] = ctk
|
||
|
||
return values
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def set_json_schema(cls, values):
|
||
response_format = values.get("response_format")
|
||
if not response_format:
|
||
return values
|
||
|
||
if response_format.get("type") != "json_schema":
|
||
return values
|
||
|
||
schema = response_format.pop("schema", None)
|
||
json_schema = response_format.get("json_schema")
|
||
|
||
if json_schema:
|
||
return values
|
||
|
||
if schema:
|
||
name_ = schema.get("title", "Schema")
|
||
strict_ = False
|
||
if "properties" in schema and "strict" in schema["properties"]:
|
||
item = schema["properties"].pop("strict", None)
|
||
if item and item.get("default", False):
|
||
strict_ = True
|
||
|
||
response_format["json_schema"] = {
|
||
"name": name_,
|
||
"schema": schema,
|
||
"strict": strict_,
|
||
}
|
||
|
||
return values
|
||
|
||
def to_sampling_params(
|
||
self,
|
||
stop: List[str],
|
||
model_generation_config: Dict[str, Any],
|
||
tool_call_constraint: Optional[ToolCallConstraint] = None,
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Convert request to sampling parameters.
|
||
Priority: user value > model generation_config > OpenAI defaults
|
||
"""
|
||
|
||
def get_param(param_name: str):
|
||
value = getattr(self, param_name)
|
||
if value is None:
|
||
return model_generation_config.get(
|
||
param_name, self._DEFAULT_SAMPLING_PARAMS[param_name]
|
||
)
|
||
return value
|
||
|
||
# add per user request
|
||
spaces_between_special_tokens = (
|
||
True
|
||
if self.chat_template_kwargs is None
|
||
else self.chat_template_kwargs.get("spaces_between_special_tokens", True)
|
||
)
|
||
|
||
sampling_params = {
|
||
"temperature": get_param("temperature"),
|
||
"max_new_tokens": self.max_completion_tokens or self.max_tokens,
|
||
"min_new_tokens": self.min_tokens,
|
||
"stop": stop,
|
||
"stop_token_ids": self.stop_token_ids,
|
||
"stop_regex": self.stop_regex,
|
||
"top_p": get_param("top_p"),
|
||
"top_k": get_param("top_k"),
|
||
"min_p": get_param("min_p"),
|
||
"presence_penalty": self.presence_penalty,
|
||
"frequency_penalty": self.frequency_penalty,
|
||
"repetition_penalty": get_param("repetition_penalty"),
|
||
"regex": self.regex,
|
||
"ebnf": self.ebnf,
|
||
"n": self.n,
|
||
"no_stop_trim": self.no_stop_trim,
|
||
"ignore_eos": self.ignore_eos,
|
||
"skip_special_tokens": self.skip_special_tokens,
|
||
"logit_bias": self.logit_bias,
|
||
"custom_params": self.custom_params,
|
||
"sampling_seed": self.seed,
|
||
"spaces_between_special_tokens": spaces_between_special_tokens,
|
||
}
|
||
|
||
if self.response_format and self.response_format.type == "json_schema":
|
||
sampling_params["json_schema"] = convert_json_schema_to_str(
|
||
self.response_format.json_schema.schema_
|
||
)
|
||
elif self.response_format and self.response_format.type == "json_object":
|
||
sampling_params["json_schema"] = '{"type": "object"}'
|
||
elif self.response_format and self.response_format.type == "structural_tag":
|
||
sampling_params["structural_tag"] = convert_json_schema_to_str(
|
||
self.response_format.model_dump(by_alias=True)
|
||
)
|
||
|
||
# Check if there are already existing output constraints
|
||
has_existing_constraints = (
|
||
sampling_params.get("regex")
|
||
or sampling_params.get("ebnf")
|
||
or sampling_params.get("structural_tag")
|
||
or sampling_params.get("json_schema")
|
||
)
|
||
|
||
if tool_call_constraint and has_existing_constraints:
|
||
logger.warning("Constrained decoding is not compatible with tool calls.")
|
||
elif tool_call_constraint:
|
||
constraint_type, constraint_value = tool_call_constraint
|
||
if constraint_type == "structural_tag":
|
||
sampling_params[constraint_type] = convert_json_schema_to_str(
|
||
constraint_value.model_dump(by_alias=True)
|
||
)
|
||
elif constraint_type == "json_schema":
|
||
sampling_params[constraint_type] = convert_json_schema_to_str(
|
||
constraint_value # type: ignore
|
||
)
|
||
else:
|
||
sampling_params[constraint_type] = constraint_value
|
||
|
||
return sampling_params
|
||
|
||
|
||
class ChatMessage(BaseModel):
|
||
role: Optional[str] = None
|
||
content: Optional[str] = None
|
||
reasoning_content: Optional[str] = None
|
||
tool_calls: Optional[List[ToolCall]] = Field(default=None, examples=[None])
|
||
|
||
|
||
class ChatCompletionResponseChoice(BaseModel):
|
||
index: int
|
||
message: ChatMessage
|
||
logprobs: Optional[Union[LogProbs, ChoiceLogprobs]] = None
|
||
finish_reason: Optional[
|
||
Literal[
|
||
"stop", "length", "tool_calls", "content_filter", "function_call", "abort"
|
||
]
|
||
] = None
|
||
matched_stop: Union[None, int, str] = None
|
||
hidden_states: Optional[object] = None
|
||
prompt_token_ids: Optional[List[int]] = None
|
||
meta_info: Optional[Dict[str, Any]] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.hidden_states is None:
|
||
data.pop("hidden_states", None)
|
||
if self.prompt_token_ids is None:
|
||
data.pop("prompt_token_ids", None)
|
||
if self.meta_info is None:
|
||
data.pop("meta_info", None)
|
||
return data
|
||
|
||
|
||
class ChatCompletionResponse(BaseModel):
|
||
id: str
|
||
object: str = "chat.completion"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
choices: List[ChatCompletionResponseChoice]
|
||
usage: UsageInfo
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
sglext: Optional[SglExt] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.sglext is None:
|
||
data.pop("sglext", None)
|
||
return data
|
||
|
||
|
||
class DeltaMessage(BaseModel):
|
||
role: Optional[str] = None
|
||
content: Optional[str] = None
|
||
reasoning_content: Optional[str] = None
|
||
tool_calls: Optional[List[ToolCall]] = Field(default=None, examples=[None])
|
||
hidden_states: Optional[object] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.hidden_states is None:
|
||
data.pop("hidden_states", None)
|
||
return data
|
||
|
||
|
||
class ChatCompletionResponseStreamChoice(BaseModel):
|
||
index: int
|
||
delta: DeltaMessage
|
||
logprobs: Optional[Union[LogProbs, ChoiceLogprobs]] = None
|
||
finish_reason: Optional[
|
||
Literal[
|
||
"stop", "length", "tool_calls", "content_filter", "function_call", "abort"
|
||
]
|
||
] = None
|
||
matched_stop: Union[None, int, str] = None
|
||
|
||
|
||
class ChatCompletionStreamResponse(BaseModel):
|
||
id: str
|
||
object: str = "chat.completion.chunk"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
choices: List[ChatCompletionResponseStreamChoice]
|
||
usage: Optional[UsageInfo] = None
|
||
sglext: Optional[SglExt] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
if self.sglext is None:
|
||
data.pop("sglext", None)
|
||
return data
|
||
|
||
|
||
class MultimodalEmbeddingInput(BaseModel):
|
||
text: Optional[str] = None
|
||
image: Optional[str] = None
|
||
video: Optional[str] = None
|
||
|
||
|
||
EmbeddingInput = Union[
|
||
List[int], List[List[int]], str, List[str], List[MultimodalEmbeddingInput]
|
||
]
|
||
|
||
|
||
class EmbeddingRequest(BaseModel):
|
||
# Ordered by official OpenAI API documentation
|
||
# https://platform.openai.com/docs/api-reference/embeddings/create
|
||
input: EmbeddingInput
|
||
model: str = DEFAULT_MODEL_NAME
|
||
encoding_format: str = "float"
|
||
dimensions: Optional[int] = None
|
||
user: Optional[str] = None
|
||
|
||
# The request id.
|
||
rid: Optional[Union[List[str], str]] = None
|
||
# Priority for the request
|
||
priority: Optional[int] = None
|
||
# LoRA adapter path(s)
|
||
lora_path: Optional[Union[List[Optional[str]], Optional[str]]] = None
|
||
# Placeholder token id used to locate embedding override positions in input token IDs.
|
||
embed_override_token_id: Optional[int] = None
|
||
# Per-input embedding overrides (null entries skip that input).
|
||
# Shape: [num_inputs][num_replacements][hidden_size]
|
||
embed_overrides: Optional[List[Optional[List[List[float]]]]] = None
|
||
|
||
|
||
class EmbeddingObject(BaseModel):
|
||
embedding: List[float]
|
||
index: int
|
||
object: str = "embedding"
|
||
|
||
|
||
ClassifyInput = Union[str, List[str], List[int]]
|
||
|
||
|
||
class ClassifyRequest(BaseModel):
|
||
# OpenAI-compatible classification request
|
||
model: str = DEFAULT_MODEL_NAME
|
||
input: ClassifyInput
|
||
user: Optional[str] = None
|
||
|
||
# The request id.
|
||
rid: Optional[Union[List[str], str]] = None
|
||
# Priority for the request
|
||
priority: Optional[int] = None
|
||
|
||
|
||
class ClassifyData(BaseModel):
|
||
index: int
|
||
label: str
|
||
probs: List[float]
|
||
num_classes: int
|
||
|
||
|
||
class ClassifyResponse(BaseModel):
|
||
id: str
|
||
object: str = "list"
|
||
created: int
|
||
model: str
|
||
data: List[ClassifyData]
|
||
usage: UsageInfo
|
||
|
||
|
||
class EmbeddingResponse(BaseModel):
|
||
data: List[EmbeddingObject]
|
||
model: str
|
||
object: str = "list"
|
||
usage: Optional[UsageInfo] = None
|
||
|
||
|
||
class ScoringRequest(BaseModel):
|
||
query: Optional[Union[str, List[int]]] = (
|
||
None # Query text or pre-tokenized token IDs
|
||
)
|
||
items: Optional[Union[str, List[str], List[List[int]]]] = (
|
||
None # Item text(s) or pre-tokenized token IDs
|
||
)
|
||
# Placeholder token id used to locate embedding override positions in query/items.
|
||
embed_override_token_id: Optional[int] = None
|
||
# Query embedding overrides.
|
||
query_embed_overrides: Optional[List[List[float]]] = (
|
||
None # [num_query_embed_overrides][hidden_size]
|
||
)
|
||
# Per-item embedding overrides (null entries skip that item).
|
||
item_embed_overrides: Optional[List[Optional[List[List[float]]]]] = (
|
||
None # [num_items][num_item_embed_overrides][hidden_size]
|
||
)
|
||
label_token_ids: Optional[List[int]] = (
|
||
None # Token IDs to compute probabilities for
|
||
)
|
||
apply_softmax: bool = False
|
||
item_first: bool = False
|
||
return_pooled_hidden_states: bool = False
|
||
model: str = DEFAULT_MODEL_NAME
|
||
|
||
|
||
class ScoringResponse(BaseModel):
|
||
scores: List[
|
||
List[float]
|
||
] # List of lists of probabilities, each in the order of label_token_ids
|
||
pooled_hidden_states: Optional[List[Optional[List[float]]]] = None
|
||
model: str
|
||
usage: Optional[UsageInfo] = None
|
||
object: str = "scoring"
|
||
|
||
|
||
class V1RerankReqInput(BaseModel):
|
||
query: RerankContent = Field(
|
||
...,
|
||
description="The query to match against documents. Can be a string (text-only) "
|
||
"or a list of content parts for multimodal queries (text, image_url, video_url).",
|
||
)
|
||
documents: List[RerankContent] = Field(
|
||
...,
|
||
description="List of documents to rank. Each document can be a string (text-only) "
|
||
"or a list of content parts for multimodal documents (text, image_url, video_url).",
|
||
)
|
||
instruct: Optional[str] = Field(
|
||
default=None,
|
||
description="The instruct to the reranker model.",
|
||
)
|
||
top_n: Optional[int] = Field(
|
||
default=None,
|
||
description="Maximum number of documents to return. Defaults to returning all documents. "
|
||
"If specified value is greater than the total number of documents, all documents will be returned.",
|
||
)
|
||
return_documents: bool = Field(
|
||
default=True,
|
||
description="Whether to return documents in the response. Only included when set to true.",
|
||
)
|
||
|
||
@field_validator("top_n")
|
||
@classmethod
|
||
def validate_top_n(cls, v):
|
||
if v is not None and v < 1:
|
||
raise ValueError("Value error, parameter top_n should be larger than 0.")
|
||
return v
|
||
|
||
def is_multimodal(self) -> bool:
|
||
"""Check if the request contains any multimodal content."""
|
||
if isinstance(self.query, list):
|
||
return True
|
||
for doc in self.documents:
|
||
if isinstance(doc, list):
|
||
return True
|
||
return False
|
||
|
||
|
||
class RerankResponse(BaseModel):
|
||
score: float
|
||
document: Optional[str] = None
|
||
index: int
|
||
meta_info: Optional[dict] = None
|
||
|
||
@model_serializer(mode="wrap")
|
||
def _serialize(self, handler):
|
||
data = handler(self)
|
||
# Exclude document field if it's None
|
||
if self.document is None:
|
||
data.pop("document", None)
|
||
return data
|
||
|
||
|
||
class TokenizeRequest(BaseModel):
|
||
"""Request schema for the /tokenize endpoint."""
|
||
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
model: str = DEFAULT_MODEL_NAME
|
||
prompt: Optional[Union[str, List[str]]] = None
|
||
messages: Optional[List[ChatCompletionMessageParam]] = None
|
||
tools: Optional[List[Tool]] = Field(default=None, examples=[None])
|
||
tool_choice: Optional[Union[ToolChoice, Literal["auto", "required", "none"]]] = (
|
||
Field(default=None, examples=["auto"])
|
||
)
|
||
reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None
|
||
continue_final_message: bool = False
|
||
chat_template_kwargs: Optional[Dict] = None
|
||
add_special_tokens: bool = Field(
|
||
default=True,
|
||
description="whether to add model-specific special tokens (e.g. BOS/EOS) during encoding.",
|
||
)
|
||
|
||
@model_validator(mode="after")
|
||
def validate_tokenize_input(self) -> TokenizeRequest:
|
||
if (self.prompt is None) == (self.messages is None):
|
||
raise ValueError("Exactly one of 'prompt' or 'messages' must be provided.")
|
||
return self
|
||
|
||
def to_chat_completion_request(self) -> ChatCompletionRequest:
|
||
data = self.model_dump(
|
||
exclude={"prompt", "add_special_tokens"},
|
||
exclude_none=True,
|
||
)
|
||
extra = getattr(self, "__pydantic_extra__", None)
|
||
if extra:
|
||
data.update(extra)
|
||
return ChatCompletionRequest.model_validate(data)
|
||
|
||
|
||
class TokenizeResponse(BaseModel):
|
||
"""Response schema for the /tokenize endpoint."""
|
||
|
||
tokens: Union[List[int], List[List[int]]]
|
||
count: Union[int, List[int]]
|
||
max_model_len: int
|
||
|
||
|
||
class DetokenizeRequest(BaseModel):
|
||
"""Request schema for the /detokenize endpoint."""
|
||
|
||
model: str = DEFAULT_MODEL_NAME
|
||
tokens: Union[List[int], List[List[int]]]
|
||
skip_special_tokens: bool = Field(
|
||
default=True,
|
||
description="whether to exclude special tokens (e.g. padding or EOS) during decoding.",
|
||
)
|
||
|
||
|
||
class DetokenizeResponse(BaseModel):
|
||
"""Response schema for the /detokenize endpoint."""
|
||
|
||
text: Union[str, List[str]]
|
||
|
||
|
||
OpenAIServingRequest = Union[
|
||
ChatCompletionRequest,
|
||
CompletionRequest,
|
||
EmbeddingRequest,
|
||
ClassifyRequest,
|
||
ScoringRequest,
|
||
V1RerankReqInput,
|
||
TokenizeRequest,
|
||
DetokenizeRequest,
|
||
]
|
||
|
||
|
||
# Response API protocol definitions
|
||
class ResponseReasoningParam(BaseModel):
|
||
"""Reasoning parameters for responses."""
|
||
|
||
effort: Optional[Literal["low", "medium", "high"]] = Field(
|
||
default="medium",
|
||
description="Constrains effort on reasoning for reasoning models.",
|
||
)
|
||
summary: Optional[Literal["auto", "concise", "detailed"]] = Field(
|
||
default=None,
|
||
description="Include a summary of the model's reasoning trace on the response.",
|
||
)
|
||
|
||
|
||
# Only ``function`` / ``web_search*`` / ``code_interpreter`` are wired to
|
||
# execution paths; the rest pass validation so clients aren't rejected.
|
||
RESPONSE_TOOL_TYPES = Literal[
|
||
"function",
|
||
"web_search",
|
||
"web_search_preview",
|
||
"code_interpreter",
|
||
"file_search",
|
||
"image_generation",
|
||
"computer_use_preview",
|
||
"local_shell",
|
||
"mcp",
|
||
"custom",
|
||
"namespace",
|
||
"tool_search",
|
||
]
|
||
|
||
|
||
class ResponseTool(BaseModel):
|
||
"""Tool definition for responses."""
|
||
|
||
type: RESPONSE_TOOL_TYPES = Field(description="Type of tool to enable")
|
||
name: Optional[str] = None
|
||
description: Optional[str] = None
|
||
parameters: Optional[Dict[str, Any]] = None
|
||
strict: bool = False
|
||
# Inner schemas for ``namespace`` tools.
|
||
tools: Optional[List[Dict[str, Any]]] = None
|
||
|
||
@model_validator(mode="after")
|
||
def validate_function_tool(self) -> ResponseTool:
|
||
if self.type == "function" and not self.name:
|
||
raise ValueError("Function tools must include a name.")
|
||
return self
|
||
|
||
|
||
ResponseInputOutputItem: TypeAlias = Union[
|
||
ResponseInputItemParam,
|
||
"ResponseReasoningItem",
|
||
ResponseFunctionToolCall,
|
||
]
|
||
|
||
|
||
class ResponsesRequest(BaseModel):
|
||
"""Request body for v1/responses endpoint."""
|
||
|
||
# Core OpenAI API fields (ordered by official documentation)
|
||
background: Optional[bool] = False
|
||
include: Optional[
|
||
List[
|
||
Literal[
|
||
"code_interpreter_call.outputs",
|
||
"computer_call_output.output.image_url",
|
||
"file_search_call.results",
|
||
"message.input_image.image_url",
|
||
"message.output_text.logprobs",
|
||
"reasoning.encrypted_content",
|
||
]
|
||
]
|
||
] = None
|
||
# Accept dict-shaped items as the loose arm; downstream normalization
|
||
# handles replayed shapes that don't satisfy every openai TypedDict.
|
||
input: Union[str, List[ResponseInputOutputItem], List[Dict[str, Any]]]
|
||
instructions: Optional[str] = None
|
||
max_output_tokens: Optional[int] = None
|
||
max_tool_calls: Optional[int] = None
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
model: Optional[str] = None # Made optional to match vLLM
|
||
parallel_tool_calls: Optional[bool] = True
|
||
previous_response_id: Optional[str] = None
|
||
reasoning: Optional[ResponseReasoningParam] = None
|
||
service_tier: Literal["auto", "default", "flex", "scale", "priority"] = "auto"
|
||
store: Optional[bool] = True
|
||
stream: Optional[bool] = False
|
||
temperature: Optional[float] = None
|
||
tool_choice: Literal["auto", "required", "none"] = "auto"
|
||
tools: List[ResponseTool] = Field(default_factory=list)
|
||
top_logprobs: Optional[int] = 0
|
||
top_p: Optional[float] = None
|
||
truncation: Optional[Literal["auto", "disabled"]] = "disabled"
|
||
user: Optional[str] = None
|
||
|
||
# Extra SGLang parameters
|
||
request_id: str = Field(
|
||
default_factory=lambda: f"resp_{uuid.uuid4().hex}",
|
||
description="The request_id related to this request. If the caller does not set it, a random uuid will be generated.",
|
||
)
|
||
session_id: Optional[str] = None
|
||
priority: int = Field(default=0, description="Request priority")
|
||
extra_key: Optional[str] = Field(
|
||
default=None,
|
||
description="Extra key for classifying the request (e.g. cache_salt)",
|
||
)
|
||
cache_salt: Optional[str] = Field(
|
||
default=None, description="Cache salt for request caching"
|
||
)
|
||
|
||
# SGLang sampling extras. ``None`` defers to ``--preferred-sampling-params``.
|
||
frequency_penalty: float = 0.0
|
||
presence_penalty: float = 0.0
|
||
stop: Optional[Union[str, List[str]]] = None
|
||
top_k: Optional[int] = None
|
||
min_p: Optional[float] = None
|
||
repetition_penalty: Optional[float] = None
|
||
|
||
# Default sampling parameters
|
||
_DEFAULT_SAMPLING_PARAMS = {
|
||
"temperature": 0.7,
|
||
"top_p": 1.0,
|
||
"top_k": -1,
|
||
"min_p": 0.0,
|
||
"repetition_penalty": 1.0,
|
||
}
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def normalize_responses_input(cls, values):
|
||
if not isinstance(values, dict):
|
||
return values
|
||
|
||
input_value = values.get("input")
|
||
if not isinstance(input_value, list):
|
||
return values
|
||
|
||
values = values.copy()
|
||
values["input"] = [
|
||
cls._normalize_input_item_for_validation(item) for item in input_value
|
||
]
|
||
return values
|
||
|
||
@staticmethod
|
||
def _normalize_input_item_for_validation(item):
|
||
if not isinstance(item, dict):
|
||
return item
|
||
|
||
content = item.get("content")
|
||
if not isinstance(content, list):
|
||
return item
|
||
|
||
item = item.copy()
|
||
item["content"] = [
|
||
ResponsesRequest._normalize_content_part_for_validation(part)
|
||
for part in content
|
||
]
|
||
return item
|
||
|
||
@staticmethod
|
||
def _normalize_content_part_for_validation(part):
|
||
if not isinstance(part, dict):
|
||
return part
|
||
|
||
part_type = part.get("type")
|
||
if part_type != "input_image" or part.get("detail") is not None:
|
||
return part
|
||
|
||
part = part.copy()
|
||
part["detail"] = "auto"
|
||
return part
|
||
|
||
def to_sampling_params(
|
||
self,
|
||
default_max_tokens: int,
|
||
default_params: Optional[Dict] = None,
|
||
stop: Optional[Union[str, List[str]]] = None,
|
||
tool_call_constraint: Optional[ToolCallConstraint] = None,
|
||
) -> Dict[str, Any]:
|
||
"""Convert to sampling parameters for generation."""
|
||
if default_params is None:
|
||
default_params = {}
|
||
|
||
# Use max_output_tokens if available, otherwise use max_tokens for backwards compatibility
|
||
if self.max_output_tokens is not None:
|
||
max_tokens = min(self.max_output_tokens, default_max_tokens)
|
||
else:
|
||
max_tokens = default_max_tokens
|
||
|
||
# Headroom for BOS/EOS the engine appends on top of prompt+budget.
|
||
max_tokens -= 2
|
||
|
||
temperature = self.temperature
|
||
if temperature is None:
|
||
temperature = default_params.get(
|
||
"temperature", self._DEFAULT_SAMPLING_PARAMS["temperature"]
|
||
)
|
||
|
||
top_p = self.top_p
|
||
if top_p is None:
|
||
top_p = default_params.get("top_p", self._DEFAULT_SAMPLING_PARAMS["top_p"])
|
||
|
||
# Omit None entries so they fall through to ``--preferred-sampling-params``
|
||
# rather than overriding it with a literal default.
|
||
params: dict[str, Any] = {
|
||
"max_new_tokens": max_tokens,
|
||
"temperature": temperature,
|
||
"top_p": top_p,
|
||
"frequency_penalty": self.frequency_penalty,
|
||
"presence_penalty": self.presence_penalty,
|
||
"stop": self.stop if stop is None else stop,
|
||
}
|
||
if self.top_k is not None:
|
||
params["top_k"] = self.top_k
|
||
if self.min_p is not None:
|
||
params["min_p"] = self.min_p
|
||
if self.repetition_penalty is not None:
|
||
params["repetition_penalty"] = self.repetition_penalty
|
||
|
||
# Apply any additional default parameters
|
||
for key, value in default_params.items():
|
||
if key not in params or params[key] is None:
|
||
params[key] = value
|
||
|
||
has_existing_constraints = (
|
||
params.get("regex")
|
||
or params.get("ebnf")
|
||
or params.get("structural_tag")
|
||
or params.get("json_schema")
|
||
)
|
||
if tool_call_constraint and has_existing_constraints:
|
||
# Refuse rather than silently drop the tool-call grammar.
|
||
raise ValueError(
|
||
"Cannot combine tool calls with constrained decoding "
|
||
"(regex / ebnf / structural_tag / json_schema). Remove one."
|
||
)
|
||
if tool_call_constraint:
|
||
constraint_type, constraint_value = tool_call_constraint
|
||
if constraint_type in ("structural_tag", "json_schema"):
|
||
params[constraint_type] = convert_json_schema_to_str(
|
||
constraint_value.model_dump(by_alias=True)
|
||
if hasattr(constraint_value, "model_dump")
|
||
else constraint_value
|
||
)
|
||
else:
|
||
params[constraint_type] = constraint_value
|
||
|
||
return params
|
||
|
||
|
||
class PromptTokenUsageInfo(BaseModel):
|
||
"""Prompt token usage details."""
|
||
|
||
cached_tokens: int = 0
|
||
|
||
|
||
class ResponsesResponse(BaseModel):
|
||
"""Response body for v1/responses endpoint."""
|
||
|
||
id: str = Field(default_factory=lambda: f"resp_{time.time()}")
|
||
object: Literal["response"] = "response"
|
||
created_at: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
|
||
output: List[
|
||
Union[ResponseOutputItem, ResponseReasoningItem, ResponseFunctionToolCall]
|
||
] = Field(default_factory=list)
|
||
status: Literal["queued", "in_progress", "completed", "failed", "cancelled"]
|
||
usage: Optional[UsageInfo] = None
|
||
parallel_tool_calls: bool = True
|
||
tool_choice: str = "auto"
|
||
tools: List[ResponseTool] = Field(default_factory=list)
|
||
|
||
# OpenAI compatibility fields. not all are used at the moment.
|
||
# Recommend checking https://platform.openai.com/docs/api-reference/responses
|
||
error: Optional[dict] = None
|
||
incomplete_details: Optional[dict] = None # TODO(v) support this input
|
||
instructions: Optional[str] = None
|
||
max_output_tokens: Optional[int] = None
|
||
previous_response_id: Optional[str] = None
|
||
reasoning: Optional[dict] = (
|
||
# Unused. No model supports this. For GPT-oss, system prompt sets
|
||
# the field, not server args.
|
||
None # {"effort": Optional[str], "summary": Optional[str]}
|
||
)
|
||
store: Optional[bool] = None
|
||
temperature: Optional[float] = None
|
||
text: Optional[dict] = None # e.g. {"format": {"type": "text"}}
|
||
top_p: Optional[float] = None
|
||
truncation: Optional[str] = None
|
||
user: Optional[str] = None
|
||
metadata: Optional[Dict[str, Any]] = None
|
||
|
||
@classmethod
|
||
def from_request(
|
||
cls,
|
||
request: ResponsesRequest,
|
||
sampling_params: Any,
|
||
model_name: str,
|
||
created_time: int,
|
||
output: List[
|
||
Union[ResponseOutputItem, ResponseReasoningItem, ResponseFunctionToolCall]
|
||
],
|
||
status: str,
|
||
usage: Optional[UsageInfo],
|
||
) -> ResponsesResponse:
|
||
"""Create a response from a request."""
|
||
|
||
# Determine if the output is plain text only to set text.format
|
||
def _is_text_only(
|
||
items: List[
|
||
Union[
|
||
ResponseOutputItem, ResponseReasoningItem, ResponseFunctionToolCall
|
||
]
|
||
],
|
||
) -> bool:
|
||
if not items:
|
||
return False
|
||
for it in items:
|
||
# tool call -> not pure text.
|
||
if isinstance(it, ResponseReasoningItem) or isinstance(
|
||
it, ResponseFunctionToolCall
|
||
):
|
||
return False
|
||
try:
|
||
if isinstance(it, ResponseOutputText):
|
||
continue
|
||
elif isinstance(it, ResponseOutputMessage):
|
||
if not it.content:
|
||
continue
|
||
for c in it.content:
|
||
if not isinstance(c, ResponseOutputText):
|
||
return False
|
||
else:
|
||
# Unknown type, not considered text-only
|
||
return False
|
||
except AttributeError:
|
||
return False
|
||
return True
|
||
|
||
text_format = {"format": {"type": "text"}} if _is_text_only(output) else None
|
||
|
||
return cls(
|
||
id=request.request_id,
|
||
created_at=created_time,
|
||
model=model_name,
|
||
output=output,
|
||
status=status,
|
||
usage=usage,
|
||
parallel_tool_calls=(
|
||
request.parallel_tool_calls
|
||
if request.parallel_tool_calls is not None
|
||
else True
|
||
),
|
||
tool_choice=request.tool_choice,
|
||
tools=request.tools,
|
||
# fields for parity with v1/responses
|
||
error=None,
|
||
incomplete_details=None,
|
||
instructions=request.instructions,
|
||
max_output_tokens=request.max_output_tokens,
|
||
previous_response_id=request.previous_response_id, # TODO(v): ensure this is propagated if retrieved from store
|
||
reasoning={
|
||
"effort": request.reasoning.effort if request.reasoning else None,
|
||
"summary": None, # unused
|
||
},
|
||
store=request.store,
|
||
temperature=request.temperature,
|
||
text=text_format, # TODO(v): Expand coverage per https://platform.openai.com/docs/api-reference/responses/list
|
||
top_p=request.top_p,
|
||
truncation=request.truncation,
|
||
user=request.user,
|
||
metadata=request.metadata or {},
|
||
)
|
||
|
||
|
||
class RequestResponseMetadata(BaseModel):
|
||
"""Metadata for request/response tracking."""
|
||
|
||
request_id: str
|
||
final_usage_info: Optional[UsageInfo] = None
|
||
|
||
|
||
@dataclass
|
||
class MessageProcessingResult:
|
||
"""Result of processing chat messages and applying templates.
|
||
|
||
This dataclass encapsulates all the outputs from message processing including
|
||
prompt generation, multimodal data extraction, and constraint preparation.
|
||
Used internally by OpenAIServingChat to pass processed data between methods.
|
||
|
||
Args:
|
||
prompt: The final text prompt after applying chat template
|
||
prompt_ids: Either the text prompt (str) or tokenized IDs (List[int])
|
||
image_data: Extracted image data from messages, if any
|
||
audio_data: Extracted audio data from messages, if any
|
||
modalities: List of modality types present in the messages
|
||
stop: Combined stop strings from template and request
|
||
tool_call_constraint: Optional constraint for structured tool calls
|
||
"""
|
||
|
||
prompt: str
|
||
prompt_ids: Union[str, List[int]]
|
||
image_data: Optional[Any]
|
||
audio_data: Optional[Any]
|
||
video_data: Optional[Any]
|
||
modalities: List[str]
|
||
stop: List[str]
|
||
tool_call_constraint: Optional[ToolCallConstraint] = None
|
||
|
||
|
||
class ToolCallProcessingResult(NamedTuple):
|
||
"""Result of processing tool calls in a response."""
|
||
|
||
tool_calls: Optional[
|
||
List[Any]
|
||
] # List of ToolCall objects or None if parsing failed
|
||
remaining_text: str # Text remaining after parsing tool calls
|
||
finish_reason: Dict[str, Any] # Updated finish reason dictionary
|
||
|
||
|
||
class ResponseReasoningTextContent(BaseModel):
|
||
text: str
|
||
type: Literal["reasoning_text"] = "reasoning_text"
|
||
|
||
|
||
ResponseInputOutputItem: TypeAlias = Union[
|
||
ResponseInputItemParam, "ResponseReasoningItem", ResponseFunctionToolCall
|
||
]
|
||
|
||
|
||
# ================== Transcription API Protocol Definitions ==================
|
||
|
||
|
||
class TranscriptionRequest(BaseModel):
|
||
"""Request model for audio transcription (OpenAI-compatible)."""
|
||
|
||
model: str = DEFAULT_MODEL_NAME
|
||
language: Optional[str] = None
|
||
response_format: str = "json"
|
||
temperature: float = 0.0
|
||
timestamp_granularities: Optional[List[str]] = None
|
||
stream: bool = False
|
||
# Internal fields (not from API)
|
||
audio_data: Optional[bytes] = None
|
||
audio_duration_s: float = 0.0
|
||
|
||
|
||
class TranscriptionUsage(BaseModel):
|
||
"""Usage info for transcription response (duration-based)."""
|
||
|
||
type: Literal["duration"] = "duration"
|
||
seconds: int # Audio duration in seconds (rounded up)
|
||
|
||
|
||
class TranscriptionResponse(BaseModel):
|
||
"""Non-streaming transcription response (OpenAI-compatible)."""
|
||
|
||
text: str
|
||
usage: Optional[TranscriptionUsage] = None
|
||
|
||
|
||
class TranscriptionSegment(BaseModel):
|
||
"""A segment with timestamp information."""
|
||
|
||
id: int
|
||
start: float
|
||
end: float
|
||
text: str
|
||
|
||
|
||
class TranscriptionVerboseResponse(BaseModel):
|
||
"""Verbose transcription response with timestamps (OpenAI-compatible)."""
|
||
|
||
task: str = "transcribe"
|
||
language: Optional[str] = None
|
||
duration: Optional[float] = None
|
||
text: str
|
||
segments: List[TranscriptionSegment] = []
|
||
usage: Optional[TranscriptionUsage] = None
|
||
|
||
|
||
class TranscriptionStreamChoice(BaseModel):
|
||
"""Delta content for streaming transcription."""
|
||
|
||
delta: DeltaMessage
|
||
finish_reason: Optional[str] = None
|
||
|
||
|
||
class TranscriptionStreamResponse(BaseModel):
|
||
"""Streaming transcription chunk (OpenAI-compatible)."""
|
||
|
||
id: str = Field(default_factory=lambda: f"trsc-{uuid.uuid4().hex}")
|
||
object: Literal["transcription.chunk"] = "transcription.chunk"
|
||
created: int = Field(default_factory=lambda: int(time.time()))
|
||
model: str
|
||
choices: List[TranscriptionStreamChoice]
|
||
usage: Optional[UsageInfo] = None
|