CoolFace
Apppublic

command2283/jetbrainsai2api

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
main.py1387 linesDownload Raw Back to root
1import json2import time3import uuid4import threading5import asyncio6import os7from typing import Any, AsyncGenerator, Dict, List, Optional, Union8 9import httpx10import uvicorn11import aiofiles12from fastapi import FastAPI, HTTPException, Depends, Header13from fastapi.middleware.cors import CORSMiddleware14from fastapi.responses import StreamingResponse15from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials16from pydantic import BaseModel, Field17 18# Configuration19DEFAULT_REQUEST_TIMEOUT = 30.020 21# Global variables22VALID_CLIENT_KEYS: set = set()23JETBRAINS_ACCOUNTS: list = []24current_account_index: int = 025account_rotation_lock = asyncio.Lock()26file_write_lock = asyncio.Lock()27models_data: Dict[str, Any] = {}28anthropic_model_mappings: Dict[str, str] = {}29http_client: Optional[httpx.AsyncClient] = None30 31# Pydantic Models32class ChatMessage(BaseModel):33    role: str34    content: Optional[Union[str, List[Dict[str, Any]]]] = None35    tool_calls: Optional[List[Dict[str, Any]]] = None36    tool_call_id: Optional[str] = None37 38 39class ChatCompletionRequest(BaseModel):40    model: str41    messages: List[ChatMessage]42    stream: bool = False43    temperature: Optional[float] = None44    max_tokens: Optional[int] = None45    top_p: Optional[float] = None46    tools: Optional[List[Dict[str, Any]]] = None47    stop: Optional[Union[str, List[str]]] = None48 49 50# --- Anthropic-Compatible Models ---51 52 53class AnthropicContentBlock(BaseModel):54    type: str55    text: Optional[str] = None56    tool_use_id: Optional[str] = None57    content: Optional[Union[str, List[Dict[str, Any]]]] = None58    id: Optional[str] = None59    name: Optional[str] = None60    input: Optional[Dict[str, Any]] = None61 62 63class AnthropicMessage(BaseModel):64    role: str65    content: Union[str, List[AnthropicContentBlock]]66 67 68class AnthropicTool(BaseModel):69    name: str70    description: Optional[str] = None71    input_schema: Dict[str, Any]72 73 74class AnthropicMessageRequest(BaseModel):75    model: str76    messages: List[AnthropicMessage]77    system: Optional[Union[str, List[Dict[str, Any]]]] = None78    max_tokens: int79    stream: bool = False80    temperature: Optional[float] = None81    top_p: Optional[float] = None82    tools: Optional[List[AnthropicTool]] = None83    stop_sequences: Optional[List[str]] = None84 85 86# --- Anthropic-Compatible Response Models ---87 88 89class AnthropicUsage(BaseModel):90    input_tokens: int91    output_tokens: int92 93 94class AnthropicResponseContent(BaseModel):95    type: str96    id: Optional[str] = None97    name: Optional[str] = None98    input: Optional[Dict[str, Any]] = None99    text: Optional[str] = None100 101 102class AnthropicResponseMessage(BaseModel):103    id: str104    type: str = "message"105    role: str = "assistant"106    model: str107    content: List[AnthropicResponseContent]108    stop_reason: Optional[str]109    stop_sequence: Optional[str] = None110    usage: AnthropicUsage111 112 113# --- End Anthropic Models ---114 115 116class ModelInfo(BaseModel):117    id: str118    object: str = "model"119    created: int120    owned_by: str121 122 123class ModelList(BaseModel):124    object: str = "list"125    data: List[ModelInfo]126 127 128class ChatCompletionChoice(BaseModel):129    message: ChatMessage130    index: int = 0131    finish_reason: str = "stop"132 133 134class ChatCompletionResponse(BaseModel):135    id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")136    object: str = "chat.completion"137    created: int = Field(default_factory=lambda: int(time.time()))138    model: str139    choices: List[ChatCompletionChoice]140    usage: Dict[str, int] = Field(141        default_factory=lambda: {142            "prompt_tokens": 0,143            "completion_tokens": 0,144            "total_tokens": 0,145        }146    )147 148 149class StreamChoice(BaseModel):150    delta: Dict[str, Any] = Field(default_factory=dict)151    index: int = 0152    finish_reason: Optional[str] = None153 154 155class StreamResponse(BaseModel):156    id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")157    object: str = "chat.completion.chunk"158    created: int = Field(default_factory=lambda: int(time.time()))159    model: str160    choices: List[StreamChoice]161 162 163# FastAPI App164app = FastAPI(title="JetBrains AI OpenAI Compatible API")165 166# Add CORS middleware167app.add_middleware(168    CORSMiddleware,169    allow_origins=["*"],  # Allows all origins170    allow_credentials=True,171    allow_methods=["*"],  # Allows all methods172    allow_headers=["*"],  # Allows all headers173)174 175security = HTTPBearer(auto_error=False)176 177 178# Helper functions179def load_models():180    """加载模型配置和映射规则"""181    global anthropic_model_mappings182    try:183        with open("models.json", "r", encoding="utf-8") as f:184            config = json.load(f)185 186        # 支持新格式(包含 models 和 anthropic_model_mappings)187        if isinstance(config, dict):188            if "models" in config:189                model_ids = config["models"]190                # 加载模型映射配置191                anthropic_model_mappings = config.get("anthropic_model_mappings", {})192                print(f"从 models.json 加载了 {len(anthropic_model_mappings)} 个模型映射规则")193            else:194                # 处理旧格式的字典(如果有其他字段但没有 models)195                model_ids = []196                anthropic_model_mappings = {}197                print("警告: models.json 使用非标准格式,没有找到 models 字段")198        # 支持旧格式(仅包含模型列表)199        elif isinstance(config, list):200            model_ids = config201            anthropic_model_mappings = {}202            print("警告: models.json 使用旧格式,没有找到模型映射配置")203        else:204            print("错误: models.json 格式不正确")205            return {"data": []}206 207        processed_models = []208        if isinstance(model_ids, list):209            for model_id in model_ids:210                if isinstance(model_id, str):211                    processed_models.append(212                        {213                            "id": model_id,214                            "object": "model",215                            "created": int(time.time()),216                            "owned_by": "jetbrains-ai",217                        }218                    )219 220        return {"data": processed_models}221    except Exception as e:222        print(f"加载 models.json 时出错: {e}")223        # 设置默认映射规则224        anthropic_model_mappings = {}225        return {"data": []}226 227 228async def _save_accounts_to_file():229    """将当前账户状态异步保存到文件"""230    async with file_write_lock:231        try:232            async with aiofiles.open("jetbrainsai.json", "w", encoding="utf-8") as f:233                await f.write(json.dumps(JETBRAINS_ACCOUNTS, indent=2))234        except Exception as e:235            print(f"保存 jetbrainsai.json 文件时出错: {e}")236 237 238def load_client_api_keys():239    """加载客户端 API 密钥(优先从环境变量读取)"""240    global VALID_CLIENT_KEYS241    242    # 首先尝试从环境变量读取243    client_keys_env = os.getenv("CLIENT_API_KEYS")244    if client_keys_env:245        keys = [key.strip() for key in client_keys_env.split(",") if key.strip()]246        if keys:247            VALID_CLIENT_KEYS = set(keys)248            print(f"从环境变量成功加载 {len(VALID_CLIENT_KEYS)} 个客户端 API 密钥")249            return250    251    # 回退到从文件读取252    try:253        with open("client_api_keys.json", "r", encoding="utf-8") as f:254            keys = json.load(f)255            if not isinstance(keys, list):256                print("警告: client_api_keys.json 应包含密钥列表")257                VALID_CLIENT_KEYS = set()258                return259            VALID_CLIENT_KEYS = set(keys)260            if not VALID_CLIENT_KEYS:261                print("警告: client_api_keys.json 为空")262            else:263                print(f"从文件成功加载 {len(VALID_CLIENT_KEYS)} 个客户端 API 密钥")264    except FileNotFoundError:265        print("错误: 未找到 client_api_keys.json 且未设置 CLIENT_API_KEYS 环境变量")266        VALID_CLIENT_KEYS = set()267    except Exception as e:268        print(f"加载 client_api_keys.json 时出错: {e}")269        VALID_CLIENT_KEYS = set()270 271 272def load_jetbrains_accounts():273    """加载 JetBrains AI 认证信息(优先从环境变量读取)"""274    global JETBRAINS_ACCOUNTS275    processed_accounts = []276    277    # 首先尝试从环境变量读取(支持JWT_TOKEN, LICENSE_IDS, AUTHORIZATION_TOKENS)278    jwt_tokens_env = os.getenv("JWT_TOKEN")279    license_ids_env = os.getenv("LICENSE_IDS")280    authorization_tokens_env = os.getenv("AUTHORIZATION_TOKENS")281    282    if jwt_tokens_env:283        # 解析逗号分隔的值284        jwt_tokens = [token.strip() for token in jwt_tokens_env.split(",") if token.strip()]285        license_ids = []286        authorization_tokens = []287        288        # 解析LICENSE_IDS(如果提供)289        if license_ids_env:290            license_ids = [lid.strip() for lid in license_ids_env.split(",") if lid.strip()]291        292        # 解析AUTHORIZATION_TOKENS(如果提供)293        if authorization_tokens_env:294            authorization_tokens = [auth.strip() for auth in authorization_tokens_env.split(",") if auth.strip()]295        296        # 按位置配对创建账户297        max_len = max(len(jwt_tokens), len(license_ids), len(authorization_tokens))298        for i in range(max_len):299            account = {300                "jwt": jwt_tokens[i] if i < len(jwt_tokens) else None,301                "licenseId": license_ids[i] if i < len(license_ids) else None,302                "authorization": authorization_tokens[i] if i < len(authorization_tokens) else None,303                "last_updated": int(time.time()),304                "has_quota": True,305                "last_quota_check": 0,306            }307            # 至少需要有jwt或者同时有licenseId和authorization308            if account["jwt"] or (account["licenseId"] and account["authorization"]):309                processed_accounts.append(account)310        311        if processed_accounts:312            JETBRAINS_ACCOUNTS = processed_accounts313            print(f"从环境变量成功加载 {len(JETBRAINS_ACCOUNTS)} 个 JetBrains AI 账户")314            # 打印账户配置概况315            for i, account in enumerate(JETBRAINS_ACCOUNTS):316                jwt_status = "✓" if account.get("jwt") else "✗"317                license_status = "✓" if account.get("licenseId") else "✗"318                auth_status = "✓" if account.get("authorization") else "✗"319                print(f"  账户 {i+1}: JWT({jwt_status}) LicenseId({license_status}) Authorization({auth_status})")320            return321    322    # 其次尝试从分别的环境变量读取(JWT_TOKEN_1, JWT_TOKEN_2, ...)323    for i in range(1, 11):  # 支持最多10个token324        jwt_token = os.getenv(f"JWT_TOKEN_{i}")325        license_id = os.getenv(f"LICENSE_ID_{i}")326        authorization = os.getenv(f"AUTHORIZATION_TOKEN_{i}")327        328        if jwt_token or (license_id and authorization):329            processed_accounts.append({330                "licenseId": license_id.strip() if license_id else None,331                "authorization": authorization.strip() if authorization else None,332                "jwt": jwt_token.strip() if jwt_token else None,333                "last_updated": int(time.time()),334                "has_quota": True,335                "last_quota_check": 0,336            })337    338    if processed_accounts:339        JETBRAINS_ACCOUNTS = processed_accounts340        print(f"从分别的环境变量成功加载 {len(JETBRAINS_ACCOUNTS)} 个 JetBrains AI 账户")341        return342    343    # 最后回退到从文件读取344    try:345        with open("jetbrainsai.json", "r", encoding="utf-8") as f:346            accounts_data = json.load(f)347 348        if not isinstance(accounts_data, list):349            print("警告: jetbrainsai.json 格式不正确,应为对象列表")350            JETBRAINS_ACCOUNTS = []351            return352 353        for account in accounts_data:354            processed_accounts.append(355                {356                    "licenseId": account.get("licenseId"),357                    "authorization": account.get("authorization"),358                    "jwt": account.get("jwt"),359                    "last_updated": account.get("last_updated", 0),360                    "has_quota": account.get("has_quota", True),361                    "last_quota_check": account.get("last_quota_check", 0),362                }363            )364 365        JETBRAINS_ACCOUNTS = processed_accounts366        if not JETBRAINS_ACCOUNTS:367            print("警告: jetbrainsai.json 中未找到有效的认证信息")368        else:369            print(f"从文件成功加载 {len(JETBRAINS_ACCOUNTS)} 个 JetBrains AI 账户")370 371    except FileNotFoundError:372        print("错误: 未找到 jetbrainsai.json 文件且未设置 JWT_TOKEN 环境变量")373        JETBRAINS_ACCOUNTS = []374    except Exception as e:375        print(f"加载 jetbrainsai.json 时出错: {e}")376        JETBRAINS_ACCOUNTS = []377 378 379def get_model_item(model_id: str) -> Optional[Dict]:380    """根据模型ID获取模型配置"""381    for model in models_data.get("data", []):382        if model.get("id") == model_id:383            return model384    return None385 386 387async def authenticate_client(388    auth: Optional[HTTPAuthorizationCredentials] = Depends(security),389):390    """客户端认证 (OpenAI-style)"""391    if not VALID_CLIENT_KEYS:392        raise HTTPException(status_code=503, detail="服务不可用: 未配置客户端 API 密钥")393 394    if not auth or not auth.credentials:395        raise HTTPException(396            status_code=401,397            detail="需要在 Authorization header 中提供 API 密钥",398            headers={"WWW-Authenticate": "Bearer"},399        )400 401    if auth.credentials not in VALID_CLIENT_KEYS:402        raise HTTPException(status_code=403, detail="无效的客户端 API 密钥")403 404 405async def authenticate_any_client(406    auth: Optional[HTTPAuthorizationCredentials] = Depends(security),407    api_key: Optional[str] = Header(None, alias="x-api-key"),408):409    """客户端认证 (支持 OpenAI 和 Anthropic 风格)"""410    if not VALID_CLIENT_KEYS:411        raise HTTPException(status_code=503, detail="服务不可用: 未配置客户端 API 密钥")412 413    # 优先检查 x-api-key414    if api_key:415        if api_key in VALID_CLIENT_KEYS:416            return417        else:418            raise HTTPException(419                status_code=403, detail="无效的客户端 API 密钥 (x-api-key)"420            )421 422    # 其次检查 Authorization header423    if auth and auth.credentials:424        if auth.credentials in VALID_CLIENT_KEYS:425            return426        else:427            raise HTTPException(428                status_code=403, detail="无效的客户端 API 密钥 (Bearer token)"429            )430 431    # 如果两者都未提供432    raise HTTPException(433        status_code=401,434        detail="需要在 Authorization header (Bearer) 或 x-api-key header 中提供 API 密钥",435        headers={"WWW-Authenticate": "Bearer"},436    )437 438 439async def authenticate_anthropic_client(440    api_key: Optional[str] = Header(None, alias="x-api-key"),441):442    """客户端认证 (Anthropic-style)"""443    if not VALID_CLIENT_KEYS:444        raise HTTPException(status_code=503, detail="服务不可用: 未配置客户端 API 密钥")445 446    if not api_key:447        raise HTTPException(448            status_code=401,449            detail="需要在 x-api-key header 中提供 API 密钥",450        )451 452    if api_key not in VALID_CLIENT_KEYS:453        raise HTTPException(status_code=403, detail="无效的客户端 API 密钥")454 455 456async def _check_quota(account: dict):457    """检查指定账户的配额"""458    if not http_client:459        raise HTTPException(status_code=500, detail="HTTP 客户端未初始化")460 461    # 对于基于许可证的账户,如果 JWT 不存在,则先刷新462    if not account.get("jwt") and account.get("licenseId"):463        await _refresh_jetbrains_jwt(account)464 465    if not account.get("jwt"):466        account["has_quota"] = False467        return468 469    try:470        headers = {471            "User-Agent": "ktor-client",472            "Content-Length": "0",473            "Accept-Charset": "UTF-8",474            "grazie-agent": '{"name":"aia:pycharm","version":"251.26094.80.13:251.26094.141"}',475            "grazie-authenticate-jwt": account["jwt"],476        }477        response = await http_client.post(478            "https://api.jetbrains.ai/user/v5/quota/get", headers=headers, timeout=10.0479        )480 481        if response.status_code == 401 and account.get("licenseId"):482            print(f"JWT for {account['licenseId']} expired, refreshing...")483            await _refresh_jetbrains_jwt(account)484            headers["grazie-authenticate-jwt"] = account["jwt"]485            response = await http_client.post(486                "https://api.jetbrains.ai/user/v5/quota/get", headers=headers, timeout=10.0487            )488 489        response.raise_for_status()490        quota_data = response.json()491        492        has_quota = quota_data.get("dailyUsed", 0) < quota_data.get("dailyTotal", 1)493        account["has_quota"] = has_quota494        if not has_quota:495            print(f"Account {account.get('licenseId') or 'with static JWT'} has no quota.")496 497    except Exception as e:498        print(f"Error checking quota for account: {e}")499        # On error, assume it has no quota to be safe500        account["has_quota"] = False501    finally:502        account["last_quota_check"] = time.time()503        await _save_accounts_to_file()504 505 506async def _refresh_jetbrains_jwt(account: dict):507    """使用 licenseId 和 authorization 刷新 JWT"""508    if not http_client:509        raise HTTPException(status_code=500, detail="HTTP 客户端未初始化")510 511    print(f"正在为 licenseId {account['licenseId']} 刷新 JWT...")512    try:513        headers = {514            "User-Agent": "ktor-client",515            "Content-Type": "application/json",516            "Accept-Charset": "UTF-8",517            "authorization": f"Bearer {account['authorization']}",518        }519        payload = {"licenseId": account["licenseId"]}520 521        response = await http_client.post(522            "https://api.jetbrains.ai/auth/jetbrains-jwt/provide-access/license/v2",523            json=payload,524            headers=headers,525            timeout=DEFAULT_REQUEST_TIMEOUT,526        )527        response.raise_for_status()528 529        data = response.json()530        if data.get("state") == "PAID" and "token" in data:531            account["jwt"] = data["token"]532            account["last_updated"] = time.time()533            print(f"成功刷新 licenseId {account['licenseId']} 的 JWT")534            await _save_accounts_to_file()535        else:536            print(f"刷新 JWT 失败: 无效的响应状态 {data.get('state')}")537            raise HTTPException(status_code=500, detail=f"刷新 JWT 失败: {data}")538 539    except httpx.HTTPStatusError as e:540        print(f"刷新 JWT 时 HTTP 错误: {e.response.status_code} {e.response.text}")541        raise HTTPException(542            status_code=e.response.status_code,543            detail=f"刷新 JWT 失败: {e.response.text}",544        )545    except Exception as e:546        print(f"刷新 JWT 时发生未知错误: {e}")547        raise HTTPException(status_code=500, detail=f"刷新 JWT 时发生未知错误: {e}")548 549 550async def get_next_jetbrains_account() -> dict:551    """轮询获取下一个有配额的 JetBrains 账户"""552    global current_account_index553 554    if not JETBRAINS_ACCOUNTS:555        raise HTTPException(status_code=503, detail="服务不可用: 未配置 JetBrains 账户")556 557    async with account_rotation_lock:558        start_index = current_account_index559        for _ in range(len(JETBRAINS_ACCOUNTS)):560            account = JETBRAINS_ACCOUNTS[current_account_index]561            current_account_index = (current_account_index + 1) % len(562                JETBRAINS_ACCOUNTS563            )564 565            # 如果状态是 stale,检查配额566            is_quota_stale = (567                time.time() - account.get("last_quota_check", 0) > 3600568            )  # 1 hour cache569            if account.get("has_quota") and is_quota_stale:570                await _check_quota(account)571 572            if account.get("has_quota"):573                # 如果是基于许可证的账户,检查 JWT 是否需要刷新574                if account.get("licenseId"):575                    is_jwt_stale = (576                        time.time() - account.get("last_updated", 0) > 12 * 3600577                    )578                    if not account.get("jwt") or is_jwt_stale:579                        await _refresh_jetbrains_jwt(account)580                        # 刷新 JWT 后可能需要重新检查配额581                        if not account.get("has_quota"):582                            await _check_quota(account)583                            if not account.get("has_quota"):584                                continue585 586                if account.get("jwt"):587                    return account588 589        # 循环完成,没有找到可用的账户590        raise HTTPException(status_code=429, detail="所有 JetBrains 账户均已超出配额或无效")591 592 593# FastAPI 生命周期事件594@app.on_event("startup")595async def startup():596    global models_data, http_client597    models_data = load_models()598    load_client_api_keys()599    load_jetbrains_accounts()600    http_client = httpx.AsyncClient(timeout=None)601    print("JetBrains AI OpenAI Compatible API 服务器已启动")602 603 604@app.on_event("shutdown")605async def shutdown():606    global http_client607    if http_client:608        await http_client.aclose()609 610 611# API 端点612@app.get("/v1/models", response_model=ModelList)613async def list_models(_: None = Depends(authenticate_any_client)):614    """列出可用模型"""615    model_list = [616        ModelInfo(617            id=model.get("id", ""),618            created=model.get("created", int(time.time())),619            owned_by=model.get("owned_by", "jetbrains-ai"),620        )621        for model in models_data.get("data", [])622    ]623    return ModelList(data=model_list)624 625 626async def openai_stream_adapter(627    api_stream_generator: AsyncGenerator[str, None],628    model_name: str,629    tools: Optional[List[Dict[str, Any]]],630) -> AsyncGenerator[str, None]:631    """将 JetBrains API 的流转换为 OpenAI 格式的 SSE"""632    stream_id = f"chatcmpl-{uuid.uuid4().hex}"633    first_chunk_sent = False634    tool_id = 0635 636    try:637        async for line in api_stream_generator:638            if not line or line == "data: end":639                continue640 641            if line.startswith("data: "):642                try:643                    data = json.loads(line[6:])644                    event_type = data.get("type")645 646                    if event_type == "Content":647                        content = data.get("content", "")648                        if not content:649                            continue650 651                        delta_payload = {}652                        if not first_chunk_sent:653                            delta_payload = {"role": "assistant", "content": content}654                            first_chunk_sent = True655                        else:656                            delta_payload = {"content": content}657 658                        stream_resp = StreamResponse(659                            id=stream_id,660                            model=model_name,661                            choices=[StreamChoice(delta=delta_payload)],662                        )663                        yield f"data: {stream_resp.json()}\n\n"664 665                    elif event_type == "FunctionCall":666                        func_name = data.get("name", None)667                        func_argu = data.get("content", None)668                        if func_name and tools:669                            for tool_id, tool in enumerate(tools):670                                if tool["name"] == func_name:671                                    break672 673                        delta_payload = {674                            "tool_calls": [675                                {676                                    "index": tool_id,677                                    "id": f"call_{uuid.uuid4().hex}",678                                    "function": {679                                        "arguments": func_argu,680                                        "name": func_name,681                                    },682                                    "type": "function" if func_name else None,683                                }684                            ]685                        }686                        stream_resp = StreamResponse(687                            id=stream_id,688                            model=model_name,689                            choices=[StreamChoice(delta=delta_payload)],690                        )691                        yield f"data: {stream_resp.json()}\n\n"692 693                    elif event_type == "FinishMetadata":694                        final_resp = StreamResponse(695                            id=stream_id,696                            model=model_name,697                            choices=[StreamChoice(delta={}, finish_reason="stop")],698                        )699                        yield f"data: {final_resp.json()}\n\n"700                        break701                except json.JSONDecodeError:702                    print(f"警告: 无法解析的 JSON 行: {line}")703                    continue704 705        yield "data: [DONE]\n\n"706 707    except Exception as e:708        print(f"流式适配器错误: {e}")709        error_resp = StreamResponse(710            id=stream_id,711            model=model_name,712            choices=[713                StreamChoice(714                    delta={"role": "assistant", "content": f"内部错误: {str(e)}"},715                    index=0,716                    finish_reason="stop",717                )718            ],719        )720        yield f"data: {error_resp.json()}\n\n"721        yield "data: [DONE]\n\n"722 723 724async def aggregate_stream_for_non_stream_response(725    openai_sse_stream: AsyncGenerator[str, None], model_name: str726) -> ChatCompletionResponse:727    """聚合流式响应为完整响应"""728    content_parts = []729    tool_calls_map = {}730    final_finish_reason = "stop"731 732    async for sse_line in openai_sse_stream:733        if sse_line.startswith("data: ") and sse_line.strip() != "data: [DONE]":734            try:735                data = json.loads(sse_line[6:].strip())736                if not data.get("choices"):737                    continue738 739                choice = data["choices"][0]740                delta = choice.get("delta", {})741 742                if choice.get("finish_reason"):743                    final_finish_reason = choice.get("finish_reason")744 745                if delta.get("content"):746                    content_parts.append(delta["content"])747 748                if "tool_calls" in delta:749                    for tc_chunk in delta["tool_calls"]:750                        idx = tc_chunk["index"]751                        if idx not in tool_calls_map:752                            tool_calls_map[idx] = {753                                "type": "function",754                                "function": {"name": "", "arguments": ""},755                            }756 757                        if tc_chunk.get("id"):758                            tool_calls_map[idx]["id"] = tc_chunk["id"]759 760                        func_chunk = tc_chunk.get("function", {})761                        if func_chunk.get("name"):762                            tool_calls_map[idx]["function"]["name"] = func_chunk["name"]763                        if func_chunk.get("arguments"):764                            tool_calls_map[idx]["function"]["arguments"] += func_chunk[765                                "arguments"766                            ]767            except json.JSONDecodeError:768                print(f"警告: 聚合时无法解析的 JSON 行: {sse_line}")769 770    final_tool_calls = []771    for k, v in sorted(tool_calls_map.items()):772        if "id" not in v:773            v["id"] = f"call_{uuid.uuid4().hex}"774        final_tool_calls.append(v)775 776    full_content = "".join(content_parts) or None777 778    if final_tool_calls:779        message = ChatMessage(780            role="assistant", content=full_content, tool_calls=final_tool_calls781        )782        final_finish_reason = "tool_calls"783    else:784        message = ChatMessage(role="assistant", content=full_content)785 786    return ChatCompletionResponse(787        model=model_name,788        choices=[789            ChatCompletionChoice(790                message=message,791                finish_reason=final_finish_reason,792            )793        ],794    )795 796 797def extract_text_content(content: Optional[Union[str, List[Dict[str, Any]]]]) -> str:798    """从消息内容中提取文本内容"""799    if isinstance(content, str):800        return content801    elif isinstance(content, list):802        # 处理多模态消息格式,提取所有文本内容803        text_parts = []804        for item in content:805            if isinstance(item, dict) and item.get("type") == "text":806                text_parts.append(item.get("text", ""))807        return " ".join(text_parts)808    return ""809 810 811@app.post("/v1/chat/completions")812async def chat_completions(813    request: ChatCompletionRequest, _: None = Depends(authenticate_client)814):815    """创建聊天完成"""816    model_config = get_model_item(request.model)817    if not model_config:818        raise HTTPException(status_code=404, detail=f"模型 {request.model} 未找到")819 820    account = await get_next_jetbrains_account()821    auth_token = account["jwt"]822 823    # 从历史消息中创建 tool_call_id 到 function_name 的映射824    tool_id_to_func_name_map = {}825    for m in request.messages:826        if m.role == "assistant" and m.tool_calls:827            for tc in m.tool_calls:828                if tc.get("id") and tc.get("function", {}).get("name"):829                    tool_id_to_func_name_map[tc["id"]] = tc["function"]["name"]830 831    # 将 OpenAI 格式的消息转换为 JetBrains 格式832    jetbrains_messages = []833    for msg in request.messages:834        # 提取文本内容,处理多模态消息格式835        text_content = extract_text_content(msg.content)836 837        if msg.role in ["user", "system"]:838            jetbrains_messages.append(839                {"type": f"{msg.role}_message", "content": text_content}840            )841 842        elif msg.role == "assistant":843            if msg.tool_calls:844                # 只处理第一个工具调用,以匹配 JetBrains API 的限制845                first_tool_call = msg.tool_calls[0]846                tool_id_to_func_name_map[first_tool_call["id"]] = first_tool_call[847                    "function"848                ]["name"]849                jetbrains_messages.append(850                    {851                        "type": "assistant_message",852                        "content": text_content,853                        "functionCall": {854                            "functionName": first_tool_call["function"]["name"],855                            "content": first_tool_call["function"]["arguments"],856                        },857                    }858                )859            else:860                jetbrains_messages.append(861                    {"type": "assistant_message", "content": text_content}862                )863 864        elif msg.role == "tool":865            function_name = tool_id_to_func_name_map.get(msg.tool_call_id)866            if function_name:867                jetbrains_messages.append(868                    {869                        "type": "function_message",870                        "content": text_content,871                        "functionName": function_name,872                    }873                )874            else:875                print(876                    f"警告: 无法为 tool_call_id {msg.tool_call_id} 找到对应的函数调用"877                )878        else:879            jetbrains_messages.append({"type": "user_message", "content": text_content})880 881    data = []882    tools = None883    if request.tools:884        data.append({"type": "json", "fqdn": "llm.parameters.functions"})885        tools = [t["function"] for t in request.tools]886        data.append({"type": "json", "value": json.dumps(tools)})887 888    # 创建 API 请求的 payload889    payload = {890        "prompt": "ij.chat.request.new-chat-on-start",891        "profile": request.model,892        "chat": {"messages": jetbrains_messages},893        "parameters": {"data": data},894    }895 896    headers = {897        "User-Agent": "ktor-client",898        "Accept": "text/event-stream",899        "Content-Type": "application/json",900        "Accept-Charset": "UTF-8",901        "Cache-Control": "no-cache",902        "grazie-agent": '{"name":"aia:pycharm","version":"251.26094.80.13:251.26094.141"}',903        "grazie-authenticate-jwt": auth_token,904    }905 906    async def api_stream_generator():907        """一个包装 httpx 请求的异步生成器"""908        try:909            async with http_client.stream(910                "POST",911                "https://api.jetbrains.ai/user/v5/llm/chat/stream/v8",912                json=payload,913                headers=headers,914            ) as response:915                if response.status_code == 477:916                    print(f"Account {account.get('licenseId') or 'with static JWT'} has no quota (received 477).")917                    account["has_quota"] = False918                    account["last_quota_check"] = time.time()919                    await _save_accounts_to_file()920                response.raise_for_status()921                async for line in response.aiter_lines():922                    yield line923        except httpx.HTTPStatusError as e:924            if e.response.status_code == 477:925                print(f"Account {account.get('licenseId') or 'with static JWT'} has no quota (received 477).")926                account["has_quota"] = False927                account["last_quota_check"] = time.time()928                await _save_accounts_to_file()929            raise e930 931    # 创建 OpenAI 格式的流932    openai_sse_stream = openai_stream_adapter(933        api_stream_generator(), request.model, tools or []934    )935 936    # 返回流式或非流式响应937    if request.stream:938        return StreamingResponse(openai_sse_stream, media_type="text/event-stream")939    else:940        return await aggregate_stream_for_non_stream_response(941            openai_sse_stream, request.model942        )943 944 945def convert_anthropic_to_openai(946    anthropic_req: AnthropicMessageRequest,947) -> ChatCompletionRequest:948    openai_messages = []949    tool_id_to_func_name_map = {}950 951    if anthropic_req.system:952        system_prompt = ""953        if isinstance(anthropic_req.system, str):954            system_prompt = anthropic_req.system955        elif isinstance(anthropic_req.system, list):956            system_prompt = " ".join(957                [958                    item.get("text", "")959                    for item in anthropic_req.system960                    if isinstance(item, dict) and item.get("type") == "text"961                ]962            )963        if system_prompt:964            openai_messages.append(ChatMessage(role="system", content=system_prompt))965 966    for msg in anthropic_req.messages:967        if msg.role == "user":968            text_parts = []969            if isinstance(msg.content, str):970                text_parts.append(msg.content)971            else:972                for block in msg.content:973                    if block.type == "text":974                        text_parts.append(block.text)975                    elif block.type == "tool_result" and block.tool_use_id:976                        content_str = (977                            block.content978                            if isinstance(block.content, str)979                            else json.dumps(block.content)980                        )981                        openai_messages.append(982                            ChatMessage(983                                role="tool",984                                tool_call_id=block.tool_use_id,985                                content=content_str,986                            )987                        )988 989            if text_parts:990                openai_messages.append(991                    ChatMessage(role="user", content=" ".join(text_parts))992                )993 994        elif msg.role == "assistant":995            text_parts = []996            tool_calls = []997            if isinstance(msg.content, list):998                for block in msg.content:999                    if block.type == "text":1000                        text_parts.append(block.text)1001                    elif block.type == "tool_use" and block.id and block.name:1002                        arguments = (1003                            json.dumps(block.input) if block.input is not None else "{}"1004                        )1005                        tool_calls.append(1006                            {1007                                "id": block.id,1008                                "type": "function",1009                                "function": {1010                                    "name": block.name,1011                                    "arguments": arguments,1012                                },1013                            }1014                        )1015                        tool_id_to_func_name_map[block.id] = block.name1016 1017            content_text = " ".join(text_parts) if text_parts else None1018            openai_messages.append(1019                ChatMessage(1020                    role="assistant",1021                    content=content_text,1022                    tool_calls=tool_calls if tool_calls else None,1023                )1024            )1025 1026    openai_tools = None1027    if anthropic_req.tools:1028        openai_tools = [1029            {1030                "type": "function",1031                "function": {1032                    "name": t.name,1033                    "description": t.description,1034                    "parameters": t.input_schema,1035                },1036            }1037            for t in anthropic_req.tools1038        ]1039 1040    return ChatCompletionRequest(1041        model=anthropic_req.model,1042        messages=openai_messages,1043        stream=anthropic_req.stream,1044        temperature=anthropic_req.temperature,1045        max_tokens=anthropic_req.max_tokens,1046        top_p=anthropic_req.top_p,1047        tools=openai_tools,1048        stop=anthropic_req.stop_sequences,1049    )1050 1051 1052def map_finish_reason(finish_reason: Optional[str]) -> Optional[str]:1053    if finish_reason == "stop":1054        return "end_turn"1055    if finish_reason == "length":1056        return "max_tokens"1057    if finish_reason == "tool_calls":1058        return "tool_use"1059    return finish_reason1060 1061 1062async def openai_to_anthropic_stream_adapter(1063    openai_stream: AsyncGenerator[str, None], model_name: str1064) -> AsyncGenerator[str, None]:1065    message_id = f"msg_{uuid.uuid4().hex.replace('-', '')}"1066    yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': {'id': message_id, 'type': 'message', 'role': 'assistant', 'model': model_name, 'content': [], 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"1067    yield f"event: ping\ndata: {json.dumps({'type': 'ping'})}\n\n"1068 1069    content_block_index = 01070    text_block_open = False1071    tool_blocks = {}  # index -> {id, name, args}1072 1073    async for sse_line in openai_stream:1074        if not sse_line.startswith("data:") or sse_line.strip() == "data: [DONE]":1075            continue1076 1077        data_str = sse_line[6:].strip()1078        try:1079            data = json.loads(data_str)1080            if not data.get("choices"):1081                continue1082 1083            delta = data["choices"][0].get("delta", {})1084            finish_reason = data["choices"][0].get("finish_reason")1085 1086            if delta.get("content"):1087                if not text_block_open:1088                    yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': content_block_index, 'content_block': {'type': 'text', 'text': ''}})}\n\n"1089                    text_block_open = True1090 1091                yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': content_block_index, 'delta': {'type': 'text_delta', 'text': delta['content']}})}\n\n"1092 1093            if delta.get("tool_calls"):1094                if text_block_open:1095                    yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"1096                    text_block_open = False1097                    content_block_index += 11098 1099                for tc in delta["tool_calls"]:1100                    idx = tc["index"]1101                    if idx not in tool_blocks:1102                        tool_blocks[idx] = {1103                            "id": tc.get("id"),1104                            "name": tc.get("function", {}).get("name"),1105                            "args": "",1106                        }1107                        yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': content_block_index + idx, 'content_block': {'type': 'tool_use', 'id': tc.get('id'), 'name': tc.get('function', {}).get('name'), 'input': {}}})}\n\n"1108 1109                    if tc.get("function", {}).get("arguments"):1110                        args_delta = tc["function"]["arguments"]1111                        tool_blocks[idx]["args"] += args_delta1112                        yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': content_block_index + idx, 'delta': {'type': 'input_json_delta', 'partial_json': args_delta}})}\n\n"1113 1114            if finish_reason:1115                if text_block_open:1116                    yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index})}\n\n"1117 1118                for i in range(len(tool_blocks)):1119                    yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': content_block_index + i})}\n\n"1120 1121                message_delta_data = {1122                    "type": "message_delta",1123                    "delta": {1124                        "stop_reason": map_finish_reason(finish_reason),1125                        "stop_sequence": None,1126                    },1127                    "usage": {"input_tokens": 0, "output_tokens": 0},1128                }1129                yield f"event: message_delta\ndata: {json.dumps(message_delta_data)}\n\n"1130                break1131        except json.JSONDecodeError:1132            print(f"Anthropic adapter JSON decode error: {data_str}")1133            continue1134 1135    yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"1136 1137 1138def convert_openai_to_anthropic_response(1139    resp: ChatCompletionResponse,1140) -> AnthropicResponseMessage:1141    message = resp.choices[0].message1142    content_blocks = []1143 1144    if message.content:1145        content_blocks.append(1146            AnthropicResponseContent(type="text", text=message.content)1147        )1148 1149    if message.tool_calls:1150        for tc in message.tool_calls:1151            try:1152                tool_input = json.loads(tc["function"]["arguments"])1153            except json.JSONDecodeError:1154                tool_input = {1155                    "error": "invalid JSON in arguments",1156                    "arguments": tc["function"]["arguments"],1157                }1158            content_blocks.append(1159                AnthropicResponseContent(1160                    type="tool_use",1161                    id=tc["id"],1162                    name=tc["function"]["name"],1163                    input=tool_input,1164                )1165            )1166 1167    return AnthropicResponseMessage(1168        id=resp.id.replace("chatcmpl-", "msg_"),1169        model=resp.model,1170        content=content_blocks,1171        stop_reason=map_finish_reason(resp.choices[0].finish_reason),1172        usage=AnthropicUsage(1173            input_tokens=resp.usage.get("prompt_tokens", 0),1174            output_tokens=resp.usage.get("completion_tokens", 0),1175        ),1176    )1177 1178 1179@app.post("/v1/messages", response_model=None)1180async def messages_completions(1181    request: AnthropicMessageRequest, _: None = Depends(authenticate_anthropic_client)1182):1183    """创建符合 Anthropic 规范的聊天完成"""1184    openai_request = convert_anthropic_to_openai(request)1185 1186    # Apply model mapping specifically for /v1/messages endpoint using config from models.json1187    if openai_request.model in anthropic_model_mappings:1188        original_model = openai_request.model1189        openai_request.model = anthropic_model_mappings[openai_request.model]1190        print(f"Model mapping applied: {original_model} -> {openai_request.model}")1191 1192    model_config = get_model_item(openai_request.model)1193    if not model_config:1194        raise HTTPException(1195            status_code=404, detail=f"模型 {openai_request.model} 未找到"1196        )1197 1198    account = await get_next_jetbrains_account()1199    auth_token = account["jwt"]1200 

Showing the first 1,200 of 1387 lines. Download the file for the rest.