| 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 |
|
|