Enhance credential management and testing functionality
- Introduce new fields for voice, speed, and language in the AssistantConfig and ProviderCredential models to support TTS and ASR configurations. - Update the database schema and seeding script to accommodate the new fields, ensuring backward compatibility. - Implement credential testing endpoints and logic to validate OpenAI-compatible credentials, enhancing user experience and reliability. - Modify frontend components to include new fields in the credential forms and improve connection testing feedback. - Refactor related services and API interactions to support the new credential testing feature.
This commit is contained in:
@@ -58,7 +58,9 @@ async def resolve_runtime_config(
|
||||
# 模型/音色:凭证的模型ID优先
|
||||
model=(llm.model_id if llm else ""),
|
||||
asr=(stt.model_id if stt else ""),
|
||||
voice="", # 音色无独立列,留空 → service_factory 回退 .env TTS_VOICE
|
||||
voice=(tts.voice if tts else ""),
|
||||
stt_language=(stt.language if stt else ""),
|
||||
tts_speed=(tts.speed if tts else 1.0),
|
||||
realtimeModel=(realtime.model_id if realtime else ""),
|
||||
# 运行时连接信息(真 key + url):凭证优先,否则 .env 兜底
|
||||
llm_api_key=(llm.api_key if llm else config.LLM_API_KEY),
|
||||
|
||||
124
backend/services/credential_tester.py
Normal file
124
backend/services/credential_tester.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""OpenAI 兼容模型凭证的最小连通测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import time
|
||||
import wave
|
||||
|
||||
import httpx
|
||||
|
||||
from schemas import CredentialTestRequest, CredentialTestResult
|
||||
|
||||
TEST_TIMEOUT_SECONDS = 10.0
|
||||
|
||||
|
||||
def _endpoint(base_url: str, path: str) -> str:
|
||||
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
||||
|
||||
|
||||
def _silent_wav() -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with wave.open(buffer, "wb") as wav:
|
||||
wav.setnchannels(1)
|
||||
wav.setsampwidth(2)
|
||||
wav.setframerate(16_000)
|
||||
wav.writeframes(b"\x00\x00" * 1_600)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _error_detail(response: httpx.Response, api_key: str) -> str:
|
||||
try:
|
||||
body = response.json()
|
||||
detail = (
|
||||
body.get("error", {}).get("message")
|
||||
if isinstance(body, dict) and isinstance(body.get("error"), dict)
|
||||
else body.get("detail") if isinstance(body, dict) else None
|
||||
)
|
||||
except ValueError:
|
||||
detail = None
|
||||
text = str(detail or response.text or response.reason_phrase).strip()
|
||||
return text.replace(api_key, "***")[:300]
|
||||
|
||||
|
||||
async def test_openai_credential(
|
||||
config: CredentialTestRequest,
|
||||
) -> CredentialTestResult:
|
||||
started = time.perf_counter()
|
||||
headers = {"Authorization": f"Bearer {config.api_key}"}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=TEST_TIMEOUT_SECONDS) as client:
|
||||
if config.type == "LLM":
|
||||
response = await client.post(
|
||||
_endpoint(config.api_url, "chat/completions"),
|
||||
headers=headers,
|
||||
json={
|
||||
"model": config.model_id,
|
||||
"messages": [{"role": "user", "content": "Reply with OK."}],
|
||||
"max_tokens": 1,
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
elif config.type == "Embedding":
|
||||
response = await client.post(
|
||||
_endpoint(config.api_url, "embeddings"),
|
||||
headers=headers,
|
||||
json={"model": config.model_id, "input": "ping"},
|
||||
)
|
||||
elif config.type == "ASR":
|
||||
response = await client.post(
|
||||
_endpoint(config.api_url, "audio/transcriptions"),
|
||||
headers=headers,
|
||||
data={
|
||||
"model": config.model_id,
|
||||
**({"language": config.language} if config.language else {}),
|
||||
},
|
||||
files={"file": ("test.wav", _silent_wav(), "audio/wav")},
|
||||
)
|
||||
elif config.type == "TTS":
|
||||
response = await client.post(
|
||||
_endpoint(config.api_url, "audio/speech"),
|
||||
headers=headers,
|
||||
json={
|
||||
"model": config.model_id,
|
||||
"input": "测试",
|
||||
"voice": config.voice,
|
||||
"speed": config.speed,
|
||||
},
|
||||
)
|
||||
else:
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
message="暂不支持该资源类型的连通测试",
|
||||
detail=f"当前仅支持 LLM、Embedding、ASR、TTS,收到 {config.type}",
|
||||
)
|
||||
|
||||
latency_ms = round((time.perf_counter() - started) * 1000)
|
||||
if response.is_success:
|
||||
return CredentialTestResult(
|
||||
ok=True,
|
||||
latency_ms=latency_ms,
|
||||
message="连接成功",
|
||||
detail=f"OpenAI 兼容接口响应正常(HTTP {response.status_code})",
|
||||
)
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
latency_ms=latency_ms,
|
||||
message=f"连接失败(HTTP {response.status_code})",
|
||||
detail=_error_detail(response, config.api_key),
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
latency_ms=round((time.perf_counter() - started) * 1000),
|
||||
message="连接超时",
|
||||
detail=f"服务未在 {TEST_TIMEOUT_SECONDS:g} 秒内响应",
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
return CredentialTestResult(
|
||||
ok=False,
|
||||
latency_ms=round((time.perf_counter() - started) * 1000),
|
||||
message="无法连接到模型服务",
|
||||
detail=str(exc)[:300],
|
||||
)
|
||||
@@ -11,6 +11,17 @@ from models import AssistantConfig
|
||||
from pipecat.services.openai.llm import OpenAILLMService
|
||||
from pipecat.services.openai.stt import OpenAISTTService
|
||||
from pipecat.services.openai.tts import OpenAITTSService
|
||||
from pipecat.transcriptions.language import Language
|
||||
|
||||
|
||||
def _language(value: str) -> Language | None:
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return Language(value)
|
||||
except ValueError:
|
||||
logger.warning(f"忽略不支持的 ASR language: {value}")
|
||||
return None
|
||||
|
||||
|
||||
def create_stt(cfg: AssistantConfig):
|
||||
@@ -22,6 +33,7 @@ def create_stt(cfg: AssistantConfig):
|
||||
api_key=cfg.stt_api_key or config.STT_API_KEY,
|
||||
base_url=cfg.stt_base_url or config.STT_BASE_URL,
|
||||
model=cfg.asr or config.STT_MODEL,
|
||||
language=_language(cfg.stt_language),
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +53,7 @@ def create_tts(cfg: AssistantConfig):
|
||||
base_url=cfg.tts_base_url or config.TTS_BASE_URL,
|
||||
model=config.TTS_MODEL,
|
||||
voice=cfg.voice or config.TTS_VOICE,
|
||||
speed=cfg.tts_speed,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user