mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
* fix(model-management): honor an explicit null as a clear on model update
PATCH /model/{model_id}/update merged the patch with exclude_none and then
popped explicit nulls only for the mirrored pricing fields, so a null sent for
max_input_tokens, mode, supports_vision or any other key was dropped and a value
pinned by an earlier save could never be removed.
The route now follows JSON Merge Patch over both blobs: a key absent from the
body is unchanged, a key sent as null is removed from the stored row, and a key
sent with a value is set. Ownership and identity keys keep ignoring a null, as
do the fields the stored models require, since clearing one writes a row no
reload can rebuild. Mirrored pricing keys still clear from both blobs.
Clearing a price also needed the router to stop merging a deployment's cost-map
entry onto its previous registration, which left the old rate in place and kept
billing at a price the deployment no longer carried.
Adds a create, read, partial-update, clear, enforce, delete lifecycle e2e that
reads back on every replica, and a harness helper for that read-back.
* fix(router): keep a deployment id that names a real model from evicting its catalog entry
Deployments are keyed into litellm.model_cost alongside the built-in catalog, so
evicting a deployment's stale entry by id could take a real model's entry with it:
registering a deployment whose model_info.id is "gpt-4o" stripped that model's
pricing, context window and capability flags process-wide, for every other
deployment of it, until the next price-map reload.
Only evict an entry this registration owns. A colliding id keeps the previous
merge, which pollutes the catalog entry rather than emptying it.
Also pins the Admin UI round trip: the model edit form echoes the whole /model/info
row back on save, and that read reports every key the deployment never stored as an
explicit null, so the clear path has to leave those keys alone.
* fix(router): decide cost-map eviction by what this registrar created
The previous guard read a catalog entry off `litellm_provider`, so a deployment
that declares its own provider in model_info was treated as one and kept billing
at a price it no longer carried. It also only held for a single registration: a
second one under a colliding id saw the id the first merge left behind and
evicted the catalog entry anyway.
Track the cost-map keys this registrar creates instead. A key it created is
evicted before re-registration; one it did not is left to merge, which is what a
deployment id colliding with a catalog model name needs.
Also folds the required-fields comment into the docstring that already gives the
reason.
* fix(router): release a deployment's cost-map key when it is deleted
The ownership ledger only grew. A deleted deployment kept its claim, so if a
later catalog refresh started publishing a model under that same name, the next
registration would treat the catalog entry as the deployment's own and evict it.
Deleting a deployment now gives the key back, which also stops the ledger
growing for the life of the process.
* fix(router): hold a cost-map key while another live router still serves it
The claim is process-wide but the release was per-deletion, so with two routers
serving one deployment id, the first deletion put the survivor back on merging
and the price it had just cleared would keep billing.
Release the key only once no live router still serves that id.
* fix(router): register a router in the live set when it gains a deployment
_live_routers was only joined when a router was constructed with a model_list,
but a router built empty is populated through add_deployment, and the empty
branch exists for exactly that. Such a router was invisible to the live-router
scan, so deleting the deployment from another router released the shared
cost-map key while it was still serving that id.
Joining the set where a deployment enters the list covers every path, and it
also lets a price reload rebuild what a dynamically built router serves.
* fix(e2e): read the stored model row from the control plane, not each gateway
The lifecycle suite polled /model/info on every URL in PROXY_REPLICA_URLS. Those
URLs are the stack's gateways, and gateway/routes/allowlist.py trims them to the
LLM data-plane surface, so /model/info answers only on the backend and 404s on
every replica. All five tests failed at their first read-back in CI while passing
against a monolith, where one process serves both planes.
The stored row has one answer behind it, so it is read through the shared
transport, which routes control-plane paths to the backend. What every gateway
must agree on is which models it serves, so the create and delete steps poll
/v1/models per replica instead, a route the gateway does serve.
read_back_everywhere now rejects a control-plane path outright rather than
timing out on it.
Two things surfaced behind that. /public/ was missing from the transport's
control-plane prefixes, so model_cost_map() was routed to a gateway and 404'd,
and the billing steps needed a data-plane wait: a PATCH lands on the backend and
each gateway picks it up on its own config reload, measured here at 12-24s, so
they now drive calls until the new rate reaches the spend row and let the
deadline fail them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1S92J8gSxxKVe1JBzxWBF
* test(models): keep polling outcomes immutable and document shared ownership
* test: validate opaque stream IDs and hide log-reader credentials
* test: isolate auto-router scenarios and clean partial setup
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1341 lines
37 KiB
Python
1341 lines
37 KiB
Python
"""Shared pydantic request/response models for the e2e gateway.
|
|
|
|
Only the fields the tests read are modelled; pydantic ignores the rest, so a
|
|
response validates without mirroring every proxy field. No untyped dicts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from datetime import datetime
|
|
from typing import Final, Literal
|
|
|
|
from e2e_http import PartialBody
|
|
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, RootModel, model_serializer, model_validator
|
|
|
|
# ---------- keys ----------
|
|
|
|
|
|
class ModelBudgetEntry(BaseModel):
|
|
budget_limit: float = Field(validation_alias=AliasChoices("budget_limit", "max_budget"))
|
|
time_period: str = Field(validation_alias=AliasChoices("time_period", "budget_duration"))
|
|
rpm_limit: int | None = None
|
|
tpm_limit: int | None = None
|
|
|
|
|
|
class BudgetWindow(BaseModel):
|
|
budget_duration: str
|
|
max_budget: float
|
|
|
|
|
|
class BudgetWindowState(BudgetWindow):
|
|
reset_at: datetime | None = None
|
|
|
|
|
|
class KeyLoggingCallbackVars(BaseModel):
|
|
langfuse_public_key: str | None = None
|
|
langfuse_secret_key: str | None = None
|
|
langfuse_host: str | None = None
|
|
wandb_api_key: str | None = None
|
|
weave_project_id: str | None = None
|
|
|
|
|
|
class KeyLoggingCallback(BaseModel):
|
|
callback_name: str
|
|
callback_type: str = "success_and_failure"
|
|
callback_vars: KeyLoggingCallbackVars
|
|
|
|
|
|
class KeyMetadata(BaseModel):
|
|
logging: list[KeyLoggingCallback] | None = None
|
|
priority: str | None = None
|
|
batch_enqueued_token_limit: int | None = None
|
|
tag: str | None = None
|
|
|
|
|
|
class ObjectPermission(BaseModel):
|
|
mcp_servers: list[str] | None = None
|
|
mcp_access_groups: list[str] | None = None
|
|
mcp_toolsets: list[str] | None = None
|
|
|
|
|
|
class KeyGenerateBody(BaseModel):
|
|
models: list[str] = []
|
|
duration: str | None = None
|
|
max_budget: float | None = None
|
|
soft_budget: float | None = None
|
|
budget_duration: str | None = None
|
|
user_id: str | None = None
|
|
team_id: str | None = None
|
|
organization_id: str | None = None
|
|
budget_id: str | None = None
|
|
key_alias: str | None = None
|
|
model_max_budget: dict[str, ModelBudgetEntry] | None = None
|
|
budget_fallbacks: dict[str, list[str]] | None = None
|
|
budget_limits: list[BudgetWindow] | None = None
|
|
tpm_limit: int | None = None
|
|
rpm_limit: int | None = None
|
|
allowed_routes: list[str] | None = None
|
|
allowed_passthrough_routes: list[str] | None = None
|
|
metadata: KeyMetadata | None = None
|
|
object_permission: ObjectPermission | None = None
|
|
router_settings: RouterSettingsOverride | None = None
|
|
|
|
|
|
class KeyGenerateResponse(BaseModel):
|
|
key: str
|
|
key_alias: str | None = None
|
|
models: list[str] = []
|
|
max_budget: float | None = None
|
|
tpm_limit: int | None = None
|
|
rpm_limit: int | None = None
|
|
budget_duration: str | None = None
|
|
team_id: str | None = None
|
|
metadata: KeyMetadata | None = None
|
|
|
|
|
|
class KeyRegenerateBody(BaseModel):
|
|
key: str
|
|
grace_period: str | None = None
|
|
|
|
|
|
class KeyResetSpendBody(BaseModel):
|
|
reset_to: float
|
|
|
|
|
|
class KeyResetSpendResponse(BaseModel):
|
|
spend: float
|
|
previous_spend: float
|
|
|
|
|
|
class KeyDeleteBody(BaseModel):
|
|
keys: list[str]
|
|
|
|
|
|
class KeyInfoParams(BaseModel):
|
|
key: str
|
|
|
|
|
|
class LiteLLMBudgetTable(BaseModel):
|
|
max_budget: float | None = None
|
|
soft_budget: float | None = None
|
|
budget_duration: str | None = None
|
|
budget_reset_at: str | None = None
|
|
|
|
|
|
class KeyInfo(BaseModel):
|
|
key_alias: str | None = None
|
|
metadata: KeyMetadata | None = None
|
|
models: list[str] = []
|
|
tpm_limit: int | None = None
|
|
rpm_limit: int | None = None
|
|
team_id: str | None = None
|
|
blocked: bool | None = None
|
|
spend: float | None = None
|
|
max_budget: float | None = None
|
|
budget_duration: str | None = None
|
|
budget_reset_at: str | None = None
|
|
budget_id: str | None = None
|
|
litellm_budget_table: LiteLLMBudgetTable | None = None
|
|
budget_limits: list[BudgetWindowState] | None = None
|
|
object_permission: ObjectPermission | None = None
|
|
|
|
|
|
class KeyInfoResponse(BaseModel):
|
|
info: KeyInfo
|
|
|
|
|
|
# ---------- customers ----------
|
|
|
|
|
|
class CustomerNewBody(BaseModel):
|
|
user_id: str
|
|
|
|
|
|
class CustomerResponse(BaseModel):
|
|
user_id: str | None = None
|
|
|
|
|
|
class CustomerInfoParams(BaseModel):
|
|
end_user_id: str
|
|
|
|
|
|
class CustomerDeleteBody(BaseModel):
|
|
user_ids: list[str]
|
|
|
|
|
|
# ---------- chat / embeddings ----------
|
|
|
|
|
|
class ChatMetadata(BaseModel):
|
|
tags: list[str] | None = None
|
|
|
|
|
|
class ImageUrl(BaseModel):
|
|
url: str
|
|
|
|
|
|
class TextContentPart(BaseModel):
|
|
type: str = "text"
|
|
text: str
|
|
|
|
|
|
class ImageContentPart(BaseModel):
|
|
type: str = "image_url"
|
|
image_url: ImageUrl
|
|
|
|
|
|
ContentPart = TextContentPart | ImageContentPart
|
|
|
|
|
|
class ChatMessage(BaseModel):
|
|
role: str
|
|
content: str | list[ContentPart]
|
|
|
|
|
|
class CacheControl(BaseModel):
|
|
type: str = "ephemeral"
|
|
ttl: str | None = None
|
|
|
|
|
|
class TextBlock(BaseModel):
|
|
type: str = "text"
|
|
text: str
|
|
cache_control: CacheControl | None = None
|
|
|
|
|
|
class RichMessage(BaseModel):
|
|
role: str
|
|
content: list[TextBlock]
|
|
|
|
|
|
class ThinkingParam(BaseModel):
|
|
"""Extended-thinking control shared by Anthropic and DeepSeek reasoner models.
|
|
DeepSeek accepts only ``type`` (enabled/disabled) and ignores budget_tokens;
|
|
Anthropic also honors budget_tokens. Sending ``type="disabled"`` is the
|
|
product-facing way a caller turns reasoning off (LIT-3686 / GH #27453)."""
|
|
|
|
type: Literal["enabled", "disabled"]
|
|
budget_tokens: int | None = None
|
|
|
|
|
|
class ChatToolFunction(BaseModel):
|
|
name: str
|
|
description: str | None = None
|
|
parameters: dict[str, object] | None = None
|
|
|
|
|
|
class ChatTool(BaseModel):
|
|
type: str = "function"
|
|
function: ChatToolFunction
|
|
|
|
|
|
class McpChatTool(BaseModel):
|
|
"""An MCP server attached to a chat completion (OpenAI `type: "mcp"` tool).
|
|
`server_url` selects the gateway-registered server by its alias suffix; with
|
|
`require_approval="never"` the gateway lists, calls, and feeds the server's
|
|
tools back to the model in one agentic turn."""
|
|
|
|
type: Literal["mcp"] = "mcp"
|
|
server_url: str
|
|
require_approval: str
|
|
server_label: str | None = None
|
|
allowed_tools: list[str] | None = None
|
|
|
|
|
|
class ToolCallFunction(BaseModel):
|
|
name: str | None = None
|
|
arguments: str | None = None
|
|
|
|
|
|
class ToolCall(BaseModel):
|
|
id: str | None = None
|
|
type: str | None = None
|
|
function: ToolCallFunction = ToolCallFunction()
|
|
|
|
|
|
class ChatAssistantTurn(BaseModel):
|
|
role: Literal["assistant"] = "assistant"
|
|
content: str | None = None
|
|
reasoning_content: str | None = None
|
|
tool_calls: list[ToolCall] | None = None
|
|
|
|
|
|
class ChatToolResultTurn(BaseModel):
|
|
role: Literal["tool"] = "tool"
|
|
tool_call_id: str
|
|
content: str
|
|
|
|
|
|
type ChatTurn = ChatMessage | ChatAssistantTurn | ChatToolResultTurn
|
|
|
|
|
|
class ChatBody(BaseModel):
|
|
model: str
|
|
messages: Sequence[ChatTurn]
|
|
stream: bool = False
|
|
max_tokens: int | None = None
|
|
max_completion_tokens: int | None = None
|
|
temperature: float | None = None
|
|
user: str | None = None
|
|
metadata: ChatMetadata | None = None
|
|
reasoning_effort: str | None = None
|
|
thinking: ThinkingParam | None = None
|
|
service_tier: str | None = None
|
|
prompt_cache_key: str | None = None
|
|
tools: Sequence[ChatTool | McpChatTool] | None = None
|
|
tool_choice: str | None = None
|
|
guardrails: list[str] | None = None
|
|
response_format: dict[str, object] | None = None
|
|
chat_template_kwargs: dict[str, bool] | None = None
|
|
cache: dict[str, bool] | None = {"no-cache": True}
|
|
|
|
|
|
class RouterSettingsOverride(BaseModel):
|
|
"""Router settings a test scopes below the global config: sent per request as
|
|
`router_settings_override` in a /chat/completions body (the reliability suite's
|
|
fallback and retry knobs) or stored on a key as `router_settings` at
|
|
/key/generate (the auto-router suite's tag filtering switch). Serialized
|
|
exclude_none, so an override sets only the knobs a test exercises. Each
|
|
fallbacks map is model_name -> the ordered fallback model_names to try."""
|
|
|
|
fallbacks: list[dict[str, list[str]]] | None = None
|
|
context_window_fallbacks: list[dict[str, list[str]]] | None = None
|
|
content_policy_fallbacks: list[dict[str, list[str]]] | None = None
|
|
num_retries: int | None = None
|
|
model_group_retry_policy: dict[str, dict[str, int]] | None = None
|
|
enable_tag_filtering: bool | None = None
|
|
|
|
|
|
class ReliabilityChatBody(ChatBody):
|
|
"""A /chat/completions body carrying a per-request router_settings_override.
|
|
Composes ChatBody (no attribute repetition) and adds the override; serialized
|
|
exclude_none so an absent override never leaks into the request."""
|
|
|
|
router_settings_override: RouterSettingsOverride | None = None
|
|
|
|
|
|
class McpToolFunctionRef(BaseModel):
|
|
name: str
|
|
|
|
|
|
class McpListedTool(BaseModel):
|
|
"""One entry of `mcp_list_tools`: a tool the gateway listed from the
|
|
attached MCP server and exposed to the model, in OpenAI function shape."""
|
|
|
|
function: McpToolFunctionRef | None = None
|
|
|
|
|
|
class McpToolCall(BaseModel):
|
|
"""One entry of `mcp_tool_calls`: a tool the model asked the gateway to run."""
|
|
|
|
function: McpToolFunctionRef | None = None
|
|
|
|
|
|
class McpCallResult(BaseModel):
|
|
"""One entry of `mcp_call_results`: what the gateway got back from executing
|
|
a tool upstream on the caller's behalf."""
|
|
|
|
name: str | None = None
|
|
result: str | None = None
|
|
|
|
|
|
class McpResponseMetadata(BaseModel):
|
|
"""`choices[].message.provider_specific_fields` MCP section: which tools the
|
|
gateway listed from the attached server, which the model called, and their
|
|
results. Populated only when the completion drove an MCP server."""
|
|
|
|
mcp_list_tools: list[McpListedTool] | None = None
|
|
mcp_tool_calls: list[McpToolCall] | None = None
|
|
mcp_call_results: list[McpCallResult] | None = None
|
|
|
|
|
|
class OutMessage(BaseModel):
|
|
role: str | None = None
|
|
content: str | None = None
|
|
reasoning_content: str | None = None
|
|
tool_calls: list[ToolCall] | None = None
|
|
provider_specific_fields: McpResponseMetadata | None = None
|
|
|
|
|
|
class ChatChoice(BaseModel):
|
|
message: OutMessage | None = None
|
|
finish_reason: str | None = None
|
|
|
|
|
|
class PromptTokensDetails(BaseModel):
|
|
cached_tokens: int | None = None
|
|
|
|
|
|
class CompletionTokensDetails(BaseModel):
|
|
reasoning_tokens: int | None = None
|
|
|
|
|
|
class Usage(BaseModel):
|
|
prompt_tokens: int | None = None
|
|
completion_tokens: int | None = None
|
|
total_tokens: int | None = None
|
|
cache_read_input_tokens: int | None = None
|
|
cache_creation_input_tokens: int | None = None
|
|
prompt_tokens_details: PromptTokensDetails | None = None
|
|
completion_tokens_details: CompletionTokensDetails | None = None
|
|
|
|
|
|
class ChatResponse(BaseModel):
|
|
id: str | None = None
|
|
object: str | None = None
|
|
model: str | None = None
|
|
choices: list[ChatChoice] = []
|
|
usage: Usage | None = None
|
|
service_tier: str | None = None
|
|
|
|
|
|
# ---------- anthropic /v1/messages + count_tokens ----------
|
|
|
|
|
|
class JsonSchemaProperty(BaseModel):
|
|
"""One property in a tool's JSON-Schema `input_schema`. Only `type` is
|
|
modelled; the endpoints under test read no further into the schema."""
|
|
|
|
type: str
|
|
|
|
|
|
class ToolInputSchema(BaseModel):
|
|
type: str = "object"
|
|
properties: dict[str, JsonSchemaProperty] = {}
|
|
required: list[str] = []
|
|
|
|
|
|
class AnthropicServerTool(BaseModel):
|
|
"""An Anthropic-managed tool the upstream executes itself. It carries no
|
|
`input_schema`; `type` is the SDK-version-pinned identifier LiteLLM keys its
|
|
per-provider translation on, and `name` is the unsuffixed canonical name the
|
|
upstream accepts."""
|
|
|
|
type: str
|
|
name: str
|
|
|
|
|
|
class AnthropicToolSearchTool(AnthropicServerTool):
|
|
"""The tool_search discovery tool, e.g. ``tool_search_tool_regex_20251119``."""
|
|
|
|
|
|
class AnthropicWebSearchTool(AnthropicServerTool):
|
|
"""The web_search server tool, e.g. ``web_search_20250305``. Distinct from
|
|
Claude Code's client-side ``WebSearch`` tool, which is an ordinary custom
|
|
tool the CLI executes and feeds back as a tool_result."""
|
|
|
|
max_uses: int | None = None
|
|
|
|
|
|
class AnthropicCustomTool(BaseModel):
|
|
name: str
|
|
description: str
|
|
input_schema: ToolInputSchema
|
|
|
|
|
|
type AnthropicTool = AnthropicToolSearchTool | AnthropicWebSearchTool | AnthropicCustomTool
|
|
|
|
|
|
class AnthropicContentBlock(BaseModel):
|
|
"""One block of a `content` array. Only the fields a test reads are
|
|
declared; `extra="allow"` keeps the rest (a `server_tool_use` block's
|
|
`input`, a `tool_search_tool_result` block's nested `content`) so an
|
|
assistant turn read off the wire can be replayed into history verbatim
|
|
instead of being silently flattened to its text."""
|
|
|
|
model_config = ConfigDict(extra="allow")
|
|
type: str | None = None
|
|
text: str | None = None
|
|
id: str | None = None
|
|
name: str | None = None
|
|
input: dict[str, object] | None = None
|
|
|
|
|
|
class AnthropicToolResultBlock(BaseModel):
|
|
"""The user-turn answer to a client-side `tool_use`. `tool_use_id` must be
|
|
the id the model actually emitted; an invented one is rejected by
|
|
Anthropic's own schema validator, which Bedrock inherits."""
|
|
|
|
type: Literal["tool_result"] = "tool_result"
|
|
tool_use_id: str
|
|
content: str
|
|
|
|
|
|
class AnthropicAssistantTurn(BaseModel):
|
|
role: Literal["assistant"] = "assistant"
|
|
content: list[AnthropicContentBlock]
|
|
|
|
|
|
class AnthropicToolResultTurn(BaseModel):
|
|
role: Literal["user"] = "user"
|
|
content: list[AnthropicToolResultBlock]
|
|
|
|
|
|
type AnthropicMessage = ChatMessage | AnthropicAssistantTurn | AnthropicToolResultTurn
|
|
|
|
|
|
class AnthropicMessagesBody(BaseModel):
|
|
model: str
|
|
messages: list[AnthropicMessage]
|
|
max_tokens: int
|
|
stream: bool | None = None
|
|
tools: list[AnthropicTool] | None = None
|
|
guardrails: list[str] | None = None
|
|
cache: dict[str, bool] | None = {"no-cache": True}
|
|
|
|
|
|
class CountTokensBody(BaseModel):
|
|
"""POST /v1/messages/count_tokens body: the /v1/messages shape minus
|
|
max_tokens (the endpoint only counts the prompt)."""
|
|
|
|
model: str
|
|
messages: list[ChatMessage]
|
|
|
|
|
|
class AnthropicMessagesResponse(BaseModel):
|
|
"""A /v1/messages answer. `content` is the Anthropic-native passthrough
|
|
shape; `choices` is the OpenAI-normalized shape LiteLLM emits for some
|
|
providers (e.g. Bedrock Converse). Presence of either proves the proxy
|
|
accepted and round-tripped the request. `extra="allow"` keeps the other
|
|
top-level keys so a shape-check failure can report the actual response keys
|
|
for triage."""
|
|
|
|
model_config = ConfigDict(extra="allow")
|
|
model: str | None = None
|
|
content: list[AnthropicContentBlock] | None = None
|
|
choices: list[ChatChoice] | None = None
|
|
usage: Usage | None = None
|
|
|
|
|
|
class CountTokensResponse(BaseModel):
|
|
"""`/v1/messages/count_tokens` answer. `input_tokens` is required so a 200
|
|
whose body lacks it fails validation instead of passing vacuously."""
|
|
|
|
input_tokens: int
|
|
|
|
|
|
# ---------- mcp servers ----------
|
|
|
|
|
|
class McpInfo(BaseModel):
|
|
"""The `mcp_info` display block stored on an MCP server; only the fields the
|
|
lifecycle test writes and reads back."""
|
|
|
|
server_name: str | None = None
|
|
description: str | None = None
|
|
logo_url: str | None = None
|
|
|
|
|
|
class McpServerCreateBody(BaseModel):
|
|
"""POST /v1/mcp/server. For a gateway-managed OAuth server, `auth_type` is
|
|
`oauth2` and `oauth2_flow` is `authorization_code`; the upstream endpoints
|
|
are discovered and registered via DCR when left unset. `allow_all_keys`
|
|
false scopes the server to keys granted it through object_permission."""
|
|
|
|
alias: str
|
|
url: str
|
|
transport: str = "http"
|
|
allow_all_keys: bool = True
|
|
auth_type: str | None = None
|
|
oauth2_flow: Literal["client_credentials", "authorization_code"] | None = None
|
|
authorization_url: str | None = None
|
|
token_url: str | None = None
|
|
server_name: str | None = None
|
|
description: str | None = None
|
|
mcp_info: McpInfo | None = None
|
|
|
|
|
|
class McpServerUpdateBody(PartialBody):
|
|
"""PUT /v1/mcp/server: a field left unset keeps its stored value, a field set
|
|
to None is cleared."""
|
|
|
|
server_id: str
|
|
alias: str | None = None
|
|
description: str | None = None
|
|
|
|
|
|
class McpServerInfo(BaseModel):
|
|
"""Response of POST /v1/mcp/server and GET /v1/mcp/server/{server_id}."""
|
|
|
|
server_id: str
|
|
alias: str | None = None
|
|
url: str | None = None
|
|
auth_type: str | None = None
|
|
oauth2_flow: str | None = None
|
|
allow_all_keys: bool | None = None
|
|
|
|
|
|
class McpServerRow(McpServerInfo):
|
|
"""A stored MCP server as the create, get, and list routes return it: the
|
|
fields the lifecycle test asserts survive the round trip."""
|
|
|
|
server_name: str | None = None
|
|
transport: str | None = None
|
|
description: str | None = None
|
|
mcp_info: McpInfo | None = None
|
|
|
|
|
|
class McpServerListResponse(RootModel[list[McpServerRow]]):
|
|
"""GET /v1/mcp/server answers with a bare array of servers."""
|
|
|
|
|
|
class ToolsetTool(BaseModel):
|
|
server_id: str
|
|
tool_name: str
|
|
|
|
|
|
class ToolsetCreateBody(BaseModel):
|
|
toolset_name: str
|
|
description: str | None = None
|
|
tools: list[ToolsetTool]
|
|
|
|
|
|
class ToolsetUpdateBody(PartialBody):
|
|
"""PUT /v1/mcp/toolset: a field left unset keeps its stored value, a field set
|
|
to None is cleared."""
|
|
|
|
toolset_id: str
|
|
description: str | None = None
|
|
tools: list[ToolsetTool] | None = None
|
|
|
|
|
|
class ToolsetRow(BaseModel):
|
|
"""A stored toolset as POST /v1/mcp/toolset, GET /v1/mcp/toolset/{toolset_id},
|
|
and each row of GET /v1/mcp/toolset return it."""
|
|
|
|
toolset_id: str
|
|
toolset_name: str
|
|
description: str | None = None
|
|
tools: list[ToolsetTool] = Field(default_factory=list)
|
|
|
|
|
|
class ToolsetListResponse(RootModel[list[ToolsetRow]]):
|
|
"""GET /v1/mcp/toolset answers with a bare array of toolsets."""
|
|
|
|
|
|
class EmbedBody(BaseModel):
|
|
model: str
|
|
input: str
|
|
cache: dict[str, bool] | None = {"no-cache": True}
|
|
|
|
|
|
class EmbedResponse(BaseModel):
|
|
model: str | None = None
|
|
|
|
|
|
# ---------- ocr ----------
|
|
|
|
|
|
class OcrDocument(BaseModel):
|
|
"""A document for /v1/ocr in Mistral OCR format: a document_url for PDFs/docs
|
|
or an image_url for images. exclude_none on serialize drops the unset one."""
|
|
|
|
type: str
|
|
document_url: str | None = None
|
|
image_url: str | None = None
|
|
|
|
|
|
class OcrBody(BaseModel):
|
|
model: str
|
|
document: OcrDocument
|
|
|
|
|
|
class OcrPage(BaseModel):
|
|
index: int
|
|
markdown: str
|
|
|
|
|
|
class OcrResponse(BaseModel):
|
|
object: str | None = None
|
|
model: str | None = None
|
|
pages: list[OcrPage] = []
|
|
|
|
|
|
# ---------- spend logs ----------
|
|
|
|
|
|
class CostBreakdown(BaseModel):
|
|
input_cost: float | None = None
|
|
output_cost: float | None = None
|
|
|
|
|
|
class GuardrailEntityMatch(BaseModel):
|
|
entity_type: str
|
|
score: float
|
|
start: int
|
|
end: int
|
|
|
|
|
|
class GuardrailRunRecord(BaseModel):
|
|
guardrail_name: str | None = None
|
|
guardrail_mode: str | None = None
|
|
guardrail_status: str | None = None
|
|
guardrail_provider: str | None = None
|
|
masked_entity_count: dict[str, int] | None = None
|
|
guardrail_response: object | None = None
|
|
|
|
|
|
class SpendLogMetadata(BaseModel):
|
|
cost_breakdown: CostBreakdown | None = None
|
|
applied_guardrails: list[str] | None = None
|
|
guardrail_information: list[GuardrailRunRecord] | None = None
|
|
|
|
|
|
class SpendLogRow(BaseModel):
|
|
request_id: str | None = None
|
|
api_key: str | None = None
|
|
model: str | None = None
|
|
spend: float | None = None
|
|
status: str | None = None
|
|
cache_hit: str | None = None
|
|
call_type: str | None = None
|
|
custom_llm_provider: str | None = None
|
|
team_id: str | None = None
|
|
user: str | None = None
|
|
end_user: str | None = None
|
|
prompt_tokens: int | None = None
|
|
completion_tokens: int | None = None
|
|
total_tokens: int | None = None
|
|
request_tags: list[str] | None = None
|
|
metadata: SpendLogMetadata | None = None
|
|
|
|
|
|
class SpendLogs(RootModel[list[SpendLogRow]]):
|
|
pass
|
|
|
|
|
|
class SpendLogsParams(BaseModel):
|
|
request_id: str | None = None
|
|
api_key: str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def require_filter(self) -> SpendLogsParams:
|
|
if self.request_id is None and self.api_key is None:
|
|
raise ValueError(
|
|
"unfiltered /spend/logs returns the entire spend table and OOMs the "
|
|
"runner on long-lived environments; filter by request_id or api_key, "
|
|
"or use ProxyClient.spend_logs_window for a bounded /spend/logs/v2 read"
|
|
)
|
|
return self
|
|
|
|
|
|
class SpendLogsPageParams(BaseModel):
|
|
"""Query for /spend/logs/v2, which requires an explicit date window and
|
|
serves pages of at most 100 rows."""
|
|
|
|
start_date: str
|
|
end_date: str
|
|
page: int
|
|
page_size: int
|
|
api_key: str | None = None
|
|
|
|
|
|
class SpendLogsPage(BaseModel):
|
|
data: list[SpendLogRow] = []
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
total_pages: int
|
|
|
|
|
|
# ---------- spend calculate ----------
|
|
|
|
|
|
class SpendCalculateBody(BaseModel):
|
|
model: str
|
|
messages: list[ChatMessage]
|
|
|
|
|
|
class SpendCalculateResponse(BaseModel):
|
|
cost: float
|
|
|
|
|
|
# ---------- spend tags ----------
|
|
|
|
|
|
class TagSpend(BaseModel):
|
|
individual_request_tag: str | None = None
|
|
log_count: int | None = None
|
|
total_spend: float | None = None
|
|
|
|
|
|
class SpendTagsResponse(RootModel[list[TagSpend]]):
|
|
"""GET /spend/tags answers with a bare array of per-tag aggregates, not an
|
|
object wrapping them (that's /global/spend/tags). Read the rows off .root."""
|
|
|
|
|
|
# ---------- route probing ----------
|
|
|
|
|
|
class DateRangeParams(BaseModel):
|
|
start_date: str
|
|
end_date: str
|
|
|
|
|
|
class RouteSpec(RootModel[dict[str, object]]):
|
|
"""One /openapi.json path entry: a map of HTTP method -> operation. Only the
|
|
method names are read, so the operation specs stay opaque."""
|
|
|
|
@property
|
|
def methods(self) -> frozenset[str]:
|
|
return frozenset(method.lower() for method in self.root)
|
|
|
|
|
|
class OpenAPISchema(BaseModel):
|
|
paths: dict[str, RouteSpec] = {}
|
|
|
|
|
|
# ---------- model info / custom pricing ----------
|
|
|
|
|
|
class CustomPricing(BaseModel):
|
|
"""The per-token custom-pricing fields a deployment can override in
|
|
litellm_params - the token-cost subset of litellm's CustomPricingLiteLLMParams
|
|
the proxy applies to chat spend. All optional: a config sets only what it
|
|
overrides, and /model/info echoes the rates the proxy resolved."""
|
|
|
|
model_config = ConfigDict(extra="ignore")
|
|
mode: str | None = None
|
|
input_cost_per_token: float | None = None
|
|
output_cost_per_token: float | None = None
|
|
cache_read_input_token_cost: float | None = None
|
|
cache_creation_input_token_cost: float | None = None
|
|
|
|
def overrides(self) -> dict[str, float]:
|
|
"""The rates actually declared (non-null) - e.g. those a config.yml sets."""
|
|
declared = {
|
|
"input_cost_per_token": self.input_cost_per_token,
|
|
"output_cost_per_token": self.output_cost_per_token,
|
|
"cache_read_input_token_cost": self.cache_read_input_token_cost,
|
|
"cache_creation_input_token_cost": self.cache_creation_input_token_cost,
|
|
}
|
|
return {field: rate for field, rate in declared.items() if rate is not None}
|
|
|
|
def token_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
|
|
"""Spend for a fresh (uncached) call under these rates: the proxy's
|
|
custom-pricing formula (prompt * input + completion * output)."""
|
|
assert self.input_cost_per_token is not None and self.output_cost_per_token is not None, (
|
|
"custom pricing has no per-token rates"
|
|
)
|
|
return prompt_tokens * self.input_cost_per_token + completion_tokens * self.output_cost_per_token
|
|
|
|
|
|
class DeploymentParams(CustomPricing):
|
|
"""The litellm_params half of a /model/info row: the stored deployment as written,
|
|
credentials scrubbed. Unlike model_info it is never back-filled from the cost map,
|
|
so a key the store dropped is absent here (check `model_fields_set`)."""
|
|
|
|
model: str | None = None
|
|
api_base: str | None = None
|
|
max_input_tokens: int | None = None
|
|
|
|
|
|
class DeploymentModelInfo(CustomPricing):
|
|
id: str | None = None
|
|
max_input_tokens: int | None = None
|
|
|
|
|
|
class ModelInfoEntry(BaseModel):
|
|
"""One /model/info row. `litellm_params` is the configured deployment (carries
|
|
any custom-pricing override); `model_info` is the price the proxy resolved for
|
|
it - the override merged over the cost-map defaults, so a key cleared from the
|
|
stored blob reads as the cost-map default here."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
model_name: str
|
|
litellm_params: DeploymentParams = DeploymentParams()
|
|
model_info: DeploymentModelInfo = DeploymentModelInfo()
|
|
|
|
|
|
class StoredDeployment(BaseModel):
|
|
"""PATCH /model/{model_id}/update answers with the row as stored: both blobs raw,
|
|
nothing back-filled, so a cleared key is absent from `model_fields_set` of the
|
|
blob it was cleared from."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
model_name: str
|
|
litellm_params: DeploymentParams
|
|
model_info: DeploymentModelInfo
|
|
|
|
|
|
class ModelInfoResponse(BaseModel):
|
|
data: list[ModelInfoEntry] = []
|
|
|
|
|
|
class CostMapEntry(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
litellm_provider: str | None = None
|
|
mode: str | None = None
|
|
deprecation_date: str | None = None
|
|
input_cost_per_token: float | None = None
|
|
output_cost_per_token: float | None = None
|
|
cache_read_input_token_cost: float | None = None
|
|
supports_function_calling: bool | None = None
|
|
supports_reasoning: bool | None = None
|
|
supports_response_schema: bool | None = None
|
|
|
|
|
|
class CostMap(RootModel[dict[str, CostMapEntry]]):
|
|
pass
|
|
|
|
|
|
class FileEntry(BaseModel):
|
|
id: str
|
|
|
|
|
|
class FileListResponse(BaseModel):
|
|
"""GET /files answer. `data` is required on purpose: a 200 whose body lacks
|
|
the OpenAI-format file list must fail validation, not pass vacuously."""
|
|
|
|
data: list[FileEntry]
|
|
|
|
|
|
class FineTuningJobsParams(BaseModel):
|
|
custom_llm_provider: Literal["openai", "azure"]
|
|
|
|
|
|
class FineTuningJobEntry(BaseModel):
|
|
id: str
|
|
|
|
|
|
class FineTuningJobsResponse(BaseModel):
|
|
"""GET /fine_tuning/jobs answer; `data` required for the same reason as
|
|
FileListResponse."""
|
|
|
|
data: list[FineTuningJobEntry]
|
|
|
|
|
|
# ---------- model management ----------
|
|
|
|
|
|
class LiteLLMParamsBody(BaseModel):
|
|
"""POST /model/new litellm_params: `model` is the only required field; `api_key`
|
|
et al may be an `os.environ/FOO` reference the proxy resolves at call time.
|
|
The `*_cost_per_token` / `*_token_cost` fields register a per-deployment custom
|
|
pricing override (the cache and `_priority` rates only apply when both base
|
|
rates are set, which is what makes the proxy register the deployment's full
|
|
pricing entry); left None (and dropped from the body) the deployment keeps the
|
|
backend's canonical rate."""
|
|
|
|
model: str
|
|
api_key: str | None = None
|
|
litellm_credential_name: str | None = None
|
|
api_base: str | None = None
|
|
api_version: str | None = None
|
|
realtime_protocol: str | None = None
|
|
aws_access_key_id: str | None = None
|
|
aws_secret_access_key: str | None = None
|
|
aws_region_name: str | None = None
|
|
vertex_project: str | None = None
|
|
vertex_location: str | None = None
|
|
vertex_credentials: str | None = None
|
|
gcs_bucket_name: str | None = None
|
|
bucket_name: str | None = None
|
|
s3_bucket_name: str | None = None
|
|
s3_region_name: str | None = None
|
|
s3_access_key_id: str | None = None
|
|
s3_secret_access_key: str | None = None
|
|
aws_batch_role_arn: str | None = None
|
|
aws_role_name: str | None = None
|
|
aws_session_name: str | None = None
|
|
aws_external_id: str | None = None
|
|
input_cost_per_token: float | None = None
|
|
output_cost_per_token: float | None = None
|
|
cache_read_input_token_cost: float | None = None
|
|
cache_creation_input_token_cost: float | None = None
|
|
input_cost_per_token_priority: float | None = None
|
|
output_cost_per_token_priority: float | None = None
|
|
extra_headers: dict[str, str] | None = None
|
|
use_in_pass_through: bool | None = None
|
|
complexity_router_config: dict[str, object] | None = None
|
|
auto_router_config: str | None = None
|
|
auto_router_default_model: str | None = None
|
|
auto_router_embedding_model: str | None = None
|
|
tags: list[str] | None = None
|
|
mock_response: str | None = None
|
|
timeout: float | None = None
|
|
tpm: int | None = None
|
|
weight: int | None = None
|
|
max_input_tokens: int | None = None
|
|
|
|
|
|
ModelMode = Literal["chat", "batch", "realtime", "image_generation"]
|
|
|
|
|
|
class ModelInfoBody(BaseModel):
|
|
# id is left unset so the proxy assigns a unique model_id per deployment.
|
|
# Pinning it to the model_name made re-registrations of a fixed-name model
|
|
# (e.g. the batch suite's openai-batch) collide on the model_id unique
|
|
# constraint when a prior run's teardown had not removed the row.
|
|
id: str | None = None
|
|
mode: ModelMode | None = None
|
|
max_input_tokens: int | None = None
|
|
access_groups: list[str] | None = None
|
|
team_id: str | None = None
|
|
allowed_fails_policy: dict[str, int] | None = None
|
|
|
|
|
|
class ModelNewBody(BaseModel):
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
model_name: str
|
|
litellm_params: LiteLLMParamsBody
|
|
model_info: ModelInfoBody
|
|
|
|
|
|
class ModelNewResponse(BaseModel):
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
model_id: str
|
|
|
|
|
|
class ModelUpdateBody(BaseModel):
|
|
"""POST /model/update body: the target deployment (`model_info.id`) plus the
|
|
`litellm_params` to merge over its stored params. The handler overlays only the
|
|
non-null fields, so a body carrying `input_cost_per_token` re-prices the
|
|
deployment while leaving its other params intact."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
litellm_params: LiteLLMParamsBody
|
|
model_info: ModelInfoBody
|
|
|
|
|
|
class Clear(BaseModel):
|
|
"""Serializes to JSON null. The transport dumps every body with exclude_none, so a
|
|
field set to this is how a patch carries the explicit null that removes a stored key."""
|
|
|
|
@model_serializer
|
|
def _as_null(self) -> None:
|
|
return None
|
|
|
|
|
|
class LiteLLMParamsPatch(BaseModel):
|
|
api_base: str | Clear | None = None
|
|
max_input_tokens: int | Clear | None = None
|
|
input_cost_per_token: float | Clear | None = None
|
|
output_cost_per_token: float | Clear | None = None
|
|
|
|
|
|
class ModelInfoPatch(BaseModel):
|
|
mode: ModelMode | Clear | None = None
|
|
max_input_tokens: int | Clear | None = None
|
|
|
|
|
|
class ModelPatchBody(BaseModel):
|
|
"""PATCH /model/{model_id}/update body, JSON Merge Patch over the stored deployment:
|
|
a field left None is dropped from the body and unchanged, a field set to `Clear()`
|
|
is sent as null and removed, a field with a value is set."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
litellm_params: LiteLLMParamsPatch | None = None
|
|
model_info: ModelInfoPatch | None = None
|
|
|
|
|
|
class ModelListEntry(BaseModel):
|
|
id: str
|
|
|
|
|
|
class ModelsListParams(BaseModel):
|
|
"""Query for GET /v1/models. A wildcard route such as ``openai/gpt-5.4*`` is
|
|
listed only under ``return_wildcard_routes``; without it the route is dropped
|
|
and only its expansions remain, so a readiness poll for the pattern itself
|
|
never resolves."""
|
|
|
|
return_wildcard_routes: bool = True
|
|
|
|
|
|
class ModelsListResponse(BaseModel):
|
|
"""GET /v1/models on the data plane: the deployments the gateway can actually
|
|
serve right now. Used to confirm a freshly created model has propagated from
|
|
the control plane before a test calls it."""
|
|
|
|
data: tuple[ModelListEntry, ...] = ()
|
|
|
|
|
|
class ModelDeleteBody(BaseModel):
|
|
id: str
|
|
|
|
|
|
class ConnectionTestBody(BaseModel):
|
|
"""POST /health/test_connection body, the API behind the Admin UI's Test
|
|
Connection button: the deployment params as typed into the add-model form and
|
|
the health-check mode picking which endpoint the probe calls. The endpoint
|
|
rejects `os.environ/` references, so credentials are either literal values or
|
|
omitted to fall through to the proxy's own environment."""
|
|
|
|
litellm_params: LiteLLMParamsBody
|
|
mode: Literal["chat", "completion", "embedding", "responses"]
|
|
|
|
|
|
class ConnectionTestResult(BaseModel):
|
|
error: str | None = None
|
|
|
|
|
|
class ConnectionTestResponse(BaseModel):
|
|
status: Literal["success", "error"]
|
|
result: ConnectionTestResult | None = None
|
|
|
|
|
|
class CredentialCreateBody(BaseModel):
|
|
credential_name: str
|
|
credential_values: dict[str, str]
|
|
credential_info: dict[str, str] = {}
|
|
|
|
|
|
class CredentialCreateResponse(BaseModel):
|
|
success: bool
|
|
|
|
|
|
# ---------- key / team / user / organization management ----------
|
|
|
|
|
|
class Cleared(BaseModel):
|
|
"""An explicit JSON null in a merge-patch body. The transport drops `None` fields
|
|
before sending (`exclude_none`), so `None` means "leave the stored value alone"; a
|
|
field set to `CLEAR` reaches the wire as `null`, which tells the proxy to clear it."""
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
@model_serializer
|
|
def _as_null(self) -> None:
|
|
return None
|
|
|
|
|
|
CLEAR: Final = Cleared()
|
|
|
|
|
|
class KeyUpdateBody(BaseModel):
|
|
"""POST /key/update is a merge patch: a field left `None` is dropped from the body and
|
|
keeps its stored value, `CLEAR` sends an explicit null that clears it (`budget_duration`
|
|
clears `budget_reset_at` with it), and `metadata` replaces the stored metadata wholesale."""
|
|
|
|
key: str
|
|
models: list[str] | None = None
|
|
key_alias: str | None = None
|
|
tpm_limit: int | None = None
|
|
rpm_limit: int | None = None
|
|
max_budget: float | Cleared | None = None
|
|
budget_duration: str | Cleared | None = None
|
|
metadata: KeyMetadata | None = None
|
|
|
|
|
|
class KeyBlockBody(BaseModel):
|
|
key: str
|
|
|
|
|
|
class KeyListParams(BaseModel):
|
|
key_alias: str
|
|
|
|
|
|
class KeyListResponse(BaseModel):
|
|
total_count: int
|
|
|
|
|
|
# ---------- admin UI session ----------
|
|
|
|
|
|
class UiLoginBody(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class UiLoginResponse(BaseModel):
|
|
token: str
|
|
redirect_url: str
|
|
|
|
|
|
class UiSessionClaims(BaseModel):
|
|
user_id: str
|
|
key: str
|
|
user_role: str
|
|
login_method: Literal["sso", "username_password"]
|
|
exp: int
|
|
|
|
|
|
class TeamMemberEntry(BaseModel):
|
|
role: Literal["admin", "user"]
|
|
user_id: str
|
|
|
|
|
|
class TeamMetadata(BaseModel):
|
|
disable_global_guardrails: bool | None = None
|
|
|
|
|
|
class TeamNewBody(BaseModel):
|
|
team_alias: str
|
|
models: list[str] = []
|
|
team_id: str | None = None
|
|
organization_id: str | None = None
|
|
metadata: TeamMetadata | None = None
|
|
|
|
|
|
class TeamNewResponse(BaseModel):
|
|
team_id: str
|
|
|
|
|
|
class TeamUpdateBody(BaseModel):
|
|
team_id: str
|
|
team_alias: str
|
|
models: list[str] | None = None
|
|
|
|
|
|
class TeamInfoParams(BaseModel):
|
|
team_id: str
|
|
|
|
|
|
class TeamData(BaseModel):
|
|
team_alias: str | None = None
|
|
models: list[str] = []
|
|
members_with_roles: list[TeamMemberEntry] = []
|
|
|
|
|
|
class TeamInfoResponse(BaseModel):
|
|
team_id: str
|
|
team_info: TeamData
|
|
|
|
|
|
class TeamMemberAddBody(BaseModel):
|
|
team_id: str
|
|
member: TeamMemberEntry
|
|
|
|
|
|
class TeamMemberDeleteBody(BaseModel):
|
|
team_id: str
|
|
user_id: str
|
|
|
|
|
|
class TeamDeleteBody(BaseModel):
|
|
team_ids: list[str]
|
|
|
|
|
|
class TeamListEntry(BaseModel):
|
|
team_id: str
|
|
|
|
|
|
class TeamListResponse(RootModel[list[TeamListEntry]]):
|
|
"""GET /team/list answers with a bare array of team objects (not an object
|
|
wrapping them). Only team_id is read; pydantic ignores the rest."""
|
|
|
|
|
|
UserRole = Literal["proxy_admin", "proxy_admin_viewer", "internal_user", "internal_user_viewer"]
|
|
|
|
|
|
class UserNewBody(BaseModel):
|
|
user_email: str
|
|
user_role: UserRole
|
|
user_id: str | None = None
|
|
|
|
|
|
class UserNewResponse(BaseModel):
|
|
user_id: str
|
|
|
|
|
|
class UserUpdateBody(BaseModel):
|
|
user_id: str
|
|
user_role: UserRole
|
|
|
|
|
|
class UserInfoParams(BaseModel):
|
|
user_id: str
|
|
|
|
|
|
class UserData(BaseModel):
|
|
user_id: str | None = None
|
|
user_email: str | None = None
|
|
user_role: str | None = None
|
|
|
|
|
|
class UserInfoResponse(BaseModel):
|
|
user_id: str
|
|
user_info: UserData
|
|
|
|
|
|
class UserDeleteBody(BaseModel):
|
|
user_ids: list[str]
|
|
|
|
|
|
class UserDeleteResponse(RootModel[int]):
|
|
pass
|
|
|
|
|
|
class UserListParams(BaseModel):
|
|
user_ids: str
|
|
|
|
|
|
class UserListRow(BaseModel):
|
|
user_id: str
|
|
|
|
|
|
class UserListResponse(BaseModel):
|
|
users: list[UserListRow]
|
|
total: int
|
|
|
|
|
|
class OrgNewBody(BaseModel):
|
|
organization_alias: str
|
|
models: list[str] = []
|
|
|
|
|
|
class OrgNewResponse(BaseModel):
|
|
organization_id: str
|
|
|
|
|
|
class OrgUpdateBody(BaseModel):
|
|
organization_id: str
|
|
organization_alias: str
|
|
|
|
|
|
class OrgInfoParams(BaseModel):
|
|
organization_id: str
|
|
|
|
|
|
class OrgInfoResponse(BaseModel):
|
|
organization_id: str
|
|
organization_alias: str | None = None
|
|
models: list[str] = []
|
|
|
|
|
|
class OrgDeleteBody(BaseModel):
|
|
organization_ids: list[str]
|
|
|
|
|
|
# ---------- tags (management) ----------
|
|
|
|
|
|
class TagNewBody(BaseModel):
|
|
name: str
|
|
description: str | None = None
|
|
|
|
|
|
class TagDeleteBody(BaseModel):
|
|
name: str
|
|
|
|
|
|
class TagListEntry(BaseModel):
|
|
name: str
|
|
description: str | None = None
|
|
|
|
|
|
class TagListResponse(RootModel[list[TagListEntry]]):
|
|
"""GET /tag/list answers with a bare array of tag configs (the stored tags plus
|
|
any dynamically-seen spend tags), not an object wrapping them. Read the rows off
|
|
.root."""
|
|
|
|
|
|
# ---------- health / lifecycle ----------
|
|
|
|
|
|
class ReadinessResponse(BaseModel):
|
|
"""GET /health/readiness (public probe). The low-detail payload a load
|
|
balancer sees: `status` plus the resolved DB state (`connected`,
|
|
`disconnected`, or `Not connected`)."""
|
|
|
|
status: str
|
|
db: str | None = None
|
|
|
|
|
|
class ReadinessDetailsResponse(ReadinessResponse):
|
|
"""GET /health/readiness/details (authenticated). Extends the public payload
|
|
with the diagnostics only an authenticated caller may read."""
|
|
|
|
litellm_version: str | None = None
|
|
success_callbacks: list[str] = []
|