Complete admin model management
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0002_expand_model_config"
|
||||
down_revision = "0001_initial_schema"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("sys_model", sa.Column("display_name", sa.String(length=100), nullable=True))
|
||||
op.add_column(
|
||||
"sys_model",
|
||||
sa.Column("api_type", sa.String(length=50), nullable=False, server_default="openai_compatible"),
|
||||
)
|
||||
op.add_column("sys_model", sa.Column("base_url", sa.String(length=255), nullable=True))
|
||||
op.add_column(
|
||||
"sys_model",
|
||||
sa.Column("auth_type", sa.String(length=30), nullable=False, server_default="bearer"),
|
||||
)
|
||||
op.add_column("sys_model", sa.Column("api_version", sa.String(length=50), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("top_p", sa.Numeric(4, 2), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("top_k", sa.Integer(), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("presence_penalty", sa.Numeric(4, 2), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("frequency_penalty", sa.Numeric(4, 2), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("context_window", sa.Integer(), nullable=True))
|
||||
op.add_column(
|
||||
"sys_model",
|
||||
sa.Column("stream_enabled", sa.Integer(), nullable=False, server_default="1"),
|
||||
)
|
||||
op.add_column("sys_model", sa.Column("response_format", sa.String(length=50), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("extra_params", sa.Text(), nullable=True))
|
||||
op.add_column("sys_model", sa.Column("remark", sa.String(length=255), nullable=True))
|
||||
|
||||
op.alter_column("sys_model", "api_type", server_default=None)
|
||||
op.alter_column("sys_model", "auth_type", server_default=None)
|
||||
op.alter_column("sys_model", "stream_enabled", server_default=None)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("sys_model", "remark")
|
||||
op.drop_column("sys_model", "extra_params")
|
||||
op.drop_column("sys_model", "response_format")
|
||||
op.drop_column("sys_model", "stream_enabled")
|
||||
op.drop_column("sys_model", "context_window")
|
||||
op.drop_column("sys_model", "frequency_penalty")
|
||||
op.drop_column("sys_model", "presence_penalty")
|
||||
op.drop_column("sys_model", "top_k")
|
||||
op.drop_column("sys_model", "top_p")
|
||||
op.drop_column("sys_model", "api_version")
|
||||
op.drop_column("sys_model", "auth_type")
|
||||
op.drop_column("sys_model", "base_url")
|
||||
op.drop_column("sys_model", "api_type")
|
||||
op.drop_column("sys_model", "display_name")
|
||||
@@ -11,6 +11,7 @@ from app.models.admin import Admin
|
||||
from app.models.ai_config import ModelConfig, Prompt, SystemConfig
|
||||
from app.schemas.admin import EnableModelRequest, ModelSaveRequest, PromptSaveRequest, SystemConfigSaveRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.model_service import ModelClientService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -59,13 +60,29 @@ def create_model(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
if not payload.apiKey:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="API Key 不能为空")
|
||||
model = ModelConfig(
|
||||
provider=payload.provider,
|
||||
display_name=payload.displayName,
|
||||
api_type=payload.apiType,
|
||||
model_name=payload.modelName,
|
||||
base_url=payload.baseUrl,
|
||||
api_url=payload.apiUrl,
|
||||
api_key=payload.apiKey,
|
||||
auth_type=payload.authType,
|
||||
api_version=payload.apiVersion,
|
||||
temperature=payload.temperature,
|
||||
top_p=payload.topP,
|
||||
top_k=payload.topK,
|
||||
presence_penalty=payload.presencePenalty,
|
||||
frequency_penalty=payload.frequencyPenalty,
|
||||
max_token=payload.maxToken,
|
||||
context_window=payload.contextWindow,
|
||||
stream_enabled=payload.streamEnabled,
|
||||
response_format=payload.responseFormat,
|
||||
extra_params=payload.extraParams,
|
||||
remark=payload.remark,
|
||||
timeout_second=payload.timeoutSecond,
|
||||
enabled=0,
|
||||
)
|
||||
@@ -77,6 +94,46 @@ def create_model(
|
||||
return api_success(_model_dict(model))
|
||||
|
||||
|
||||
@router.put("/model/{model_id}")
|
||||
def update_model(
|
||||
model_id: int,
|
||||
payload: ModelSaveRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = db.get(ModelConfig, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
|
||||
|
||||
model.provider = payload.provider
|
||||
model.display_name = payload.displayName
|
||||
model.api_type = payload.apiType
|
||||
model.model_name = payload.modelName
|
||||
model.base_url = payload.baseUrl
|
||||
model.api_url = payload.apiUrl
|
||||
if payload.apiKey:
|
||||
model.api_key = payload.apiKey
|
||||
model.auth_type = payload.authType
|
||||
model.api_version = payload.apiVersion
|
||||
model.temperature = payload.temperature
|
||||
model.top_p = payload.topP
|
||||
model.top_k = payload.topK
|
||||
model.presence_penalty = payload.presencePenalty
|
||||
model.frequency_penalty = payload.frequencyPenalty
|
||||
model.max_token = payload.maxToken
|
||||
model.context_window = payload.contextWindow
|
||||
model.stream_enabled = payload.streamEnabled
|
||||
model.response_format = payload.responseFormat
|
||||
model.extra_params = payload.extraParams
|
||||
model.remark = payload.remark
|
||||
model.timeout_second = payload.timeoutSecond
|
||||
db.add(model)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="update", target_id=model.id)
|
||||
db.commit()
|
||||
db.refresh(model)
|
||||
return api_success(_model_dict(model))
|
||||
|
||||
|
||||
@router.post("/model/enable")
|
||||
def enable_model(
|
||||
payload: EnableModelRequest,
|
||||
@@ -109,6 +166,21 @@ def delete_model(
|
||||
return api_success()
|
||||
|
||||
|
||||
@router.post("/model/{model_id}/test")
|
||||
def test_model(
|
||||
model_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
model = db.get(ModelConfig, model_id)
|
||||
if model is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在")
|
||||
result = ModelClientService.test_model(model)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="model", action="test", target_id=model.id)
|
||||
db.commit()
|
||||
return api_success(result)
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def list_config(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
configs = db.scalars(select(SystemConfig).order_by(SystemConfig.config_key.asc())).all()
|
||||
@@ -139,11 +211,25 @@ def _model_dict(model: ModelConfig) -> dict:
|
||||
return {
|
||||
"id": model.id,
|
||||
"provider": model.provider,
|
||||
"displayName": model.display_name,
|
||||
"apiType": model.api_type,
|
||||
"modelName": model.model_name,
|
||||
"baseUrl": model.base_url,
|
||||
"apiUrl": model.api_url,
|
||||
"apiKeyMasked": "******" if model.api_key else "",
|
||||
"authType": model.auth_type,
|
||||
"apiVersion": model.api_version,
|
||||
"temperature": float(model.temperature) if model.temperature is not None else None,
|
||||
"topP": float(model.top_p) if model.top_p is not None else None,
|
||||
"topK": model.top_k,
|
||||
"presencePenalty": float(model.presence_penalty) if model.presence_penalty is not None else None,
|
||||
"frequencyPenalty": float(model.frequency_penalty) if model.frequency_penalty is not None else None,
|
||||
"maxToken": model.max_token,
|
||||
"contextWindow": model.context_window,
|
||||
"streamEnabled": model.stream_enabled,
|
||||
"responseFormat": model.response_format,
|
||||
"extraParams": model.extra_params,
|
||||
"remark": model.remark,
|
||||
"timeoutSecond": model.timeout_second,
|
||||
"enabled": model.enabled,
|
||||
}
|
||||
|
||||
@@ -23,11 +23,25 @@ class ModelConfig(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
provider: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
display_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
api_type: Mapped[str] = mapped_column(String(50), default="openai_compatible", nullable=False)
|
||||
model_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
base_url: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
api_url: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
api_key: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
auth_type: Mapped[str] = mapped_column(String(30), default="bearer", nullable=False)
|
||||
api_version: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
temperature: Mapped[Decimal | None] = mapped_column(Numeric(4, 2), nullable=True)
|
||||
top_p: Mapped[Decimal | None] = mapped_column(Numeric(4, 2), nullable=True)
|
||||
top_k: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
presence_penalty: Mapped[Decimal | None] = mapped_column(Numeric(4, 2), nullable=True)
|
||||
frequency_penalty: Mapped[Decimal | None] = mapped_column(Numeric(4, 2), nullable=True)
|
||||
max_token: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
context_window: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
stream_enabled: Mapped[int] = mapped_column(default=1, nullable=False)
|
||||
response_format: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
extra_params: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
timeout_second: Mapped[int] = mapped_column(Integer, default=30, nullable=False)
|
||||
enabled: Mapped[int] = mapped_column(default=0, nullable=False)
|
||||
|
||||
|
||||
@@ -53,11 +53,25 @@ class PromptSaveRequest(BaseModel):
|
||||
|
||||
class ModelSaveRequest(BaseModel):
|
||||
provider: str = Field(min_length=1, max_length=50)
|
||||
displayName: str | None = Field(default=None, max_length=100)
|
||||
apiType: str = Field(default="openai_compatible", max_length=50)
|
||||
modelName: str = Field(min_length=1, max_length=100)
|
||||
apiUrl: str = Field(min_length=1, max_length=255)
|
||||
apiKey: str = Field(min_length=1)
|
||||
baseUrl: str | None = Field(default=None, max_length=255)
|
||||
apiUrl: str = Field(default="", max_length=255)
|
||||
apiKey: str = ""
|
||||
authType: str = Field(default="bearer", max_length=30)
|
||||
apiVersion: str | None = Field(default=None, max_length=50)
|
||||
temperature: float | None = Field(default=0.2, ge=0, le=2)
|
||||
topP: float | None = Field(default=None, ge=0, le=1)
|
||||
topK: int | None = Field(default=None, ge=1, le=1000)
|
||||
presencePenalty: float | None = Field(default=None, ge=-2, le=2)
|
||||
frequencyPenalty: float | None = Field(default=None, ge=-2, le=2)
|
||||
maxToken: int | None = Field(default=1024, ge=1, le=100000)
|
||||
contextWindow: int | None = Field(default=None, ge=1, le=2000000)
|
||||
streamEnabled: int = Field(default=1, ge=0, le=1)
|
||||
responseFormat: str | None = Field(default=None, max_length=50)
|
||||
extraParams: str | None = None
|
||||
remark: str | None = Field(default=None, max_length=255)
|
||||
timeoutSecond: int = Field(default=30, ge=1, le=300)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import select
|
||||
@@ -37,7 +39,7 @@ class ModelClientService:
|
||||
answer = _mock_answer(rag_result)
|
||||
else:
|
||||
model_name = model.model_name
|
||||
answer = _call_openai_compatible_model(model, rag_result)
|
||||
answer = _call_configured_model(model, rag_result)
|
||||
|
||||
return ModelCompletion(
|
||||
answer=answer,
|
||||
@@ -47,6 +49,16 @@ class ModelClientService:
|
||||
output_token=_rough_token_count(answer),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def test_model(model: ModelConfig) -> dict[str, Any]:
|
||||
test_prompt = "请回复 OK,用于测试模型配置是否可用。"
|
||||
rag_result = RagResult(question=test_prompt, knowledge_scopes=[], chunks=[], prompt=test_prompt)
|
||||
try:
|
||||
answer = _call_configured_model(model, rag_result, allow_no_hit=True)
|
||||
except ExternalServiceError as exc:
|
||||
return {"ok": False, "message": str(exc), "answer": ""}
|
||||
return {"ok": True, "message": "模型配置可用", "answer": answer[:500]}
|
||||
|
||||
|
||||
def _mock_answer(rag_result: RagResult) -> str:
|
||||
if not rag_result.is_hit:
|
||||
@@ -67,40 +79,116 @@ def _rough_token_count(text: str) -> int:
|
||||
return max(1, len(text.strip()) // 2)
|
||||
|
||||
|
||||
def _call_openai_compatible_model(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
if not rag_result.is_hit:
|
||||
def _call_configured_model(model: ModelConfig, rag_result: RagResult, *, allow_no_hit: bool = False) -> str:
|
||||
if not allow_no_hit and not rag_result.is_hit:
|
||||
return NO_HIT_ANSWER
|
||||
if not model.api_url or not model.api_key:
|
||||
raise ExternalServiceError("模型 API URL 或 API Key 未配置", provider="model")
|
||||
if not (model.api_url or model.base_url) or not model.api_key:
|
||||
raise ExternalServiceError("模型 Base URL/API URL 或 API Key 未配置", provider="model")
|
||||
|
||||
api_type = model.api_type or "openai_compatible"
|
||||
if api_type == "anthropic_messages":
|
||||
return _call_anthropic_messages(model, rag_result)
|
||||
if api_type == "gemini_generate_content":
|
||||
return _call_gemini_generate_content(model, rag_result)
|
||||
return _call_openai_compatible_model(model, rag_result)
|
||||
|
||||
|
||||
def _call_openai_compatible_model(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
payload = {
|
||||
"model": model.model_name,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是企业知识库问答助手,只能基于已提供的知识片段回答。"},
|
||||
{"role": "user", "content": rag_result.prompt},
|
||||
],
|
||||
"temperature": float(model.temperature) if model.temperature is not None else 0.2,
|
||||
"max_tokens": model.max_token or 1024,
|
||||
"stream": False,
|
||||
"max_tokens": model.max_token or 1024,
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {model.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
_put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
|
||||
_put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))
|
||||
_put_if_not_none(payload, "presence_penalty", _decimal_to_float(model.presence_penalty))
|
||||
_put_if_not_none(payload, "frequency_penalty", _decimal_to_float(model.frequency_penalty))
|
||||
if model.response_format == "json_object":
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
payload.update(_load_extra_params(model.extra_params))
|
||||
|
||||
headers = _auth_headers(model)
|
||||
try:
|
||||
response = httpx.post(
|
||||
model.api_url,
|
||||
_resolve_openai_endpoint(model),
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=model.timeout_second,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return _extract_answer(response.json())
|
||||
return _extract_openai_answer(response.json())
|
||||
except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
|
||||
raise ExternalServiceError(f"模型调用失败:{exc}", provider="model") from exc
|
||||
|
||||
|
||||
def _extract_answer(data: dict[str, Any]) -> str:
|
||||
def _call_anthropic_messages(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
payload = {
|
||||
"model": model.model_name,
|
||||
"max_tokens": model.max_token or 1024,
|
||||
"system": "你是企业知识库问答助手,只能基于已提供的知识片段回答。",
|
||||
"messages": [{"role": "user", "content": rag_result.prompt}],
|
||||
"stream": False,
|
||||
}
|
||||
_put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
|
||||
_put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))
|
||||
_put_if_not_none(payload, "top_k", model.top_k)
|
||||
payload.update(_load_extra_params(model.extra_params))
|
||||
|
||||
headers = {
|
||||
"x-api-key": model.api_key,
|
||||
"anthropic-version": model.api_version or "2023-06-01",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
response = httpx.post(
|
||||
_resolve_anthropic_endpoint(model),
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=model.timeout_second,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return _extract_anthropic_answer(response.json())
|
||||
except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
|
||||
raise ExternalServiceError(f"模型调用失败:{exc}", provider="model") from exc
|
||||
|
||||
|
||||
def _call_gemini_generate_content(model: ModelConfig, rag_result: RagResult) -> str:
|
||||
generation_config: dict[str, Any] = {}
|
||||
_put_if_not_none(generation_config, "temperature", _decimal_to_float(model.temperature))
|
||||
_put_if_not_none(generation_config, "topP", _decimal_to_float(model.top_p))
|
||||
_put_if_not_none(generation_config, "topK", model.top_k)
|
||||
_put_if_not_none(generation_config, "maxOutputTokens", model.max_token)
|
||||
if model.response_format == "json_object":
|
||||
generation_config["responseMimeType"] = "application/json"
|
||||
|
||||
payload = {
|
||||
"systemInstruction": {
|
||||
"parts": [{"text": "你是企业知识库问答助手,只能基于已提供的知识片段回答。"}]
|
||||
},
|
||||
"contents": [{"role": "user", "parts": [{"text": rag_result.prompt}]}],
|
||||
}
|
||||
if generation_config:
|
||||
payload["generationConfig"] = generation_config
|
||||
payload.update(_load_extra_params(model.extra_params))
|
||||
|
||||
try:
|
||||
response = httpx.post(
|
||||
_resolve_gemini_endpoint(model),
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=model.timeout_second,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return _extract_gemini_answer(response.json())
|
||||
except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
|
||||
raise ExternalServiceError(f"模型调用失败:{exc}", provider="model") from exc
|
||||
|
||||
|
||||
def _extract_openai_answer(data: dict[str, Any]) -> str:
|
||||
choices = data.get("choices")
|
||||
if not isinstance(choices, list) or not choices:
|
||||
raise ValueError("模型响应缺少 choices")
|
||||
@@ -117,3 +205,80 @@ def _extract_answer(data: dict[str, Any]) -> str:
|
||||
return str(first_choice["text"])
|
||||
|
||||
raise ValueError("模型响应缺少回答内容")
|
||||
|
||||
|
||||
def _extract_anthropic_answer(data: dict[str, Any]) -> str:
|
||||
content = data.get("content")
|
||||
if not isinstance(content, list):
|
||||
raise ValueError("Anthropic 响应缺少 content")
|
||||
texts = [item.get("text", "") for item in content if isinstance(item, dict) and item.get("type") == "text"]
|
||||
answer = "".join(texts).strip()
|
||||
if not answer:
|
||||
raise ValueError("Anthropic 响应缺少文本内容")
|
||||
return answer
|
||||
|
||||
|
||||
def _extract_gemini_answer(data: dict[str, Any]) -> str:
|
||||
candidates = data.get("candidates")
|
||||
if not isinstance(candidates, list) or not candidates:
|
||||
raise ValueError("Gemini 响应缺少 candidates")
|
||||
parts = candidates[0].get("content", {}).get("parts", [])
|
||||
texts = [item.get("text", "") for item in parts if isinstance(item, dict)]
|
||||
answer = "".join(texts).strip()
|
||||
if not answer:
|
||||
raise ValueError("Gemini 响应缺少文本内容")
|
||||
return answer
|
||||
|
||||
|
||||
def _resolve_openai_endpoint(model: ModelConfig) -> str:
|
||||
if model.api_url:
|
||||
return model.api_url
|
||||
base_url = (model.base_url or "").rstrip("/")
|
||||
return f"{base_url}/chat/completions"
|
||||
|
||||
|
||||
def _resolve_anthropic_endpoint(model: ModelConfig) -> str:
|
||||
if model.api_url:
|
||||
return model.api_url
|
||||
base_url = (model.base_url or "https://api.anthropic.com").rstrip("/")
|
||||
return f"{base_url}/v1/messages"
|
||||
|
||||
|
||||
def _resolve_gemini_endpoint(model: ModelConfig) -> str:
|
||||
if model.api_url:
|
||||
return model.api_url
|
||||
base_url = (model.base_url or "https://generativelanguage.googleapis.com/v1beta").rstrip("/")
|
||||
query = urlencode({"key": model.api_key})
|
||||
return f"{base_url}/models/{model.model_name}:generateContent?{query}"
|
||||
|
||||
|
||||
def _auth_headers(model: ModelConfig) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if model.auth_type == "api_key":
|
||||
headers["x-api-key"] = model.api_key
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {model.api_key}"
|
||||
if model.api_version:
|
||||
headers["api-version"] = model.api_version
|
||||
return headers
|
||||
|
||||
|
||||
def _decimal_to_float(value: Any) -> float | None:
|
||||
return float(value) if value is not None else None
|
||||
|
||||
|
||||
def _put_if_not_none(target: dict[str, Any], key: str, value: Any) -> None:
|
||||
if value is not None:
|
||||
target[key] = value
|
||||
|
||||
|
||||
def _load_extra_params(extra_params: str | None) -> dict[str, Any]:
|
||||
if not extra_params:
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(extra_params)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ExternalServiceError(f"模型高级参数不是合法 JSON:{exc}", provider="model") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise ExternalServiceError("模型高级参数必须是 JSON 对象", provider="model")
|
||||
return data
|
||||
|
||||
Reference in New Issue
Block a user