XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
UTF-8
from __future__ import annotations

import json
import re

from ...errors import RateLimitError, ResponseError


BARD_ERROR_PATTERN = re.compile(r"BardErrorInfo\s*\[(\d+)\]")
LENGTH_MARKER_PATTERN = re.compile(r"(\d+)\n")
ACCOUNT_STATUS_AVAILABLE = 1000
ACCOUNT_STATUS_UNAUTHENTICATED = 1016
MODEL_HEADER_KEY = "x-goog-ext-525001261-jspb"
MODEL_HEADER_AUXILIARY = {
    "x-goog-ext-73010989-jspb": "[0]",
    "x-goog-ext-73010990-jspb": "[0]",
}
MODEL_FAMILIES = {
    "gemini-3.6-flash": "flash",
    "gemini-3.5-flash-lite": "flash",
    "gemini-3.1-pro": "pro",
}
ANONYMOUS_MODELS = {"gemini-3.6-flash", "gemini-3.5-flash-lite"}
KNOWN_MODEL_IDS = {
    "fbb127bbb056c959": "flash",
    "5bf011840784117a": "thinking",
    "9d8ca3786ebdfbea": "pro",
}
GEMINI_ERROR_MESSAGES = {
    1013: "Gemini encountered a temporary generation error",
    1037: "Gemini usage limit exceeded for the requested model",
    1050: "The requested Gemini model is inconsistent with the conversation",
    1052: "The requested Gemini model header is invalid or unavailable",
    1060: "Gemini temporarily blocked this IP address",
}


def iter_wrb_payloads(value):
    if isinstance(value, list):
        if len(value) >= 3 and value[0] == "wrb.fr" and isinstance(value[2], str):
            yield value[2]
            return
        for item in value:
            yield from iter_wrb_payloads(item)


def get_nested_value(value, path, default=None):
    current = value
    for key in path:
        if isinstance(key, int):
            if not isinstance(current, list) or not -len(current) <= key < len(current):
                return default
        elif not isinstance(current, dict) or key not in current:
            return default
        current = current[key]
    return default if current is None else current


def _utf16_char_count(value: str, start: int, units: int) -> tuple[int, int]:
    count = found = 0
    while found < units and start + count < len(value):
        size = 2 if ord(value[start + count]) > 0xFFFF else 1
        if found + size > units:
            break
        found += size
        count += 1
    return count, found


def parse_google_frames(content: str) -> tuple[list, str]:
    """Parse Google's length-prefixed frames, including UTF-16 lengths."""
    frames = []
    position = 0
    while position < len(content):
        while position < len(content) and content[position].isspace():
            position += 1
        if position >= len(content):
            break
        match = LENGTH_MARKER_PATTERN.match(content, position)
        if match is None:
            break
        length = int(match.group(1))
        start = match.start() + len(match.group(1))
        char_count, units_found = _utf16_char_count(content, start, length)
        if units_found < length:
            break
        end = start + char_count
        chunk = content[start:end].strip()
        position = end
        if not chunk:
            continue
        try:
            parsed = json.loads(chunk)
        except ValueError:
            continue
        if isinstance(parsed, list):
            frames.extend(parsed)
        else:
            frames.append(parsed)
    return frames, content[position:]


def _iter_google_json(content: str):
    content = content.lstrip()
    if content.startswith(")]}'"):
        content = content[4:].lstrip()
    frames, _ = parse_google_frames(content)
    if frames:
        yield from frames
        return
    try:
        parsed = json.loads(content)
    except ValueError:
        parsed = None
    if parsed is not None:
        if isinstance(parsed, list):
            yield from parsed
        else:
            yield parsed
        return
    for line in content.splitlines():
        line = line.strip()
        if not line or line.isdigit():
            continue
        try:
            parsed = json.loads(line)
        except ValueError:
            continue
        if isinstance(parsed, list):
            yield from parsed
        else:
            yield parsed


def _compute_model_capacity(
    tier_flags: list, capability_flags: list
) -> tuple[int, int]:
    if 21 in tier_flags:
        return 1, 13
    if 22 in tier_flags:
        return 2, 13
    if 115 in capability_flags:
        return 4, 12
    if 16 in tier_flags or 106 in capability_flags:
        return 3, 12
    if 8 in tier_flags or (106 not in capability_flags and 19 in capability_flags):
        return 2, 12
    return 1, 12


def _model_family(model_id: str, display_name: str, description: str) -> str | None:
    if model_id in KNOWN_MODEL_IDS:
        return KNOWN_MODEL_IDS[model_id]
    label = f"{display_name} {description}".lower()
    if "thinking" in label:
        return "thinking"
    if "pro" in label:
        return "pro"
    if "flash" in label:
        return "flash"
    return None


def build_model_headers(
    model_id: str, capacity: int, capacity_field: int
) -> dict[str, str]:
    header = [1, None, None, None, model_id, None, None, 0, [4], None, None]
    if capacity_field == 13:
        header.extend([None, capacity])
    else:
        header.append(capacity)
    return {
        MODEL_HEADER_KEY: json.dumps(header, separators=(",", ":")),
        **MODEL_HEADER_AUXILIARY,
    }


def parse_account_models(content: str) -> tuple[int | None, dict[str, dict]]:
    status_code = None
    registry = {}
    for frame in _iter_google_json(content):
        for payload in iter_wrb_payloads(frame):
            try:
                body = json.loads(payload)
            except (TypeError, ValueError):
                continue
            current_status = get_nested_value(body, [14])
            if isinstance(current_status, int):
                status_code = current_status
            models_list = get_nested_value(body, [15], [])
            if not isinstance(models_list, list):
                continue
            tier_flags = get_nested_value(body, [16], [])
            capability_flags = get_nested_value(body, [17], [])
            tier_flags = tier_flags if isinstance(tier_flags, list) else []
            capability_flags = (
                capability_flags if isinstance(capability_flags, list) else []
            )
            capacity, capacity_field = _compute_model_capacity(
                tier_flags, capability_flags
            )
            for model_data in models_list:
                if not isinstance(model_data, list):
                    continue
                label = get_nested_value(model_data, [-1], "")
                model_id = get_nested_value(model_data, [0], "")
                display_name = get_nested_value(model_data, [1], "")
                description = get_nested_value(model_data, [2], "")
                mode = get_nested_value(model_data, [17], "")
                if not isinstance(model_id, str) or not model_id:
                    continue
                family = _model_family(model_id, display_name, description)
                # The authenticated response commonly omits field 14. Gemini's
                # web client treats an absent status as the normal/available
                # state; explicit non-1000 values describe restricted states.
                available = status_code in (None, ACCOUNT_STATUS_AVAILABLE)
                if status_code == ACCOUNT_STATUS_UNAUTHENTICATED:
                    available = family == "flash"
                registry[model_id] = {
                    "id": model_id,
                    "label": label,
                    "mode": mode,
                    "family": family,
                    "display_name": display_name,
                    "description": description,
                    "capacity": capacity,
                    "capacity_field": capacity_field,
                    "available": available,
                    "headers": build_model_headers(model_id, capacity, capacity_field),
                }
    if registry and status_code is None:
        status_code = ACCOUNT_STATUS_AVAILABLE
    return status_code, registry


def extract_reasoning(response_part: list) -> str | None:
    candidates = get_nested_value(response_part, [4], [])
    if not isinstance(candidates, list):
        return None
    for candidate in candidates:
        reasoning = get_nested_value(candidate, [37, 0, 0])
        if isinstance(reasoning, str) and reasoning:
            return reasoning
    return None


def extract_gemini_error_code(value) -> int | None:
    if isinstance(value, str):
        match = BARD_ERROR_PATTERN.search(value)
        return int(match.group(1)) if match else None
    if not isinstance(value, list):
        return None
    if value and value[0] == "wrb.fr":
        code = get_nested_value(value, [5, 2, 0, 1, 0])
        if isinstance(code, int):
            return code
    for item in value:
        code = extract_gemini_error_code(item)
        if code is not None:
            return code
    return None


def raise_gemini_error(code: int, model: str) -> None:
    message = GEMINI_ERROR_MESSAGES.get(
        code, f"Gemini rejected the request with error code {code}"
    )
    if code in (1037, 1060):
        raise RateLimitError(f"{message}: {model}")
    raise ResponseError(f"{message}: {model}")