|
8 | 8 | from pydantic import BaseModel, Field |
9 | 9 | from sqlalchemy.ext.asyncio import AsyncSession |
10 | 10 |
|
11 | | -from src.core.llm.response import ( |
12 | | - APIResponse, |
13 | | - GenerateResult, |
14 | | - EmbeddingResult, |
15 | | - RerankResult, |
16 | | - StreamChunk, |
17 | | - UsageInfo, |
18 | | -) |
19 | | -from src.services.config_reader_service import ConfigReaderService |
20 | | -from src.core.llm.factory import ModelFactory |
| 11 | +from src.core.llm.response import APIResponse |
| 12 | +from src.core.llm.base_provider import BaseProvider |
| 13 | +from src.core.llm.exceptions import UserModelConfigMissingError |
| 14 | +from src.core.llm.user_model_resolver import aresolve_user_model |
21 | 15 | from src.database import get_db |
22 | 16 |
|
23 | 17 | router = APIRouter(prefix="/api/v1/llm", tags=["llm"]) |
24 | 18 |
|
25 | | -# 依赖注入 |
26 | | -model_factory = ModelFactory() |
| 19 | + |
| 20 | +def _coerce_int(value: str, field: str) -> int: |
| 21 | + """把请求边界传入的 ID 字符串归一成 int,非法值 → 422。 |
| 22 | +
|
| 23 | + ``user_id``(来自 ``X-User-Id`` Header)与 ``config_id``(来自请求体)在路由层是 |
| 24 | + 字符串,但下游 resolver / ConfigReaderService / ``BigInteger`` 主键都按 int 契约。 |
| 25 | + 在此显式转换并校验,避免把弱类型一路下沉到 SQL 靠驱动隐式转换。 |
| 26 | + """ |
| 27 | + try: |
| 28 | + return int(value) |
| 29 | + except (TypeError, ValueError) as exc: |
| 30 | + raise HTTPException(status_code=422, detail=f"invalid {field}") from exc |
| 31 | + |
| 32 | + |
| 33 | +async def _resolve_provider( |
| 34 | + db: AsyncSession, |
| 35 | + user_id: str, |
| 36 | + capability: str, |
| 37 | + *, |
| 38 | + config_id: Optional[str] = None, |
| 39 | + override_model: Optional[str] = None, |
| 40 | +) -> BaseProvider: |
| 41 | + """按用户解析指定能力的 Provider,未命中(含系统兜底)→ 404。 |
| 42 | +
|
| 43 | + 统一走 :func:`aresolve_user_model`(``/llm`` 路由保留系统兜底):config_id 指定优先, |
| 44 | + 否则取用户该能力默认配置,仍无则系统环境兜底;都没有抛 ``UserModelConfigMissingError`` |
| 45 | + 在此翻成 404,保持原有对外行为。``user_id`` / ``config_id`` 在边界归一成 int。 |
| 46 | + """ |
| 47 | + uid = _coerce_int(user_id, "X-User-Id") |
| 48 | + cid = _coerce_int(config_id, "config_id") if config_id is not None else None |
| 49 | + try: |
| 50 | + resolved = await aresolve_user_model( |
| 51 | + user_id=uid, |
| 52 | + capability=capability, |
| 53 | + config_id=cid, |
| 54 | + allow_system_fallback=True, |
| 55 | + override_model=override_model, |
| 56 | + db=db, |
| 57 | + ) |
| 58 | + except UserModelConfigMissingError as exc: |
| 59 | + raise HTTPException( |
| 60 | + status_code=404, detail=f"{capability} configuration not found" |
| 61 | + ) from exc |
| 62 | + return resolved.provider |
27 | 63 |
|
28 | 64 |
|
29 | 65 | # ============ 请求模型 ============ |
@@ -81,32 +117,9 @@ async def generate_text( |
81 | 117 | APIResponse[GenerateResult] |
82 | 118 | """ |
83 | 119 | try: |
84 | | - config_service = ConfigReaderService(db) |
85 | | - |
86 | | - # 获取用户配置 |
87 | | - if request.config_id: |
88 | | - config = await config_service.get_user_config_by_id(x_user_id, request.config_id) |
89 | | - else: |
90 | | - config = await config_service.get_user_default_config_by_capability(x_user_id, "CHAT") |
91 | | - |
92 | | - if not config: |
93 | | - config = config_service.get_system_fallback_config_by_capability("CHAT") |
94 | | - |
95 | | - if not config: |
96 | | - raise HTTPException(status_code=404, detail="LLM CHAT configuration not found") |
97 | | - |
98 | | - # 获取 Provider |
99 | | - provider_type = config.get("provider_type", "openai") |
100 | | - if config.get("is_system_fallback"): |
101 | | - api_key = config.get("api_key", "") |
102 | | - else: |
103 | | - api_key = await config_service.decrypt_api_key(config.get("api_key", "")) |
104 | | - |
105 | | - client = model_factory.create_client( |
106 | | - provider_type=provider_type, |
107 | | - api_key=api_key, |
108 | | - api_base_url=config.get("custom_api_base_url"), |
109 | | - model_name=request.model or config.get("model_name"), |
| 120 | + client = await _resolve_provider( |
| 121 | + db, x_user_id, "CHAT", |
| 122 | + config_id=request.config_id, override_model=request.model, |
110 | 123 | ) |
111 | 124 |
|
112 | 125 | # 调用生成 |
@@ -147,30 +160,9 @@ async def generate_text_stream( |
147 | 160 | from fastapi.responses import StreamingResponse |
148 | 161 |
|
149 | 162 | try: |
150 | | - config_service = ConfigReaderService(db) |
151 | | - |
152 | | - if request.config_id: |
153 | | - config = await config_service.get_user_config_by_id(x_user_id, request.config_id) |
154 | | - else: |
155 | | - config = await config_service.get_user_default_config_by_capability(x_user_id, "CHAT") |
156 | | - |
157 | | - if not config: |
158 | | - config = config_service.get_system_fallback_config_by_capability("CHAT") |
159 | | - |
160 | | - if not config: |
161 | | - raise HTTPException(status_code=404, detail="LLM CHAT configuration not found") |
162 | | - |
163 | | - provider_type = config.get("provider_type", "openai") |
164 | | - if config.get("is_system_fallback"): |
165 | | - api_key = config.get("api_key", "") |
166 | | - else: |
167 | | - api_key = await config_service.decrypt_api_key(config.get("api_key", "")) |
168 | | - |
169 | | - client = model_factory.create_client( |
170 | | - provider_type=provider_type, |
171 | | - api_key=api_key, |
172 | | - api_base_url=config.get("custom_api_base_url"), |
173 | | - model_name=request.model or config.get("model_name"), |
| 163 | + client = await _resolve_provider( |
| 164 | + db, x_user_id, "CHAT", |
| 165 | + config_id=request.config_id, override_model=request.model, |
174 | 166 | ) |
175 | 167 |
|
176 | 168 | async def event_generator(): |
@@ -206,30 +198,9 @@ async def embed_text( |
206 | 198 | APIResponse[EmbeddingResult] |
207 | 199 | """ |
208 | 200 | try: |
209 | | - config_service = ConfigReaderService(db) |
210 | | - |
211 | | - if request.config_id: |
212 | | - config = await config_service.get_user_config_by_id(x_user_id, request.config_id) |
213 | | - else: |
214 | | - config = await config_service.get_user_default_config_by_capability(x_user_id, "EMBEDDING") |
215 | | - |
216 | | - if not config: |
217 | | - config = config_service.get_system_fallback_config_by_capability("EMBEDDING") |
218 | | - |
219 | | - if not config: |
220 | | - raise HTTPException(status_code=404, detail="Embedding configuration not found") |
221 | | - |
222 | | - provider_type = config.get("provider_type", "openai") |
223 | | - if config.get("is_system_fallback"): |
224 | | - api_key = config.get("api_key", "") |
225 | | - else: |
226 | | - api_key = await config_service.decrypt_api_key(config.get("api_key", "")) |
227 | | - |
228 | | - client = model_factory.create_client( |
229 | | - provider_type=provider_type, |
230 | | - api_key=api_key, |
231 | | - api_base_url=config.get("custom_api_base_url"), |
232 | | - model_name=request.model or config.get("model_name"), |
| 201 | + client = await _resolve_provider( |
| 202 | + db, x_user_id, "EMBEDDING", |
| 203 | + config_id=request.config_id, override_model=request.model, |
233 | 204 | ) |
234 | 205 |
|
235 | 206 | result = await client.embed(texts=request.input, model=request.model) |
@@ -258,30 +229,9 @@ async def rerank_documents( |
258 | 229 | APIResponse[RerankResult] |
259 | 230 | """ |
260 | 231 | try: |
261 | | - config_service = ConfigReaderService(db) |
262 | | - |
263 | | - if request.config_id: |
264 | | - config = await config_service.get_user_config_by_id(x_user_id, request.config_id) |
265 | | - else: |
266 | | - config = await config_service.get_user_default_config_by_capability(x_user_id, "RERANK") |
267 | | - |
268 | | - if not config: |
269 | | - config = config_service.get_system_fallback_config_by_capability("RERANK") |
270 | | - |
271 | | - if not config: |
272 | | - raise HTTPException(status_code=404, detail="Rerank configuration not found") |
273 | | - |
274 | | - provider_type = config.get("provider_type", "openai") |
275 | | - if config.get("is_system_fallback"): |
276 | | - api_key = config.get("api_key", "") |
277 | | - else: |
278 | | - api_key = await config_service.decrypt_api_key(config.get("api_key", "")) |
279 | | - |
280 | | - client = model_factory.create_client( |
281 | | - provider_type=provider_type, |
282 | | - api_key=api_key, |
283 | | - api_base_url=config.get("custom_api_base_url"), |
284 | | - model_name=request.model or config.get("model_name"), |
| 232 | + client = await _resolve_provider( |
| 233 | + db, x_user_id, "RERANK", |
| 234 | + config_id=request.config_id, override_model=request.model, |
285 | 235 | ) |
286 | 236 |
|
287 | 237 | result = await client.rerank( |
@@ -315,30 +265,8 @@ async def extract_text_from_image( |
315 | 265 | APIResponse[dict] |
316 | 266 | """ |
317 | 267 | try: |
318 | | - config_service = ConfigReaderService(db) |
319 | | - |
320 | | - if request.config_id: |
321 | | - config = await config_service.get_user_config_by_id(x_user_id, request.config_id) |
322 | | - else: |
323 | | - config = await config_service.get_user_default_config_by_capability(x_user_id, "OCR") |
324 | | - |
325 | | - if not config: |
326 | | - config = config_service.get_system_fallback_config_by_capability("OCR") |
327 | | - |
328 | | - if not config: |
329 | | - raise HTTPException(status_code=404, detail="OCR configuration not found") |
330 | | - |
331 | | - provider_type = config.get("provider_type", "openai") |
332 | | - if config.get("is_system_fallback"): |
333 | | - api_key = config.get("api_key", "") |
334 | | - else: |
335 | | - api_key = await config_service.decrypt_api_key(config.get("api_key", "")) |
336 | | - |
337 | | - client = model_factory.create_client( |
338 | | - provider_type=provider_type, |
339 | | - api_key=api_key, |
340 | | - api_base_url=config.get("custom_api_base_url"), |
341 | | - model_name=request.model or config.get("model_name"), |
| 268 | + client = await _resolve_provider( |
| 269 | + db, x_user_id, "OCR", config_id=request.config_id, |
342 | 270 | ) |
343 | 271 |
|
344 | 272 | result = await client.extract_text( |
|
0 commit comments