feat(embedding): 支持 OpenAI 兼容 API 并增强错误处理

- 更新 OpenAI 提供商描述以支持兼容 API 服务商
- 前端添加兼容 API 使用引导说明
- 后端 QwenEmbedding 添加 API 密钥验证和错误处理
This commit is contained in:
lintsinghua 2025-12-19 16:37:39 +08:00
parent 9eddef589a
commit 4d7abae245
3 changed files with 60 additions and 25 deletions

View File

@ -76,8 +76,8 @@ class TestEmbeddingResponse(BaseModel):
EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
EmbeddingProvider(
id="openai",
name="OpenAI",
description="OpenAI 官方嵌入模型,高质量、稳定",
name="OpenAI (兼容 DeepSeek/Moonshot/智谱 等)",
description="OpenAI 官方或兼容 API填写自定义端点可接入其他服务商",
models=[
"text-embedding-3-small",
"text-embedding-3-large",

View File

@ -486,6 +486,12 @@ class QwenEmbedding(EmbeddingProvider):
or getattr(settings, "QWEN_API_KEY", None)
or settings.LLM_API_KEY
)
# 🔥 API 密钥验证
if not self.api_key:
raise ValueError(
"Qwen embedding requires API key. "
"Set EMBEDDING_API_KEY, QWEN_API_KEY or LLM_API_KEY environment variable."
)
# DashScope 兼容 OpenAI 的 embeddings 端点
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
self.model = model
@ -502,41 +508,51 @@ class QwenEmbedding(EmbeddingProvider):
async def embed_texts(self, texts: List[str]) -> List[EmbeddingResult]:
if not texts:
return []
# 与 OpenAI 接口保持一致的截断策略
max_length = 8191
truncated_texts = [text[:max_length] for text in texts]
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self.model,
"input": truncated_texts,
"encoding_format": "float",
}
url = f"{self.base_url.rstrip('/')}/embeddings"
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
usage = data.get("usage", {}) or {}
total_tokens = usage.get("total_tokens") or usage.get("prompt_tokens") or 0
results: List[EmbeddingResult] = []
for item in data.get("data", []):
results.append(EmbeddingResult(
embedding=item["embedding"],
tokens_used=total_tokens // max(len(texts), 1),
model=self.model,
))
return results
try:
async with httpx.AsyncClient(timeout=60) as client:
response = await client.post(url, headers=headers, json=payload)
response.raise_for_status()
data = response.json()
usage = data.get("usage", {}) or {}
total_tokens = usage.get("total_tokens") or usage.get("prompt_tokens") or 0
results: List[EmbeddingResult] = []
for item in data.get("data", []):
results.append(EmbeddingResult(
embedding=item["embedding"],
tokens_used=total_tokens // max(len(texts), 1),
model=self.model,
))
return results
except httpx.HTTPStatusError as e:
logger.error(f"Qwen embedding API error: {e.response.status_code} - {e.response.text}")
raise RuntimeError(f"Qwen embedding API failed: {e.response.status_code}") from e
except httpx.RequestError as e:
logger.error(f"Qwen embedding network error: {e}")
raise RuntimeError(f"Qwen embedding network error: {e}") from e
except Exception as e:
logger.error(f"Qwen embedding unexpected error: {e}")
raise RuntimeError(f"Qwen embedding failed: {e}") from e
class EmbeddingService:

View File

@ -434,7 +434,7 @@ export default function EmbeddingConfigPanel() {
</div>
{/* 说明 */}
<div className="bg-muted border border-border p-4 rounded-lg text-xs space-y-2">
<div className="bg-muted border border-border p-4 rounded-lg text-xs space-y-3">
<p className="font-bold uppercase text-muted-foreground flex items-center gap-2">
<Info className="w-4 h-4 text-sky-400" />
@ -445,6 +445,25 @@ export default function EmbeddingConfigPanel() {
<li> 使 <span className="text-foreground">OpenAI text-embedding-3-small</span> <span className="text-foreground">Ollama</span></li>
<li> </li>
</ul>
{/* OpenAI 兼容 API 引导 */}
<div className="mt-3 pt-3 border-t border-border/50">
<p className="font-bold text-amber-400 flex items-center gap-2 mb-2">
<Zap className="w-4 h-4" />
使 OpenAI API
</p>
<p className="text-muted-foreground mb-2">
OpenAI API使 <span className="text-foreground">openai</span>
</p>
<ul className="text-muted-foreground space-y-1 ml-4">
<li> <span className="text-foreground">DeepSeek</span>: <code className="text-primary bg-primary/10 px-1 rounded">https://api.deepseek.com/v1</code></li>
<li> <span className="text-foreground">Moonshot</span>: <code className="text-primary bg-primary/10 px-1 rounded">https://api.moonshot.cn/v1</code></li>
<li> <span className="text-foreground"> GLM</span>: <code className="text-primary bg-primary/10 px-1 rounded">https://open.bigmodel.cn/api/paas/v4</code></li>
</ul>
<p className="text-muted-foreground mt-2 text-[11px]">
openai API Key
</p>
</div>
</div>
</div>
);