ppt-web / src /landppt /services /provider_test_utils.py
26fwyzpz6f-max
Clean deploy without binary files
6aecb2e
Raw
History Blame Contribute Delete
5.64 kB
from __future__ import annotations
from typing import Any, Dict, Tuple
DEFAULT_GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com"
DEFAULT_GOOGLE_MODEL = "gemini-2.5-flash"
DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
DEFAULT_ANTHROPIC_MODEL = "claude-3-5-sonnet-20241022"
def normalize_provider_name(provider: Any) -> str:
"""统一提供商名称,避免 gemini/google 别名在配置键和路由分支上跑偏。"""
normalized = str(provider or "").strip().lower()
return "google" if normalized == "gemini" else normalized
def normalize_google_test_base_url(base_url: str) -> str:
"""Google 连接测试必须使用 API 根路径,不能把 /v1 或 /v1beta 再拼一层。"""
normalized = str(base_url or "").strip()
if not normalized:
return DEFAULT_GOOGLE_BASE_URL
if "://" not in normalized:
normalized = f"https://{normalized}"
normalized = normalized.rstrip("/")
lowered = normalized.lower()
for suffix in ("/v1beta", "/v1"):
if lowered.endswith(suffix):
normalized = normalized[: -len(suffix)]
break
return normalized.rstrip("/")
def build_google_generate_content_url(base_url: str, model: str) -> str:
"""统一生成 Gemini generateContent 地址,兼容官方域名和带前缀的代理地址。"""
normalized_base_url = normalize_google_test_base_url(base_url)
normalized_model = str(model or DEFAULT_GOOGLE_MODEL).strip() or DEFAULT_GOOGLE_MODEL
return f"{normalized_base_url}/v1beta/models/{normalized_model}:generateContent"
def build_google_test_payload(prompt: str) -> Dict[str, Any]:
"""Google 测试请求走原生 Gemini 格式,避免误用 OpenAI 兼容协议。"""
return {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": 0,
},
}
def extract_google_test_result(data: Dict[str, Any]) -> Tuple[str, Dict[str, int]]:
"""抽取 Gemini 文本与用量,兼容官方字段和少量代理的变体字段。"""
response_text = ""
candidates = data.get("candidates") or []
if candidates:
candidate = candidates[0] or {}
candidate_content = candidate.get("content") or {}
parts = candidate_content.get("parts") or []
texts = [part.get("text", "") for part in parts if isinstance(part, dict) and part.get("text")]
if texts:
response_text = "".join(texts)
else:
response_text = candidate.get("text") or ""
usage_metadata = data.get("usageMetadata") or data.get("usage_metadata") or {}
usage = {
"prompt_tokens": int(usage_metadata.get("promptTokenCount") or 0),
"completion_tokens": int(usage_metadata.get("candidatesTokenCount") or 0),
"total_tokens": int(usage_metadata.get("totalTokenCount") or 0),
}
return response_text, usage
def normalize_anthropic_test_base_url(base_url: str) -> str:
"""Anthropic 测试固定走 /v1/messages,避免不同入口各自拼接造成偏差。"""
normalized = str(base_url or "").strip()
if not normalized:
normalized = DEFAULT_ANTHROPIC_BASE_URL
if "://" not in normalized:
normalized = f"https://{normalized}"
normalized = normalized.rstrip("/")
if not normalized.endswith("/v1"):
normalized = f"{normalized}/v1"
return normalized
def build_anthropic_messages_url(base_url: str) -> str:
"""统一生成 Anthropic messages 地址。"""
return f"{normalize_anthropic_test_base_url(base_url)}/messages"
def build_anthropic_test_payload(prompt: str) -> Dict[str, Any]:
"""Anthropic 测试使用官方 messages 协议。"""
return {
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
}
def extract_anthropic_test_result(data: Dict[str, Any]) -> Tuple[str, Dict[str, int]]:
"""把 Anthropic 响应统一转换成前端可复用的文本和 token 统计结构。"""
content = data.get("content") or []
response_text = ""
if content and isinstance(content[0], dict):
response_text = str(content[0].get("text") or "")
usage_data = data.get("usage") or {}
prompt_tokens = int(usage_data.get("input_tokens") or 0)
completion_tokens = int(usage_data.get("output_tokens") or 0)
usage = {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
}
return response_text, usage
def extract_openai_compatible_test_result(
data: Dict[str, Any],
*,
use_responses_api: bool,
) -> Tuple[str, Dict[str, int]]:
"""把 OpenAI 兼容响应统一成前端需要的预览文本与用量结构。"""
if use_responses_api:
usage_data = data.get("usage") or {}
usage = {
"prompt_tokens": int(usage_data.get("input_tokens") or 0),
"completion_tokens": int(usage_data.get("output_tokens") or 0),
"total_tokens": int(usage_data.get("total_tokens") or 0),
}
return str(data.get("output_text") or ""), usage
response_text = ""
choices = data.get("choices") or []
if choices and isinstance(choices[0], dict):
response_text = str(((choices[0].get("message") or {}).get("content")) or "")
usage_data = data.get("usage") or {}
usage = {
"prompt_tokens": int(usage_data.get("prompt_tokens") or 0),
"completion_tokens": int(usage_data.get("completion_tokens") or 0),
"total_tokens": int(usage_data.get("total_tokens") or 0),
}
return response_text, usage