mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat(fingerprint): 引入 per-key 请求指纹系统,替代全局 TLS 指纹开关
为每个 ProviderAPIKey 生成并持久化独立的请求指纹配置,涵盖 TLS impersonate profile、浏览器 UA、Stainless SDK 头部、Node/Chrome/Electron 版本等维度。 指纹基于 key ID 确定性生成,支持手动编辑和批量重新生成。 - 新增 fingerprint 模块:生成、加载、校验、懒持久化 - 数据库迁移:provider_api_keys 新增 fingerprint JSON 列 - 请求链路注入:handler 基类设置上下文指纹,request_builder 和 envelope 消费 - HTTP Client 支持动态 impersonate profile 选择 - Antigravity 适配器使用指纹覆盖 UA/session/Node 版本 - 前端移除手动 TLS 指纹开关,新增批量 regenerate_fingerprint 操作 - 号池管理 UI 优化:token 缩写格式、blocked 行样式、时间显示改为日期格式
This commit is contained in:
@@ -14,6 +14,8 @@ import uuid
|
||||
# ============== API 端点 ==============
|
||||
# 唯一定义在 core 层,此处 re-export 保持向后兼容
|
||||
from src.core.provider_templates.fixed_providers import ANTIGRAVITY_PROD_URL as PROD_BASE_URL
|
||||
from src.services.provider.fingerprint import resolve_platform_token
|
||||
from src.services.provider.request_context import get_current_fingerprint
|
||||
|
||||
DAILY_BASE_URL = "https://daily-cloudcode-pa.googleapis.com"
|
||||
SANDBOX_BASE_URL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
||||
@@ -39,11 +41,27 @@ def _detect_platform_info() -> str:
|
||||
|
||||
_PLATFORM_INFO = _detect_platform_info()
|
||||
|
||||
|
||||
def _build_antigravity_http_user_agent(
|
||||
*,
|
||||
platform_token: str,
|
||||
version: str,
|
||||
chrome_version: str,
|
||||
electron_version: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"Mozilla/5.0 ({platform_token}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Antigravity/{version} Chrome/{chrome_version} "
|
||||
f"Electron/{electron_version} Safari/537.36"
|
||||
)
|
||||
|
||||
|
||||
# HTTP Header User-Agent(对齐 AM constants.rs: 完整 Electron 浏览器格式)
|
||||
HTTP_USER_AGENT = (
|
||||
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Antigravity/{_FALLBACK_VERSION} Chrome/{_FALLBACK_CHROME} "
|
||||
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
|
||||
HTTP_USER_AGENT = _build_antigravity_http_user_agent(
|
||||
platform_token=_PLATFORM_INFO,
|
||||
version=_FALLBACK_VERSION,
|
||||
chrome_version=_FALLBACK_CHROME,
|
||||
electron_version=_FALLBACK_ELECTRON,
|
||||
)
|
||||
|
||||
# V1InternalRequest.userAgent 字段(固定值)
|
||||
@@ -57,10 +75,11 @@ _ua_version: str = _FALLBACK_VERSION
|
||||
def get_http_user_agent() -> str:
|
||||
"""返回当前 HTTP User-Agent 字符串(对齐 AM Electron UA 格式)。"""
|
||||
with _ua_lock:
|
||||
return (
|
||||
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Antigravity/{_ua_version} Chrome/{_FALLBACK_CHROME} "
|
||||
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
|
||||
return _build_antigravity_http_user_agent(
|
||||
platform_token=_PLATFORM_INFO,
|
||||
version=_ua_version,
|
||||
chrome_version=_FALLBACK_CHROME,
|
||||
electron_version=_FALLBACK_ELECTRON,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,10 +91,11 @@ def update_user_agent_version(version: str) -> None:
|
||||
return
|
||||
with _ua_lock:
|
||||
_ua_version = version
|
||||
HTTP_USER_AGENT = (
|
||||
f"Mozilla/5.0 ({_PLATFORM_INFO}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Antigravity/{_ua_version} Chrome/{_FALLBACK_CHROME} "
|
||||
f"Electron/{_FALLBACK_ELECTRON} Safari/537.36"
|
||||
HTTP_USER_AGENT = _build_antigravity_http_user_agent(
|
||||
platform_token=_PLATFORM_INFO,
|
||||
version=_ua_version,
|
||||
chrome_version=_FALLBACK_CHROME,
|
||||
electron_version=_FALLBACK_ELECTRON,
|
||||
)
|
||||
|
||||
|
||||
@@ -94,16 +114,43 @@ URL_UNAVAILABLE_TTL_SECONDS = 300 # 5 分钟
|
||||
_SESSION_ID = uuid.uuid4().hex # 每次进程启动生成一个固定 session ID
|
||||
|
||||
|
||||
def _normalize_node_version(raw: str | None) -> str:
|
||||
text = str(raw or "").strip()
|
||||
if text.startswith(("v", "V")):
|
||||
text = text[1:]
|
||||
return text or "18.18.2"
|
||||
|
||||
|
||||
def get_v1internal_extra_headers() -> dict[str, str]:
|
||||
"""构建 v1internal 请求需要的额外 header(对齐 AM upstream/client.rs)。"""
|
||||
fp = get_current_fingerprint()
|
||||
with _ua_lock:
|
||||
version = _ua_version
|
||||
|
||||
user_agent = get_http_user_agent()
|
||||
session_id = _SESSION_ID
|
||||
node_version = "18.18.2"
|
||||
|
||||
if fp:
|
||||
user_agent = _build_antigravity_http_user_agent(
|
||||
platform_token=resolve_platform_token(
|
||||
platform_info=fp.platform_info,
|
||||
stainless_os=fp.stainless_os,
|
||||
stainless_arch=fp.stainless_arch,
|
||||
),
|
||||
version=version,
|
||||
chrome_version=fp.chrome_version or _FALLBACK_CHROME,
|
||||
electron_version=fp.electron_version or _FALLBACK_ELECTRON,
|
||||
)
|
||||
session_id = fp.vscode_session_id or _SESSION_ID
|
||||
node_version = _normalize_node_version(fp.node_version)
|
||||
|
||||
return {
|
||||
"User-Agent": get_http_user_agent(),
|
||||
"User-Agent": user_agent,
|
||||
"x-client-name": "antigravity",
|
||||
"x-client-version": version,
|
||||
"x-vscode-sessionid": _SESSION_ID,
|
||||
"x-goog-api-client": "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x",
|
||||
"x-vscode-sessionid": session_id,
|
||||
"x-goog-api-client": f"gl-node/{node_version} fire/0.8.6 grpc/1.10.x",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from src.services.provider.adapters.claude_code.context import (
|
||||
get_claude_code_request_context,
|
||||
set_claude_code_request_context,
|
||||
)
|
||||
from src.services.provider.request_context import get_current_fingerprint
|
||||
|
||||
_SESSION_MARKER = "_session_"
|
||||
_DUMMY_THINKING_SIGNATURE = "skip_thought_signature_validator"
|
||||
@@ -488,6 +489,7 @@ class ClaudeCodeEnvelope:
|
||||
def extra_headers(self) -> dict[str, str] | None:
|
||||
ctx = get_claude_code_request_context()
|
||||
is_stream = bool(ctx.is_stream) if ctx else False
|
||||
fp = get_current_fingerprint()
|
||||
|
||||
headers = dict(CLAUDE_CODE_DEFAULT_HEADERS)
|
||||
headers["Accept"] = DEFAULT_ACCEPT
|
||||
@@ -496,9 +498,17 @@ class ClaudeCodeEnvelope:
|
||||
if is_stream:
|
||||
headers["x-stainless-helper-method"] = STREAM_HELPER_METHOD
|
||||
|
||||
ua = str(getattr(config, "internal_user_agent_claude_cli", "") or "").strip()
|
||||
if ua:
|
||||
headers["User-Agent"] = ua
|
||||
if fp:
|
||||
headers["X-Stainless-Package-Version"] = fp.stainless_package_version
|
||||
headers["X-Stainless-OS"] = fp.stainless_os
|
||||
headers["X-Stainless-Arch"] = fp.stainless_arch
|
||||
headers["X-Stainless-Runtime-Version"] = fp.stainless_runtime_version
|
||||
headers["X-Stainless-Timeout"] = fp.stainless_timeout
|
||||
headers["User-Agent"] = fp.user_agent
|
||||
else:
|
||||
ua = str(getattr(config, "internal_user_agent_claude_cli", "") or "").strip()
|
||||
if ua:
|
||||
headers["User-Agent"] = ua
|
||||
|
||||
return headers
|
||||
|
||||
@@ -586,6 +596,9 @@ class ClaudeCodeEnvelope:
|
||||
is_stream=is_stream,
|
||||
provider_id=provider_id,
|
||||
)
|
||||
fp = get_current_fingerprint()
|
||||
if _ctx.enable_tls_fingerprint and fp:
|
||||
return fp.impersonate
|
||||
return tls_profile
|
||||
|
||||
async def post_wrap_request(self, request_body: dict[str, Any]) -> None:
|
||||
|
||||
358
src/services/provider/fingerprint.py
Normal file
358
src/services/provider/fingerprint.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Per-key request fingerprint generation and lazy persistence helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from src.core.logger import logger
|
||||
|
||||
# curl_cffi impersonate profile pool (Chrome family)
|
||||
CHROME_IMPERSONATE_PROFILES: tuple[str, ...] = (
|
||||
"chrome110",
|
||||
"chrome116",
|
||||
"chrome119",
|
||||
"chrome120",
|
||||
"chrome123",
|
||||
"chrome124",
|
||||
"chrome131",
|
||||
"chrome133",
|
||||
)
|
||||
|
||||
KNOWN_IMPERSONATE_PROFILES: frozenset[str] = frozenset(CHROME_IMPERSONATE_PROFILES)
|
||||
|
||||
_CHROME_VERSION_BY_PROFILE: dict[str, str] = {
|
||||
"chrome110": "110.0.5481.177",
|
||||
"chrome116": "116.0.5845.188",
|
||||
"chrome119": "119.0.6045.214",
|
||||
"chrome120": "120.0.6099.216",
|
||||
"chrome123": "123.0.6312.122",
|
||||
"chrome124": "124.0.6367.243",
|
||||
"chrome131": "131.0.6778.265",
|
||||
"chrome133": "133.0.6943.142",
|
||||
}
|
||||
|
||||
_PLATFORM_VARIANTS: tuple[tuple[str, str, str, str], ...] = (
|
||||
("Linux", "x64", "X11; Linux x86_64", "Linux x86_64"),
|
||||
("Linux", "arm64", "X11; Linux arm64", "Linux arm64"),
|
||||
("Windows", "x64", "Windows NT 10.0; Win64; x64", "Windows x64"),
|
||||
("MacOS", "x64", "Macintosh; Intel Mac OS X 10_15_7", "Darwin x64"),
|
||||
("MacOS", "arm64", "Macintosh; ARM Mac OS X 14_0_0", "Darwin arm64"),
|
||||
)
|
||||
|
||||
_STAINLESS_PACKAGE_VERSIONS: tuple[str, ...] = ("0.68.0", "0.69.0", "0.70.0", "0.71.0")
|
||||
_NODE_VERSIONS: tuple[str, ...] = ("v20.18.1", "v22.12.0", "v22.14.0", "v24.13.0")
|
||||
_ELECTRON_VERSIONS: tuple[str, ...] = ("35.5.1", "36.7.1", "37.3.0", "38.7.0", "39.2.3")
|
||||
_STAINLESS_TIMEOUTS: tuple[str, ...] = ("600", "900")
|
||||
|
||||
_PENDING_LAZY_PERSIST: set[str] = set()
|
||||
_PENDING_LAZY_PERSIST_LOCK = threading.Lock()
|
||||
_PENDING_LAZY_PERSIST_MAX = 2000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FingerprintProfile:
|
||||
# TLS layer
|
||||
impersonate: str
|
||||
# Claude Code / Claude Chat feature dimensions
|
||||
stainless_package_version: str
|
||||
stainless_os: str
|
||||
stainless_arch: str
|
||||
stainless_runtime_version: str
|
||||
stainless_timeout: str
|
||||
# Generic dimensions
|
||||
user_agent: str
|
||||
node_version: str
|
||||
chrome_version: str
|
||||
electron_version: str
|
||||
vscode_session_id: str
|
||||
platform_info: str
|
||||
|
||||
|
||||
def _build_rng(seed: str | None) -> random.Random:
|
||||
if seed is None:
|
||||
return random.Random(secrets.randbits(64))
|
||||
digest = hashlib.sha256(seed.encode("utf-8")).digest()
|
||||
return random.Random(int.from_bytes(digest[:8], "big", signed=False))
|
||||
|
||||
|
||||
def _normalize_impersonate(raw: Any, fallback: str) -> str:
|
||||
value = str(raw or "").strip().lower()
|
||||
return value if value in KNOWN_IMPERSONATE_PROFILES else fallback
|
||||
|
||||
|
||||
def _build_user_agent(
|
||||
platform_token: str,
|
||||
chrome_version: str,
|
||||
electron_version: str,
|
||||
) -> str:
|
||||
return (
|
||||
f"Mozilla/5.0 ({platform_token}) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
f"Chrome/{chrome_version} Electron/{electron_version} Safari/537.36"
|
||||
)
|
||||
|
||||
|
||||
def resolve_platform_token(
|
||||
fp: dict[str, str] | None = None,
|
||||
*,
|
||||
platform_info: str | None = None,
|
||||
stainless_os: str | None = None,
|
||||
stainless_arch: str | None = None,
|
||||
) -> str:
|
||||
"""Resolve a platform token string from fingerprint dict or explicit kwargs."""
|
||||
if fp is not None:
|
||||
os_name = str(fp.get("stainless_os") or "").lower()
|
||||
arch = str(fp.get("stainless_arch") or "").lower()
|
||||
info = str(fp.get("platform_info") or "").lower()
|
||||
else:
|
||||
os_name = str(stainless_os or "").lower()
|
||||
arch = str(stainless_arch or "").lower()
|
||||
info = str(platform_info or "").lower()
|
||||
|
||||
if os_name.startswith("win") or "windows" in info:
|
||||
return "Windows NT 10.0; Win64; x64"
|
||||
if os_name in {"darwin", "mac", "macos"} or "darwin" in info:
|
||||
return (
|
||||
"Macintosh; ARM Mac OS X 14_0_0"
|
||||
if arch in {"arm64", "aarch64"}
|
||||
else "Macintosh; Intel Mac OS X 10_15_7"
|
||||
)
|
||||
return "X11; Linux arm64" if arch in {"arm64", "aarch64"} else "X11; Linux x86_64"
|
||||
|
||||
|
||||
def _normalize_text(value: Any, fallback: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
return text or fallback
|
||||
|
||||
|
||||
def _sanitize_fingerprint_dict(raw: dict[str, Any], key_id: str) -> dict[str, str]:
|
||||
generated = generate_fingerprint(seed=key_id or None)
|
||||
fp: dict[str, str] = {k: str(v) for k, v in generated.items()}
|
||||
for key, value in raw.items():
|
||||
if isinstance(value, str) and value.strip():
|
||||
fp[key] = value.strip()
|
||||
|
||||
fp["impersonate"] = _normalize_impersonate(fp.get("impersonate"), generated["impersonate"])
|
||||
fp["chrome_version"] = _normalize_text(
|
||||
fp.get("chrome_version"),
|
||||
_CHROME_VERSION_BY_PROFILE.get(fp["impersonate"], generated["chrome_version"]),
|
||||
)
|
||||
fp["node_version"] = _normalize_text(fp.get("node_version"), generated["node_version"])
|
||||
fp["electron_version"] = _normalize_text(
|
||||
fp.get("electron_version"),
|
||||
generated["electron_version"],
|
||||
)
|
||||
fp["stainless_package_version"] = _normalize_text(
|
||||
fp.get("stainless_package_version"),
|
||||
generated["stainless_package_version"],
|
||||
)
|
||||
fp["stainless_os"] = _normalize_text(fp.get("stainless_os"), generated["stainless_os"])
|
||||
fp["stainless_arch"] = _normalize_text(fp.get("stainless_arch"), generated["stainless_arch"])
|
||||
fp["stainless_runtime_version"] = _normalize_text(
|
||||
fp.get("stainless_runtime_version"),
|
||||
generated["stainless_runtime_version"],
|
||||
)
|
||||
fp["stainless_timeout"] = _normalize_text(
|
||||
fp.get("stainless_timeout"),
|
||||
generated["stainless_timeout"],
|
||||
)
|
||||
fp["platform_info"] = _normalize_text(fp.get("platform_info"), generated["platform_info"])
|
||||
fp["vscode_session_id"] = _normalize_text(
|
||||
fp.get("vscode_session_id"),
|
||||
generated["vscode_session_id"],
|
||||
)
|
||||
|
||||
if not str(fp.get("user_agent") or "").strip():
|
||||
fp["user_agent"] = _build_user_agent(
|
||||
resolve_platform_token(fp),
|
||||
fp["chrome_version"],
|
||||
fp["electron_version"],
|
||||
)
|
||||
|
||||
return fp
|
||||
|
||||
|
||||
def generate_fingerprint(seed: str | None = None) -> dict[str, str]:
|
||||
"""Generate a serializable fingerprint profile dict."""
|
||||
rng = _build_rng(seed)
|
||||
|
||||
impersonate = rng.choice(CHROME_IMPERSONATE_PROFILES)
|
||||
chrome_version = _CHROME_VERSION_BY_PROFILE.get(impersonate, "120.0.6099.216")
|
||||
node_version = rng.choice(_NODE_VERSIONS)
|
||||
electron_version = rng.choice(_ELECTRON_VERSIONS)
|
||||
stainless_os, stainless_arch, platform_token, platform_info = rng.choice(_PLATFORM_VARIANTS)
|
||||
|
||||
if seed is None:
|
||||
vscode_session_id = uuid.uuid4().hex
|
||||
else:
|
||||
vscode_session_id = uuid.uuid5(uuid.NAMESPACE_URL, f"aether:fingerprint:{seed}").hex
|
||||
|
||||
return {
|
||||
"impersonate": impersonate,
|
||||
"stainless_package_version": rng.choice(_STAINLESS_PACKAGE_VERSIONS),
|
||||
"stainless_os": stainless_os,
|
||||
"stainless_arch": stainless_arch,
|
||||
"stainless_runtime_version": node_version,
|
||||
"stainless_timeout": rng.choice(_STAINLESS_TIMEOUTS),
|
||||
"node_version": node_version,
|
||||
"chrome_version": chrome_version,
|
||||
"electron_version": electron_version,
|
||||
"vscode_session_id": vscode_session_id,
|
||||
"platform_info": platform_info,
|
||||
"user_agent": _build_user_agent(platform_token, chrome_version, electron_version),
|
||||
}
|
||||
|
||||
|
||||
def _dict_to_profile(d: dict[str, str]) -> FingerprintProfile:
|
||||
return FingerprintProfile(
|
||||
impersonate=d["impersonate"],
|
||||
stainless_package_version=d["stainless_package_version"],
|
||||
stainless_os=d["stainless_os"],
|
||||
stainless_arch=d["stainless_arch"],
|
||||
stainless_runtime_version=d["stainless_runtime_version"],
|
||||
stainless_timeout=d["stainless_timeout"],
|
||||
user_agent=d["user_agent"],
|
||||
node_version=d["node_version"],
|
||||
chrome_version=d["chrome_version"],
|
||||
electron_version=d["electron_version"],
|
||||
vscode_session_id=d["vscode_session_id"],
|
||||
platform_info=d["platform_info"],
|
||||
)
|
||||
|
||||
|
||||
def load_fingerprint(raw: dict[str, Any] | None, key_id: str) -> FingerprintProfile:
|
||||
"""Load fingerprint from DB JSON with deterministic fallback by key_id."""
|
||||
if raw and isinstance(raw, dict):
|
||||
normalized = _sanitize_fingerprint_dict(raw, key_id)
|
||||
else:
|
||||
normalized = generate_fingerprint(seed=key_id or None)
|
||||
return _dict_to_profile(normalized)
|
||||
|
||||
|
||||
def serialize_fingerprint(fp: FingerprintProfile) -> dict[str, str]:
|
||||
return {k: str(v) for k, v in asdict(fp).items()}
|
||||
|
||||
|
||||
def normalize_fingerprint(raw: dict[str, Any], key_id: str) -> dict[str, str]:
|
||||
return serialize_fingerprint(load_fingerprint(raw, key_id))
|
||||
|
||||
|
||||
def _persist_fingerprint_if_missing_sync(key_id: str, fp: dict[str, str]) -> None:
|
||||
from src.database import create_session
|
||||
from src.models.database import ProviderAPIKey
|
||||
|
||||
db = create_session()
|
||||
try:
|
||||
updated = (
|
||||
db.query(ProviderAPIKey)
|
||||
.filter(
|
||||
ProviderAPIKey.id == key_id,
|
||||
ProviderAPIKey.fingerprint.is_(None),
|
||||
)
|
||||
.update(
|
||||
{
|
||||
ProviderAPIKey.fingerprint: fp,
|
||||
ProviderAPIKey.updated_at: datetime.now(timezone.utc),
|
||||
},
|
||||
synchronize_session=False,
|
||||
)
|
||||
)
|
||||
if updated > 0:
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
db.rollback()
|
||||
logger.debug("lazy fingerprint persist failed for key {}: {}", key_id[:8], str(exc))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _mark_pending_persist(key_id: str) -> bool:
|
||||
with _PENDING_LAZY_PERSIST_LOCK:
|
||||
if key_id in _PENDING_LAZY_PERSIST:
|
||||
return False
|
||||
if len(_PENDING_LAZY_PERSIST) >= _PENDING_LAZY_PERSIST_MAX:
|
||||
return False
|
||||
_PENDING_LAZY_PERSIST.add(key_id)
|
||||
return True
|
||||
|
||||
|
||||
def _clear_pending_persist(key_id: str) -> None:
|
||||
with _PENDING_LAZY_PERSIST_LOCK:
|
||||
_PENDING_LAZY_PERSIST.discard(key_id)
|
||||
|
||||
|
||||
def schedule_lazy_fingerprint_persist(key_id: str, fp: dict[str, str]) -> None:
|
||||
"""Persist generated fingerprint in background when key.fingerprint is missing."""
|
||||
key_id = str(key_id or "").strip()
|
||||
if not key_id:
|
||||
return
|
||||
if not _mark_pending_persist(key_id):
|
||||
return
|
||||
|
||||
payload = dict(fp)
|
||||
|
||||
async def _persist_async() -> None:
|
||||
try:
|
||||
await asyncio.to_thread(_persist_fingerprint_if_missing_sync, key_id, payload)
|
||||
finally:
|
||||
_clear_pending_persist(key_id)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
try:
|
||||
_persist_fingerprint_if_missing_sync(key_id, payload)
|
||||
finally:
|
||||
_clear_pending_persist(key_id)
|
||||
return
|
||||
|
||||
task = loop.create_task(_persist_async())
|
||||
|
||||
def _on_done(done_task: asyncio.Task[None]) -> None:
|
||||
try:
|
||||
done_task.result()
|
||||
except Exception as exc:
|
||||
logger.debug("lazy fingerprint background task failed: {}", str(exc))
|
||||
|
||||
task.add_done_callback(_on_done)
|
||||
|
||||
|
||||
def ensure_key_fingerprint(
|
||||
key: Any,
|
||||
*,
|
||||
persist_if_missing: bool = False,
|
||||
) -> FingerprintProfile:
|
||||
"""Get a key fingerprint, generating deterministic fallback if missing."""
|
||||
key_id = str(getattr(key, "id", "") or "").strip()
|
||||
raw = getattr(key, "fingerprint", None)
|
||||
|
||||
if isinstance(raw, dict) and raw:
|
||||
return load_fingerprint(raw, key_id)
|
||||
|
||||
generated = generate_fingerprint(seed=key_id or None)
|
||||
|
||||
if persist_if_missing and key_id:
|
||||
schedule_lazy_fingerprint_persist(key_id, generated)
|
||||
|
||||
return _dict_to_profile(generated)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CHROME_IMPERSONATE_PROFILES",
|
||||
"FingerprintProfile",
|
||||
"KNOWN_IMPERSONATE_PROFILES",
|
||||
"ensure_key_fingerprint",
|
||||
"generate_fingerprint",
|
||||
"load_fingerprint",
|
||||
"normalize_fingerprint",
|
||||
"resolve_platform_token",
|
||||
"schedule_lazy_fingerprint_persist",
|
||||
"serialize_fingerprint",
|
||||
]
|
||||
@@ -10,11 +10,19 @@ per request.
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.services.provider.fingerprint import FingerprintProfile
|
||||
|
||||
_selected_base_url: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
||||
"provider_selected_base_url",
|
||||
default=None,
|
||||
)
|
||||
_current_fingerprint: contextvars.ContextVar[FingerprintProfile | None] = contextvars.ContextVar(
|
||||
"provider_current_fingerprint",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def set_selected_base_url(url: str | None) -> None:
|
||||
@@ -25,4 +33,17 @@ def get_selected_base_url() -> str | None:
|
||||
return _selected_base_url.get()
|
||||
|
||||
|
||||
__all__ = ["get_selected_base_url", "set_selected_base_url"]
|
||||
def set_current_fingerprint(fp: FingerprintProfile | None) -> None:
|
||||
_current_fingerprint.set(fp)
|
||||
|
||||
|
||||
def get_current_fingerprint() -> FingerprintProfile | None:
|
||||
return _current_fingerprint.get()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_current_fingerprint",
|
||||
"get_selected_base_url",
|
||||
"set_current_fingerprint",
|
||||
"set_selected_base_url",
|
||||
]
|
||||
|
||||
@@ -23,6 +23,7 @@ from src.models.endpoint_models import (
|
||||
EndpointAPIKeyResponse,
|
||||
EndpointAPIKeyUpdate,
|
||||
)
|
||||
from src.services.provider.fingerprint import generate_fingerprint, normalize_fingerprint
|
||||
from src.services.provider_keys.auth_type import normalize_auth_type
|
||||
from src.services.provider_keys.duplicate_check import check_duplicate_key
|
||||
from src.services.provider_keys.key_side_effects import (
|
||||
@@ -282,6 +283,12 @@ def _prepare_update_key_payload(
|
||||
else:
|
||||
update_data["proxy"] = key_data.proxy.model_dump(exclude_none=True)
|
||||
|
||||
if "fingerprint" in key_data.model_fields_set:
|
||||
if key_data.fingerprint is None:
|
||||
update_data["fingerprint"] = None
|
||||
else:
|
||||
update_data["fingerprint"] = normalize_fingerprint(key_data.fingerprint, key_id)
|
||||
|
||||
return _UpdateKeyPreparation(
|
||||
update_data=update_data,
|
||||
auto_fetch_enabled_before=auto_fetch_enabled_before,
|
||||
@@ -335,8 +342,10 @@ def _prepare_create_key_payload(
|
||||
if key_data.auth_config:
|
||||
encrypted_auth_config = crypto_service.encrypt(json.dumps(key_data.auth_config))
|
||||
|
||||
new_key_id = str(uuid.uuid4())
|
||||
|
||||
new_key = ProviderAPIKey(
|
||||
id=str(uuid.uuid4()),
|
||||
id=new_key_id,
|
||||
provider_id=provider_id,
|
||||
api_formats=key_data.api_formats,
|
||||
auth_type=auth_type,
|
||||
@@ -359,6 +368,7 @@ def _prepare_create_key_payload(
|
||||
model_exclude_patterns=(
|
||||
key_data.model_exclude_patterns if key_data.model_exclude_patterns else None
|
||||
),
|
||||
fingerprint=generate_fingerprint(seed=new_key_id),
|
||||
request_count=0,
|
||||
success_count=0,
|
||||
error_count=0,
|
||||
|
||||
Reference in New Issue
Block a user