Package a clean shallow main checkout with credential-free Gitea remote, localize the China launcher, and set the new-install Responses endpoint.
150 lines
5.7 KiB
Python
150 lines
5.7 KiB
Python
# Copyright 2026 Google LLC
|
||
#
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
|
||
"""Validation utility to verify LLM and Vision API keys against live provider endpoints."""
|
||
|
||
import httpx
|
||
|
||
from artemis.config.settings import settings
|
||
|
||
from artemis.utils.logger import get_logger
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
# Minimal 1x1 PNG transparent pixel base64 for Vision API testing
|
||
_TINY_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||
|
||
|
||
def _extract_error_message(resp: httpx.Response) -> str:
|
||
"""Extract a human-friendly error message from a provider HTTP response."""
|
||
try:
|
||
data = resp.json()
|
||
if isinstance(data, dict):
|
||
# Google style: {"error": {"message": "..."}}
|
||
err_obj = data.get("error")
|
||
if isinstance(err_obj, dict):
|
||
msg = err_obj.get("message")
|
||
if msg:
|
||
return str(msg)
|
||
elif isinstance(err_obj, str):
|
||
return err_obj
|
||
|
||
# OpenAI / Anthropic / OpenRouter style
|
||
if "message" in data:
|
||
return str(data["message"])
|
||
if "detail" in data:
|
||
return str(data["detail"])
|
||
except ValueError:
|
||
# Response body is not valid JSON; fall through to the text fallback.
|
||
pass
|
||
|
||
# Fallback to response text or HTTP status
|
||
text = resp.text.strip()
|
||
if text and len(text) < 200:
|
||
return text
|
||
return f"HTTP {resp.status_code}: {resp.reason_phrase}"
|
||
|
||
|
||
async def validate_api_key(
|
||
provider: str,
|
||
api_key: str,
|
||
base_url: str | None = None,
|
||
timeout: float | None = None,
|
||
) -> tuple[bool, str]:
|
||
"""Tests if the provided API key is valid and usable with the corresponding provider.
|
||
|
||
Args:
|
||
provider: Provider identifier (e.g. 'google', 'gemini', 'ocr', 'openai', 'anthropic', etc.)
|
||
api_key: Secret API key string.
|
||
base_url: Optional custom endpoint URL.
|
||
timeout: Request timeout in seconds.
|
||
|
||
Returns:
|
||
tuple[bool, str]: (is_valid, descriptive_message)
|
||
"""
|
||
clean_provider = provider.strip().lower()
|
||
clean_key = api_key.strip()
|
||
|
||
if not clean_key:
|
||
return False, "API key cannot be empty."
|
||
|
||
# Placeholders / default template values should be rejected
|
||
if clean_key in (
|
||
"API_KEY",
|
||
"your_gemini_api_key_here",
|
||
"your_google_cloud_vision_api_key_here",
|
||
"your_openai_api_key_here",
|
||
"your_anthropic_api_key_here",
|
||
):
|
||
return False, "Please enter a real API key instead of the placeholder."
|
||
|
||
try:
|
||
async with httpx.AsyncClient(
|
||
timeout=timeout if timeout is not None else settings.ARTEMIS_CREDENTIAL_TIMEOUT_SECONDS
|
||
) as client:
|
||
if clean_provider in ("ocr", "vision", "google_vision"):
|
||
# Test Google Cloud Vision OCR API
|
||
url = "https://vision.googleapis.com/v1/images:annotate"
|
||
payload = {
|
||
"requests": [
|
||
{
|
||
"image": {"content": _TINY_PNG_B64},
|
||
"features": [{"type": "TEXT_DETECTION"}],
|
||
}
|
||
]
|
||
}
|
||
resp = await client.post(
|
||
url,
|
||
json=payload,
|
||
headers={"Content-Type": "application/json", "x-goog-api-key": clean_key},
|
||
)
|
||
if resp.status_code == 200:
|
||
return True, "Google Cloud Vision OCR API key verified successfully."
|
||
err_msg = _extract_error_message(resp)
|
||
return False, f"Vision OCR API verification failed ({resp.status_code}): {err_msg}"
|
||
|
||
elif clean_provider == "openai":
|
||
base = (
|
||
base_url or settings.OPENAI_BASE_URL or "https://api-n-cd.playones.com/v1"
|
||
).rstrip("/")
|
||
resp = await client.post(
|
||
f"{base}/responses",
|
||
headers={"Authorization": f"Bearer {clean_key}"},
|
||
json={
|
||
"model": settings.OPENAI_MODEL,
|
||
"input": "Reply with OK.",
|
||
"max_output_tokens": 16,
|
||
"store": False,
|
||
},
|
||
)
|
||
if resp.status_code == 200 and resp.json().get("output"):
|
||
return True, "OpenAI Responses 连接验证成功。"
|
||
return False, f"Responses 验证失败(HTTP {resp.status_code})"
|
||
|
||
else:
|
||
return False, "仅支持 OpenAI Responses 和可选 OCR 凭据。"
|
||
|
||
except httpx.TimeoutException:
|
||
logger.warning(f"Timeout verifying API key for {provider}")
|
||
return (
|
||
False,
|
||
f"Connection to {provider.capitalize()} API timed out. Please check network/proxy settings.",
|
||
)
|
||
except httpx.RequestError as exc:
|
||
logger.warning(f"Network error verifying API key for {provider}: {exc}")
|
||
return False, f"Network error connecting to {provider.capitalize()} API: {exc}"
|
||
except Exception as exc:
|
||
logger.error(f"Unexpected error validating API key for {provider}: {exc}")
|
||
return False, f"Validation error: {exc}"
|