"""OpenAI 兼容模型凭证的最小连通测试。""" from __future__ import annotations import io import time import wave import httpx from schemas import CredentialTestRequest, CredentialTestResult from services.pipecat.xfyun_config import parse_xfyun_credential 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, "response_format": "pcm", "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], ) def test_xfyun_credential(config: CredentialTestRequest) -> CredentialTestResult: """Validate the Xfyun credential packed into the existing api_key field. Actual signed-WebSocket synthesis/recognition is exercised by the voice pipeline; this check deliberately avoids consuming provider quota. """ try: parse_xfyun_credential(config.api_key) except ValueError as exc: return CredentialTestResult( ok=False, message="讯飞凭证格式无效", detail=str(exc), ) return CredentialTestResult( ok=True, message="讯飞凭证格式有效", detail="请在语音测试页验证签名、识别和合成链路", )