feat(embedding): 支持 OpenAI 兼容 API 并增强错误处理
- 更新 OpenAI 提供商描述以支持兼容 API 服务商 - 前端添加兼容 API 使用引导说明 - 后端 QwenEmbedding 添加 API 密钥验证和错误处理
This commit is contained in:
parent
9eddef589a
commit
4d7abae245
|
|
@ -76,8 +76,8 @@ class TestEmbeddingResponse(BaseModel):
|
||||||
EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
|
EMBEDDING_PROVIDERS: List[EmbeddingProvider] = [
|
||||||
EmbeddingProvider(
|
EmbeddingProvider(
|
||||||
id="openai",
|
id="openai",
|
||||||
name="OpenAI",
|
name="OpenAI (兼容 DeepSeek/Moonshot/智谱 等)",
|
||||||
description="OpenAI 官方嵌入模型,高质量、稳定",
|
description="OpenAI 官方或兼容 API,填写自定义端点可接入其他服务商",
|
||||||
models=[
|
models=[
|
||||||
"text-embedding-3-small",
|
"text-embedding-3-small",
|
||||||
"text-embedding-3-large",
|
"text-embedding-3-large",
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,12 @@ class QwenEmbedding(EmbeddingProvider):
|
||||||
or getattr(settings, "QWEN_API_KEY", None)
|
or getattr(settings, "QWEN_API_KEY", None)
|
||||||
or settings.LLM_API_KEY
|
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 端点
|
# DashScope 兼容 OpenAI 的 embeddings 端点
|
||||||
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
self.base_url = base_url or "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
@ -502,41 +508,51 @@ class QwenEmbedding(EmbeddingProvider):
|
||||||
async def embed_texts(self, texts: List[str]) -> List[EmbeddingResult]:
|
async def embed_texts(self, texts: List[str]) -> List[EmbeddingResult]:
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 与 OpenAI 接口保持一致的截断策略
|
# 与 OpenAI 接口保持一致的截断策略
|
||||||
max_length = 8191
|
max_length = 8191
|
||||||
truncated_texts = [text[:max_length] for text in texts]
|
truncated_texts = [text[:max_length] for text in texts]
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"input": truncated_texts,
|
"input": truncated_texts,
|
||||||
"encoding_format": "float",
|
"encoding_format": "float",
|
||||||
}
|
}
|
||||||
|
|
||||||
url = f"{self.base_url.rstrip('/')}/embeddings"
|
url = f"{self.base_url.rstrip('/')}/embeddings"
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60) as client:
|
try:
|
||||||
response = await client.post(url, headers=headers, json=payload)
|
async with httpx.AsyncClient(timeout=60) as client:
|
||||||
response.raise_for_status()
|
response = await client.post(url, headers=headers, json=payload)
|
||||||
data = response.json()
|
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
|
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: List[EmbeddingResult] = []
|
||||||
results.append(EmbeddingResult(
|
for item in data.get("data", []):
|
||||||
embedding=item["embedding"],
|
results.append(EmbeddingResult(
|
||||||
tokens_used=total_tokens // max(len(texts), 1),
|
embedding=item["embedding"],
|
||||||
model=self.model,
|
tokens_used=total_tokens // max(len(texts), 1),
|
||||||
))
|
model=self.model,
|
||||||
|
))
|
||||||
return results
|
|
||||||
|
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:
|
class EmbeddingService:
|
||||||
|
|
|
||||||
|
|
@ -434,7 +434,7 @@ export default function EmbeddingConfigPanel() {
|
||||||
</div>
|
</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">
|
<p className="font-bold uppercase text-muted-foreground flex items-center gap-2">
|
||||||
<Info className="w-4 h-4 text-sky-400" />
|
<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>• 推荐使用 <span className="text-foreground">OpenAI text-embedding-3-small</span> 或本地 <span className="text-foreground">Ollama</span></li>
|
||||||
<li>• 向量维度影响存储空间和检索精度</li>
|
<li>• 向量维度影响存储空间和检索精度</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue