| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- from datetime import datetime
- from typing import TYPE_CHECKING, Generic, Literal, TypeVar
- from core_domain import ChatCompletionResponseContract
- from core_shared import JSONValue
- from pydantic import BaseModel, Field, HttpUrl
- if TYPE_CHECKING:
- from app.db.models import ModelDefinition, ModelProviderDefinition
- ModelStatus = Literal["active", "disabled"]
- T = TypeVar("T")
- class ApiErrorResponse(BaseModel):
- errorType: str
- message: str
- details: dict[str, JSONValue] = Field(default_factory=dict)
- class ApiResponse(BaseModel, Generic[T]):
- success: bool = True
- data: T | None = None
- error: ApiErrorResponse | None = None
- requestId: str
- serverTime: datetime
- class PageRequest(BaseModel):
- page: int = Field(default=1, ge=1)
- pageSize: int = Field(default=20, ge=1, le=200)
- keyword: str | None = None
- @property
- def offset(self) -> int:
- return (self.page - 1) * self.pageSize
- class PageResult(BaseModel, Generic[T]):
- items: list[T]
- total: int
- page: int
- pageSize: int
- hasMore: bool
- @classmethod
- def from_items(
- cls,
- *,
- items: list[T],
- total: int,
- page: int,
- page_size: int) -> "PageResult[T]":
- return cls(
- items=items,
- total=total,
- page=page,
- pageSize=page_size,
- hasMore=page * page_size < total)
- class ModelCreateRequest(BaseModel):
- code: str | None = Field(default=None, min_length=1, max_length=64)
- name: str = Field(min_length=1, max_length=128)
- provider_id: str | None = None
- provider_type: str = "openai_compatible"
- provider_base_url: HttpUrl | str
- provider_api_key: str | None = None
- model_name: str = Field(min_length=1, max_length=128)
- status: ModelStatus = "active"
- description: str | None = None
- capabilities_json: list[str] = Field(default_factory=lambda: ["chat"])
- context_window: int | None = Field(default=None, ge=1)
- max_output_tokens: int | None = Field(default=None, ge=1)
- default_temperature: float | None = Field(default=None, ge=0, le=2)
- timeout_seconds: float = Field(default=60.0, ge=1, le=300)
- metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
- class ModelUpdateRequest(BaseModel):
- code: str | None = Field(default=None, min_length=1, max_length=64)
- name: str | None = Field(default=None, min_length=1, max_length=128)
- provider_id: str | None = None
- provider_type: str | None = None
- provider_base_url: HttpUrl | str | None = None
- provider_api_key: str | None = None
- model_name: str | None = Field(default=None, min_length=1, max_length=128)
- status: ModelStatus | None = None
- description: str | None = None
- capabilities_json: list[str] | None = None
- context_window: int | None = Field(default=None, ge=1)
- max_output_tokens: int | None = Field(default=None, ge=1)
- default_temperature: float | None = Field(default=None, ge=0, le=2)
- timeout_seconds: float | None = Field(default=None, ge=1, le=300)
- metadata_json: dict[str, JSONValue] | None = None
- class ModelStatusUpdateRequest(BaseModel):
- status: ModelStatus
- class ModelResponse(BaseModel):
- id: str
- code: str
- name: str
- provider_id: str | None = None
- provider_type: str
- provider_base_url: str
- has_provider_api_key: bool
- model_name: str
- status: ModelStatus
- description: str | None = None
- capabilities_json: list[str] = Field(default_factory=list)
- context_window: int | None = None
- max_output_tokens: int | None = None
- default_temperature: float | None = None
- timeout_seconds: float
- metadata_json: dict[str, JSONValue] | None = None
- created_time: datetime
- updated_time: datetime
- @classmethod
- def from_entity(cls, entity: "ModelDefinition") -> "ModelResponse":
- return cls(
- id=entity.id,
- code=entity.code,
- name=entity.name,
- provider_id=entity.provider_id,
- provider_type=entity.provider_type,
- provider_base_url=entity.provider_base_url,
- has_provider_api_key=bool(entity.provider_api_key),
- model_name=entity.model_name,
- status=entity.status,
- description=entity.description,
- capabilities_json=list(entity.capabilities_json or []),
- context_window=entity.context_window,
- max_output_tokens=entity.max_output_tokens,
- default_temperature=entity.default_temperature,
- timeout_seconds=entity.timeout_seconds,
- metadata_json=entity.metadata_json,
- created_time=entity.created_time,
- updated_time=entity.updated_time,
- )
- class ModelDto(BaseModel):
- id: str
- name: str
- providerId: str | None = None
- providerType: str
- providerBaseUrl: str
- hasProviderApiKey: bool
- modelName: str
- description: str | None = None
- capabilities: list[str] = Field(default_factory=list)
- contextWindow: int | None = None
- maxOutputTokens: int | None = None
- defaultTemperature: float | None = None
- timeoutSeconds: float
- metadata: dict[str, JSONValue] | None = None
- createdTime: datetime
- updatedTime: datetime
- @classmethod
- def from_entity(cls, entity: "ModelDefinition") -> "ModelDto":
- return cls(
- id=entity.id,
- name=entity.name,
- providerId=entity.provider_id,
- providerType=entity.provider_type,
- providerBaseUrl=entity.provider_base_url,
- hasProviderApiKey=bool(entity.provider_api_key),
- modelName=entity.model_name,
- description=entity.description,
- capabilities=list(entity.capabilities_json or []),
- contextWindow=entity.context_window,
- maxOutputTokens=entity.max_output_tokens,
- defaultTemperature=entity.default_temperature,
- timeoutSeconds=entity.timeout_seconds,
- metadata=entity.metadata_json,
- createdTime=entity.created_time,
- updatedTime=entity.updated_time)
- class ModelCreateRequestDto(BaseModel):
- name: str = Field(min_length=1, max_length=128)
- providerId: str | None = None
- providerType: str = "openai_compatible"
- providerBaseUrl: HttpUrl | str | None = None
- providerApiKey: str | None = None
- modelName: str = Field(min_length=1, max_length=128)
- description: str | None = None
- capabilities: list[str] = Field(default_factory=lambda: ["chat"])
- contextWindow: int | None = Field(default=None, ge=1)
- maxOutputTokens: int | None = Field(default=None, ge=1)
- defaultTemperature: float | None = Field(default=None, ge=0, le=2)
- timeoutSeconds: float = Field(default=60.0, ge=1, le=300)
- metadata: dict[str, JSONValue] = Field(default_factory=dict)
- class ModelUpdateRequestDto(BaseModel):
- modelId: str
- name: str | None = Field(default=None, min_length=1, max_length=128)
- providerId: str | None = None
- providerType: str | None = None
- providerBaseUrl: HttpUrl | str | None = None
- providerApiKey: str | None = None
- modelName: str | None = Field(default=None, min_length=1, max_length=128)
- description: str | None = None
- capabilities: list[str] | None = None
- contextWindow: int | None = Field(default=None, ge=1)
- maxOutputTokens: int | None = Field(default=None, ge=1)
- defaultTemperature: float | None = Field(default=None, ge=0, le=2)
- timeoutSeconds: float | None = Field(default=None, ge=1, le=300)
- metadata: dict[str, JSONValue] | None = None
- class ModelDeleteRequestDto(BaseModel):
- modelId: str
- class DeleteData(BaseModel):
- deleted: bool
- modelId: str | None = None
- providerId: str | None = None
- class ModelTestRequest(BaseModel):
- prompt: str = Field(default="Reply with a short readiness check.", min_length=1)
- system_prompt: str | None = "You are a concise model connectivity checker."
- temperature: float | None = Field(default=None, ge=0, le=2)
- max_tokens: int | None = Field(default=128, ge=1)
- class ModelTestResponse(BaseModel):
- model: ModelResponse
- response: ChatCompletionResponseContract
- class ModelTestRequestDto(BaseModel):
- modelId: str
- prompt: str = Field(default="Reply with a short readiness check.", min_length=1)
- systemPrompt: str | None = "You are a concise model connectivity checker."
- temperature: float | None = Field(default=None, ge=0, le=2)
- maxTokens: int | None = Field(default=128, ge=1)
- class ModelTestData(BaseModel):
- model: ModelDto
- response: ChatCompletionResponseContract
- class ModelItemDto(BaseModel):
- modelId: str
- displayName: str
- modelType: str
- ownedBy: str | None = None
- contextWindow: int | None = None
- class ModelProviderDto(BaseModel):
- id: str
- name: str
- providerType: str
- baseUrl: str
- apiKeyRef: str
- models: list[ModelItemDto]
- defaultModel: str | None = None
- extraConfig: dict[str, JSONValue] = Field(default_factory=dict)
- createdTime: datetime
- updatedTime: datetime
- @classmethod
- def from_entity(cls, entity: "ModelProviderDefinition") -> "ModelProviderDto":
- return cls(
- id=entity.id,
- name=entity.name,
- providerType=entity.provider_type,
- baseUrl=entity.base_url,
- apiKeyRef=_mask_api_key(entity.api_key),
- models=[
- ModelItemDto(**_to_camel_model_item(item))
- for item in entity.models_json or []
- ],
- defaultModel=entity.default_model,
- extraConfig=entity.extra_config_json or {},
- createdTime=entity.created_time,
- updatedTime=entity.updated_time)
- class ModelProviderCreateRequestDto(BaseModel):
- name: str = Field(min_length=1, max_length=128)
- providerType: str = "openai_compatible"
- baseUrl: HttpUrl | str
- apiKey: str | None = None
- models: list[ModelItemDto] = Field(default_factory=list)
- defaultModel: str | None = None
- extraConfig: dict[str, JSONValue] = Field(default_factory=dict)
- class ModelProviderUpdateRequestDto(BaseModel):
- providerId: str
- name: str | None = Field(default=None, min_length=1, max_length=128)
- baseUrl: HttpUrl | str | None = None
- apiKey: str | None = None
- models: list[ModelItemDto] | None = None
- defaultModel: str | None = None
- extraConfig: dict[str, JSONValue] | None = None
- class ModelProviderDeleteRequestDto(BaseModel):
- providerId: str
- class ModelProviderTestRequestDto(BaseModel):
- providerId: str
- class ModelProviderTestData(BaseModel):
- success: bool
- message: str
- latencyMs: int | None = None
- modelList: list[str] = Field(default_factory=list)
- class DiscoverModelsRequestDto(BaseModel):
- providerId: str | None = None
- providerType: str | None = None
- baseUrl: HttpUrl | str | None = None
- apiKey: str | None = None
- class DiscoverModelsData(BaseModel):
- providerType: str
- models: list[ModelItemDto]
- def _mask_api_key(api_key: str | None) -> str:
- if not api_key:
- return ""
- return f"{api_key[:3]}***masked"
- def _to_snake_model_item(item: ModelItemDto) -> dict[str, JSONValue]:
- data: dict[str, JSONValue] = {
- "model_id": item.modelId,
- "display_name": item.displayName,
- "model_type": item.modelType,
- }
- if item.ownedBy is not None:
- data["owned_by"] = item.ownedBy
- if item.contextWindow is not None:
- data["context_window"] = item.contextWindow
- return data
- def _to_camel_model_item(item: dict[str, JSONValue]) -> dict[str, JSONValue]:
- return {
- "modelId": str(item.get("model_id") or item.get("modelId") or ""),
- "displayName": str(item.get("display_name") or item.get("displayName") or ""),
- "modelType": str(item.get("model_type") or item.get("modelType") or "chat"),
- "ownedBy": (
- item.get("owned_by")
- if isinstance(item.get("owned_by"), str)
- else item.get("ownedBy")
- ),
- "contextWindow": (
- item.get("context_window")
- if isinstance(item.get("context_window"), int)
- else item.get("contextWindow")
- ),
- }
|