返回 Pixelle-Video
llm_util.py
根目录 / pixelle_video / utils / llm_util.py
1 # Copyright (C) 2025 AIDC-AI
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 # http://www.apache.org/licenses/LICENSE-2.0
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12
13 """
14 LLM utility functions for model discovery and connection testing.
15
16 Uses the OpenAI-compatible models endpoint.
17 """
18
19 import re
20 from typing import List, Tuple
21 import httpx
22 from loguru import logger
23
24
25 def _build_models_url(base_url: str) -> str:
26 """Build a provider models endpoint from a user-entered API base URL."""
27 raw = (base_url or "").strip().rstrip("/")
28 if raw.endswith("/models"):
29 return raw
30
31 normalized = normalize_openai_base_url(base_url)
32
33 if re.search(r"/v\d+(?:\.\d+)?$", normalized):
34 return f"{normalized}/models"
35
36 return f"{normalized}/v1/models"
37
38
39 def normalize_openai_base_url(base_url: str) -> str:
40 """Normalize a user-entered OpenAI-compatible Base URL for SDK calls.
41
42 Users sometimes paste a concrete endpoint such as /chat/completions or
43 /models. The OpenAI SDK expects the API root, so concrete endpoint suffixes
44 must be stripped before real model calls.
45 """
46 normalized = (base_url or "").strip().rstrip("/")
47 for suffix in ("/chat/completions", "/completions", "/responses", "/models"):
48 if normalized.endswith(suffix):
49 normalized = normalized[: -len(suffix)].rstrip("/")
50 break
51 return normalized
52
53
54 def fetch_available_models(api_key: str, base_url: str, timeout: float = 10.0) -> List[str]:
55 """
56 Fetch available models from an OpenAI-compatible API endpoint.
57
58 Uses the provider models endpoint with Bearer token authentication.
59
60 Args:
61 api_key: The API key for authentication
62 base_url: The base URL of the API (e.g., https://api.openai.com/v1).
63 If a chat endpoint is pasted by mistake, it will be normalized.
64 timeout: Request timeout in seconds
65
66 Returns:
67 List of model IDs available from the API
68
69 Raises:
70 httpx.HTTPStatusError: If the API returns an error status code
71 httpx.RequestError: If there's a network error
72 """
73 models_url = _build_models_url(base_url)
74
75 headers = {
76 "Authorization": f"Bearer {api_key}",
77 "Content-Type": "application/json",
78 }
79
80 logger.debug(f"Fetching models from: {models_url}")
81
82 with httpx.Client(timeout=timeout) as client:
83 response = client.get(models_url, headers=headers)
84 response.raise_for_status()
85
86 data = response.json()
87 models = [model["id"] for model in data.get("data", [])]
88
89 # Sort models alphabetically for better UX
90 models.sort()
91
92 logger.debug(f"Fetched {len(models)} models")
93 return models
94
95
96 def test_llm_connection(api_key: str, base_url: str, timeout: float = 10.0) -> Tuple[bool, str, int]:
97 """
98 Test the LLM API connection by attempting to fetch the models list.
99
100 Args:
101 api_key: The API key for authentication
102 base_url: The base URL of the API
103 timeout: Request timeout in seconds
104
105 Returns:
106 Tuple of (success: bool, message: str, model_count: int)
107 - success: True if connection succeeded
108 - message: Human-readable status message
109 - model_count: Number of models available (0 if failed)
110 """
111 try:
112 models = fetch_available_models(api_key, base_url, timeout)
113 return True, f"Connection successful! {len(models)} models available.", len(models)
114 except httpx.HTTPStatusError as e:
115 status_code = e.response.status_code
116 if status_code == 401:
117 return False, "Authentication failed: Invalid API Key", 0
118 elif status_code == 403:
119 return False, "Access forbidden: Check your API Key permissions", 0
120 elif status_code == 404:
121 return False, "API endpoint not found: Check your Base URL", 0
122 else:
123 return False, f"API error: HTTP {status_code}", 0
124 except httpx.ConnectError:
125 return False, "Connection failed: Cannot reach the server", 0
126 except httpx.TimeoutException:
127 return False, "Connection timeout: Server did not respond in time", 0
128 except Exception as e:
129 logger.error(f"LLM connection test error: {e}")
130 return False, f"Error: {str(e)}", 0
131
131 lines PYTHON